diff options
Diffstat (limited to 'heatmaps/heatmap_generator.py')
-rw-r--r-- | heatmaps/heatmap_generator.py | 524 |
1 files changed, 0 insertions, 524 deletions
diff --git a/heatmaps/heatmap_generator.py b/heatmaps/heatmap_generator.py deleted file mode 100644 index 703c37d4..00000000 --- a/heatmaps/heatmap_generator.py +++ /dev/null @@ -1,524 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2018 The ChromiumOS Authors -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Python module to draw heat map for Chrome - -heat map is a histogram used to analyze the locality of function layout. - -This module is used by heat_map.py. HeatmapGenerator is a class to -generate data for drawing heat maps (the actual drawing of heat maps is -performed by another script perf-to-inst-page.sh). It can also analyze -the symbol names in hot pages. -""" - - -import bisect -import collections -import os -import pipes -import subprocess - -from cros_utils import command_executer - - -HugepageRange = collections.namedtuple("HugepageRange", ["start", "end"]) - - -class MMap(object): - """Class to store mmap information in perf report. - - We assume ASLR is disabled, so MMap for all Chrome is assumed to be - the same. This class deals with the case hugepage creates several - mmaps for Chrome but should be merged together. In these case, we - assume the first MMAP is not affected by the bug and use the MMAP. - """ - - def __init__(self, addr, size, offset): - self.start_address = addr - self.size = size - self.offset = offset - - def __str__(self): - return "(%x, %x, %x)" % (self.start_address, self.size, self.offset) - - def merge(self, mmap): - # This function should not be needed, since we should only have - # one MMAP on Chrome of each process. This function only deals with - # images that is affected by http://crbug.com/931465. - - # This function is only checking a few conditions to make sure - # the bug is within our expectation. - - if self.start_address == mmap.start_address: - assert ( - self.size >= mmap.size - ), "Original MMAP size(%x) is smaller than the forked process(%x)." % ( - self.size, - mmap.size, - ) - # The case that the MMAP is forked from the previous process - # No need to do anything, OR - # The case where hugepage causes a small Chrome mmap. - # In this case, we use the prior MMAP for the whole Chrome - return - - assert self.start_address < mmap.start_address, ( - "Original MMAP starting address(%x) is larger than the forked" - "process(%x)." % (self.start_address, mmap.start_address) - ) - - assert ( - self.start_address + self.size >= mmap.start_address + mmap.size - ), "MMAP of the forked process exceeds the end of original MMAP." - - -class HeatmapGenerator(object): - """Class to generate heat map with a perf report, containing mmaps and - - samples. This class contains two interfaces with other modules: - draw() and analyze(). - - draw() draws a heatmap with the sample information given in the perf report - analyze() prints out the symbol names in hottest pages with the given - chrome binary - """ - - def __init__( - self, perf_report, page_size, hugepage, title, log_level="verbose" - ): - self.perf_report = perf_report - # Pick 1G as a relatively large number. All addresses less than it will - # be recorded. The actual heatmap will show up to a boundary of the - # largest address in text segment. - self.max_addr = 1024 * 1024 * 1024 - self.ce = command_executer.GetCommandExecuter(log_level=log_level) - self.dir = os.path.dirname(os.path.realpath(__file__)) - with open(perf_report, "r", encoding="utf-8") as f: - self.perf_report_contents = f.readlines() - # Write histogram results to a text file, in order to use gnu plot to draw - self.hist_temp_output = open("out.txt", "w", encoding="utf-8") - self.processes = {} - self.deleted_processes = {} - self.count = 0 - if hugepage: - self.hugepage = HugepageRange(start=hugepage[0], end=hugepage[1]) - else: - self.hugepage = None - self.title = title - self.symbol_addresses = [] - self.symbol_names = [] - self.page_size = page_size - - def _parse_perf_sample(self, line): - # In a perf report, generated with -D, a PERF_RECORD_SAMPLE command should - # look like this: TODO: some arguments are unknown - # - # cpuid cycle unknown [unknown]: PERF_RECORD_SAMPLE(IP, 0x2): pid/tid: - # 0xaddr period: period addr: addr - # ... thread: threadname:tid - # ...... dso: process - # - # This is an example: - # 1 136712833349 0x6a558 [0x30]: PERF_RECORD_SAMPLE(IP, 0x2): 5227/5227: - # 0x55555683b810 period: 372151 addr: 0 - # ... thread: chrome:5227 - # ...... dso: /opt/google/chrome/chrome - # - # For this function, the 7th argument (args[6]) after spltting with spaces - # is pid/tid. We use the combination of the two as the pid. - # Also, we add an assertion here to check the tid in the 7th argument( - # args[6]) and the 15th argument(arg[14]) are the same - # - # The function returns the ((pid,tid), address) pair if the sampling - # is on Chrome. Otherwise, return (None, None) pair. - - if ( - "thread: chrome" not in line - or "dso: /opt/google/chrome/chrome" not in line - ): - return None, None - args = line.split(" ") - pid_raw = args[6].split("/") - assert ( - pid_raw[1][:-1] == args[14].split(":")[1][:-1] - ), "TID in %s of sample is not the same: %s/%s" % ( - line[:-1], - pid_raw[1][:-1], - args[14].split(":")[1][:-1], - ) - key = (int(pid_raw[0]), int(pid_raw[1][:-1])) - address = int(args[7], base=16) - return key, address - - def _parse_perf_record(self, line): - # In a perf report, generated with -D, a PERF_RECORD_MMAP2 command should - # look like this: TODO: some arguments are unknown - # - # cpuid cycle unknown [unknown]: PERF_RECORD_MMAP2 pid/tid: - # [0xaddr(0xlength) @ pageoffset maj:min ino ino_generation]: - # permission process - # - # This is an example. - # 2 136690556823 0xa6898 [0x80]: PERF_RECORD_MMAP2 5227/5227: - # [0x555556496000(0x8d1b000) @ 0xf42000 b3:03 92844 1892514370]: - # r-xp /opt/google/chrome/chrome - # - # For this function, the 6th argument (args[5]) after spltting with spaces - # is pid/tid. We use the combination of the two as the pid. - # The 7th argument (args[6]) is the [0xaddr(0xlength). We can peel the - # string to get the address and size of the mmap. - # The 9th argument (args[8]) is the page offset. - # The function returns the ((pid,tid), mmap) pair if the mmap is for Chrome - # is on Chrome. Otherwise, return (None, None) pair. - - if "chrome/chrome" not in line: - return None, None - args = line.split(" ") - pid_raw = args[5].split("/") - assert ( - pid_raw[0] == pid_raw[1][:-1] - ), "PID in %s of mmap is not the same: %s/%s" % ( - line[:-1], - pid_raw[0], - pid_raw[1], - ) - pid = (int(pid_raw[0]), int(pid_raw[1][:-1])) - address_raw = args[6].split("(") - start_address = int(address_raw[0][1:], base=16) - size = int(address_raw[1][:-1], base=16) - offset = int(args[8], base=16) - # Return an mmap object instead of only starting address, - # in case there are many mmaps for the sample PID - return pid, MMap(start_address, size, offset) - - def _parse_pair_event(self, arg): - # This function is called by the _parse_* functions that has a pattern of - # pids like: (pid:tid):(pid:tid), i.e. - # PERF_RECORD_FORK and PERF_RECORD_COMM - _, remain = arg.split("(", 1) - pid1, remain = remain.split(":", 1) - pid2, remain = remain.split(")", 1) - _, remain = remain.split("(", 1) - pid3, remain = remain.split(":", 1) - pid4, remain = remain.split(")", 1) - return (int(pid1), int(pid2)), (int(pid3), int(pid4)) - - def _process_perf_record(self, line): - # This function calls _parse_perf_record() to get information from - # PERF_RECORD_MMAP2. It records the mmap object for each pid (a pair of - # pid,tid), into a dictionary. - pid, mmap = self._parse_perf_record(line) - if pid is None: - # PID = None meaning the mmap is not for chrome - return - if pid in self.processes: - # This should never happen for a correct profiling result, as we - # should only have one MMAP for Chrome for each process. - # If it happens, see http://crbug.com/931465 - self.processes[pid].merge(mmap) - else: - self.processes[pid] = mmap - - def _process_perf_fork(self, line): - # In a perf report, generated with -D, a PERF_RECORD_FORK command should - # look like this: - # - # cpuid cycle unknown [unknown]: - # PERF_RECORD_FORK(pid_to:tid_to):(pid_from:tid_from) - # - # This is an example. - # 0 0 0x22a8 [0x38]: PERF_RECORD_FORK(1:1):(0:0) - # - # In this function, we need to peel the information of pid:tid pairs - # So we get the last argument and send it to function _parse_pair_event() - # for analysis. - # We use (pid, tid) as the pid. - args = line.split(" ") - pid_to, pid_from = self._parse_pair_event(args[-1]) - if pid_from in self.processes: - assert pid_to not in self.processes - self.processes[pid_to] = MMap( - self.processes[pid_from].start_address, - self.processes[pid_from].size, - self.processes[pid_from].offset, - ) - - def _process_perf_exit(self, line): - # In a perf report, generated with -D, a PERF_RECORD_EXIT command should - # look like this: - # - # cpuid cycle unknown [unknown]: - # PERF_RECORD_EXIT(pid1:tid1):(pid2:tid2) - # - # This is an example. - # 1 136082505621 0x30810 [0x38]: PERF_RECORD_EXIT(3851:3851):(3851:3851) - # - # In this function, we need to peel the information of pid:tid pairs - # So we get the last argument and send it to function _parse_pair_event() - # for analysis. - # We use (pid, tid) as the pid. - args = line.split(" ") - pid_to, pid_from = self._parse_pair_event(args[-1]) - assert pid_to == pid_from, "(%d, %d) (%d, %d)" % ( - pid_to[0], - pid_to[1], - pid_from[0], - pid_from[1], - ) - if pid_to in self.processes: - # Don't delete the process yet - self.deleted_processes[pid_from] = self.processes[pid_from] - - def _process_perf_sample(self, line): - # This function calls _parse_perf_sample() to get information from - # the perf report. - # It needs to check the starting address of allocated mmap from - # the dictionary (self.processes) to calculate the offset within - # the text section of the sampling. - # The offset is calculated into pages (4KB or 2MB) and writes into - # out.txt together with the total counts, which will be used to - # calculate histogram. - pid, addr = self._parse_perf_sample(line) - if pid is None: - return - - assert ( - pid in self.processes and pid not in self.deleted_processes - ), "PID %d not found mmap and not forked from another process" - - start_address = self.processes[pid].start_address - address = addr - start_address - assert ( - address >= 0 - and "addresses accessed in PERF_RECORD_SAMPLE should be larger than" - " the starting address of Chrome" - ) - if address < self.max_addr: - self.count += 1 - line = "%d/%d: %d %d" % ( - pid[0], - pid[1], - self.count, - address // self.page_size * self.page_size, - ) - if self.hugepage: - if self.hugepage.start <= address < self.hugepage.end: - line += " hugepage" - else: - line += " smallpage" - print(line, file=self.hist_temp_output) - - def _read_perf_report(self): - # Serve as main function to read perf report, generated by -D - lines = iter(self.perf_report_contents) - for line in lines: - if "PERF_RECORD_MMAP" in line: - self._process_perf_record(line) - elif "PERF_RECORD_FORK" in line: - self._process_perf_fork(line) - elif "PERF_RECORD_EXIT" in line: - self._process_perf_exit(line) - elif "PERF_RECORD_SAMPLE" in line: - # Perf sample is multi-line - self._process_perf_sample(line + next(lines) + next(lines)) - self.hist_temp_output.close() - - def _draw_heat_map(self): - # Calls a script (perf-to-inst-page.sh) to calculate histogram - # of results written in out.txt and also generate pngs for - # heat maps. - heatmap_script = os.path.join(self.dir, "perf-to-inst-page.sh") - if self.hugepage: - hp_arg = "hugepage" - else: - hp_arg = "none" - - cmd = "{0} {1} {2}".format( - heatmap_script, pipes.quote(self.title), hp_arg - ) - retval = self.ce.RunCommand(cmd) - if retval: - raise RuntimeError("Failed to run script to generate heatmap") - - def _restore_histogram(self): - # When hugepage is used, there are two files inst-histo-{hp,sp}.txt - # So we need to read in all the files. - names = [ - x for x in os.listdir(".") if "inst-histo" in x and ".txt" in x - ] - hist = {} - for n in names: - with open(n, encoding="utf-8") as f: - for l in f.readlines(): - num, addr = l.strip().split(" ") - assert int(addr) not in hist - hist[int(addr)] = int(num) - return hist - - def _read_symbols_from_binary(self, binary): - # FIXME: We are using nm to read symbol names from Chrome binary - # for now. Can we get perf to hand us symbol names, instead of - # using nm in the future? - # - # Get all the symbols (and their starting addresses) that fall into - # the page. Will be used to print out information of hot pages - # Each line shows the information of a symbol: - # [symbol value (0xaddr)] [symbol type] [symbol name] - # For some symbols, the [symbol name] field might be missing. - # e.g. - # 0000000001129da0 t Builtins_LdaNamedPropertyHandler - - # Generate a list of symbols from nm tool and check each line - # to extract symbols names - text_section_start = 0 - for l in subprocess.check_output(["nm", "-n", binary]).split("\n"): - args = l.strip().split(" ") - if len(args) < 3: - # No name field - continue - addr_raw, symbol_type, name = args - addr = int(addr_raw, base=16) - if "t" not in symbol_type and "T" not in symbol_type: - # Filter out symbols not in text sections - continue - if not self.symbol_addresses: - # The first symbol in text sections - text_section_start = addr - self.symbol_addresses.append(0) - self.symbol_names.append(name) - else: - assert ( - text_section_start != 0 - ), "The starting address of text section has not been found" - if addr == self.symbol_addresses[-1]: - # if the same address has multiple symbols, put them together - # and separate symbol names with '/' - self.symbol_names[-1] += "/" + name - else: - # The output of nm -n command is already sorted by address - # Insert to the end will result in a sorted array for bisect - self.symbol_addresses.append(addr - text_section_start) - self.symbol_names.append(name) - - def _map_addr_to_symbol(self, addr): - # Find out the symbol name - assert self.symbol_addresses - index = bisect.bisect(self.symbol_addresses, addr) - assert ( - 0 < index <= len(self.symbol_names) - ), "Failed to find an index (%d) in the list (len=%d)" % ( - index, - len(self.symbol_names), - ) - return self.symbol_names[index - 1] - - def _print_symbols_in_hot_pages(self, fp, pages_to_show): - # Print symbols in all the pages of interest - for page_num, sample_num in pages_to_show: - print( - "----------------------------------------------------------", - file=fp, - ) - print( - "Page Offset: %d MB, Count: %d" - % (page_num // 1024 // 1024, sample_num), - file=fp, - ) - - symbol_counts = collections.Counter() - # Read Sample File and find out the occurance of symbols in the page - lines = iter(self.perf_report_contents) - for line in lines: - if "PERF_RECORD_SAMPLE" in line: - pid, addr = self._parse_perf_sample( - line + next(lines) + next(lines) - ) - if pid is None: - # The sampling is not on Chrome - continue - if ( - addr // self.page_size - != (self.processes[pid].start_address + page_num) - // self.page_size - ): - # Sampling not in the current page - continue - - name = self._map_addr_to_symbol( - addr - self.processes[pid].start_address - ) - assert name, "Failed to find symbol name of addr %x" % addr - symbol_counts[name] += 1 - - assert ( - sum(symbol_counts.values()) == sample_num - ), "Symbol name matching missing for some addresses: %d vs %d" % ( - sum(symbol_counts.values()), - sample_num, - ) - - # Print out the symbol names sorted by the number of samples in - # the page - for name, count in sorted( - symbol_counts.items(), key=lambda kv: kv[1], reverse=True - ): - if count == 0: - break - print("> %s : %d" % (name, count), file=fp) - print("\n\n", file=fp) - - def draw(self): - # First read perf report to process information and save histogram - # into a text file - self._read_perf_report() - # Then use gnu plot to draw heat map - self._draw_heat_map() - - def analyze(self, binary, top_n): - # Read histogram from histo.txt - hist = self._restore_histogram() - # Sort the pages in histogram - sorted_hist = sorted( - hist.items(), key=lambda value: value[1], reverse=True - ) - - # Generate symbolizations - self._read_symbols_from_binary(binary) - - # Write hottest pages - with open("addr2symbol.txt", "w", encoding="utf-8") as fp: - if self.hugepage: - # Print hugepage region first - print( - "Hugepage top %d hot pages (%d MB - %d MB):" - % ( - top_n, - self.hugepage.start // 1024 // 1024, - self.hugepage.end // 1024 // 1024, - ), - file=fp, - ) - pages_to_print = [ - (k, v) - for k, v in sorted_hist - if self.hugepage.start <= k < self.hugepage.end - ][:top_n] - self._print_symbols_in_hot_pages(fp, pages_to_print) - print("==========================================", file=fp) - print( - "Top %d hot pages landed outside of hugepage:" % top_n, - file=fp, - ) - # Then print outside pages - pages_to_print = [ - (k, v) - for k, v in sorted_hist - if k < self.hugepage.start or k >= self.hugepage.end - ][:top_n] - self._print_symbols_in_hot_pages(fp, pages_to_print) - else: - # Print top_n hottest pages. - pages_to_print = sorted_hist[:top_n] - self._print_symbols_in_hot_pages(fp, pages_to_print) |