diff options
author | Yabin Cui <yabinc@google.com> | 2017-11-01 17:42:19 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2017-11-01 17:42:19 +0000 |
commit | 29c0043b5334432afc01584f435585c2d2da49fe (patch) | |
tree | 58ca0118623351c876838f068fbf583749fcf0ca | |
parent | 2b217ca55557c4834993c4dbee2fc452a19defaf (diff) | |
parent | 706c3dfe4a7159c110cfc92823f978f2629a9b06 (diff) | |
download | extras-29c0043b5334432afc01584f435585c2d2da49fe.tar.gz |
Merge "simpleperf: support source code in html report interface."
-rw-r--r-- | simpleperf/scripts/report_html.js | 176 | ||||
-rw-r--r-- | simpleperf/scripts/report_html.py | 473 | ||||
-rw-r--r-- | simpleperf/scripts/test.py | 22 | ||||
-rw-r--r-- | simpleperf/scripts/utils.py | 23 |
4 files changed, 596 insertions, 98 deletions
diff --git a/simpleperf/scripts/report_html.js b/simpleperf/scripts/report_html.js index fff18f2c..6f6b0210 100644 --- a/simpleperf/scripts/report_html.js +++ b/simpleperf/scripts/report_html.js @@ -73,11 +73,27 @@ function getLibName(libId) { } function getFuncName(funcId) { - return gFunctionMap[funcId][1]; + return gFunctionMap[funcId].f; } function getLibNameOfFunction(funcId) { - return getLibName(gFunctionMap[funcId][0]); + return getLibName(gFunctionMap[funcId].l); +} + +function getFuncSourceRange(funcId) { + let func = gFunctionMap[funcId]; + if (func.hasOwnProperty('s')) { + return {fileId: func.s[0], startLine: func.s[1], endLine: func.s[2]}; + } + return null; +} + +function getSourceFilePath(sourceFileId) { + return gSourceFiles[sourceFileId].path; +} + +function getSourceCode(sourceFileId) { + return gSourceFiles[sourceFileId].code; } class TabManager { @@ -413,6 +429,7 @@ class FlameGraphTab { // FunctionTab: show information of a function. // 1. Show the callgrpah and reverse callgraph of a function as flamegraphs. +// 2. Show the annotated source code of the function. class FunctionTab { static showFunction(eventInfo, processInfo, threadInfo, lib, func) { let title = 'Function'; @@ -441,6 +458,7 @@ class FunctionTab { this.selectorView = null; this.callgraphView = null; this.reverseCallgraphView = null; + this.sourceCodeView = null; this.draw(); gTabs.setActive(this); } @@ -473,6 +491,14 @@ class FunctionTab { this.div.append(getHtml('hr')); this.div.append(getHtml('b', {text: `Functions calling ${funcName}`}) + '<br/>'); this.reverseCallgraphView = new FlameGraphView(this.div, this.func.rg, true); + + let sourceFiles = collectSourceFilesForFunction(this.func); + if (sourceFiles) { + this.div.append(getHtml('hr')); + this.div.append(getHtml('b', {text: 'SourceCode:'}) + '<br/>'); + this.sourceCodeView = new SourceCodeView(this.div, sourceFiles); + } + this.onSampleWeightChange(); // Manually set sample weight function for the first time. } @@ -484,6 +510,9 @@ class FunctionTab { if (this.reverseCallgraphView) { this.reverseCallgraphView.draw(sampleWeightFunction); } + if (this.sourceCodeView) { + this.sourceCodeView.draw(sampleWeightFunction); + } } } @@ -508,7 +537,7 @@ class FunctionSampleWeightSelectorView { PERCENT_TO_CUR_THREAD: 2, RAW_EVENT_COUNT: 3, EVENT_COUNT_IN_TIME: 4, - } + }; let name = eventInfo.eventName; this.supportEventCountInTime = name.includes('task-clock') || name.includes('cpu-clock'); if (this.supportEventCountInTime) { @@ -808,7 +837,7 @@ class FlameGraphView { _enableInfo() { this.selected = null; let thisObj = this; - this.svg.find('g').on('mouseenter', function(e) { + this.svg.find('g').on('mouseenter', function() { if (thisObj.selected) { thisObj.selected.css('stroke-width', '0'); } @@ -851,6 +880,143 @@ class FlameGraphView { } } + +class SourceFile { + + constructor(fileId) { + this.path = getSourceFilePath(fileId); + this.code = getSourceCode(fileId); + this.showLines = {}; // map from line number to {eventCount, subtreeEventCount}. + this.hasCount = false; + } + + addLineRange(startLine, endLine) { + for (let i = startLine; i <= endLine; ++i) { + if (i in this.showLines || !(i in this.code)) { + continue; + } + this.showLines[i] = {eventCount: 0, subtreeEventCount: 0}; + } + } + + addLineCount(lineNumber, eventCount, subtreeEventCount) { + let line = this.showLines[lineNumber]; + if (line) { + line.eventCount += eventCount; + line.subtreeEventCount += subtreeEventCount; + this.hasCount = true; + } + } +} + +// Return a list of SourceFile related to a function. +function collectSourceFilesForFunction(func) { + if (!func.hasOwnProperty('sc')) { + return null; + } + let hitLines = func.sc; + let sourceFiles = {}; // map from sourceFileId to SourceFile. + + function getFile(fileId) { + let file = sourceFiles[fileId]; + if (!file) { + file = sourceFiles[fileId] = new SourceFile(fileId); + } + return file; + } + + // Show lines for the function. + let funcRange = getFuncSourceRange(func.g.f); + if (funcRange) { + let file = getFile(funcRange.fileId); + file.addLineRange(funcRange.startLine); + } + + // Show lines for hitLines. + for (let hitLine of hitLines) { + let file = getFile(hitLine.f); + file.addLineRange(hitLine.l - 5, hitLine.l + 5); + file.addLineCount(hitLine.l, hitLine.e, hitLine.s); + } + + let result = []; + // Show the source file containing the function before other source files. + if (funcRange) { + let file = getFile(funcRange.fileId); + if (file.hasCount) { + result.push(file); + } + delete sourceFiles[funcRange.fileId]; + } + for (let fileId in sourceFiles) { + let file = sourceFiles[fileId]; + if (file.hasCount) { + result.push(file); + } + } + return result.length > 0 ? result : null; +} + +// Show annotated source code of a function. +class SourceCodeView { + + constructor(divContainer, sourceFiles) { + this.div = $('<div>'); + this.div.appendTo(divContainer); + this.sourceFiles = sourceFiles; + } + + draw(sampleWeightFunction) { + google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction)); + } + + realDraw(sampleWeightFunction) { + this.div.empty(); + // For each file, draw a table of 'Line', 'Total', 'Self', 'Code'. + for (let sourceFile of this.sourceFiles) { + let rows = []; + let lineNumbers = Object.keys(sourceFile.showLines); + lineNumbers.sort((a, b) => a - b); + for (let lineNumber of lineNumbers) { + let code = getHtml('pre', {text: sourceFile.code[lineNumber]}); + let countInfo = sourceFile.showLines[lineNumber]; + let totalValue = ''; + let selfValue = ''; + if (countInfo.subtreeEventCount != 0) { + totalValue = sampleWeightFunction(countInfo.subtreeEventCount); + selfValue = sampleWeightFunction(countInfo.eventCount); + } + rows.push([lineNumber, totalValue, selfValue, code]); + } + + let data = new google.visualization.DataTable(); + data.addColumn('string', 'Line'); + data.addColumn('string', 'Total'); + data.addColumn('string', 'Self'); + data.addColumn('string', 'Code'); + data.addRows(rows); + for (let i = 0; i < lineNumbers.length; ++i) { + data.setProperty(i, 0, 'className', 'colForLine'); + for (let j = 1; j <= 2; ++j) { + data.setProperty(i, j, 'className', 'colForCount'); + } + } + this.div.append(getHtml('pre', {text: sourceFile.path})); + let wrapperDiv = $('<div>'); + wrapperDiv.appendTo(this.div); + let table = new google.visualization.Table(wrapperDiv.get(0)); + table.draw(data, { + width: '100%', + sort: 'disable', + fronzenColumns: 3, + allowHtml: true, + }); + } + } +} + + + function initGlobalObjects() { gTabs = new TabManager($('div#report_content')); let recordData = $('#record_data').text(); @@ -860,6 +1026,7 @@ function initGlobalObjects() { gLibList = gRecordInfo.libList; gFunctionMap = gRecordInfo.functionMap; gSampleInfo = gRecordInfo.sampleInfo; + gSourceFiles = gRecordInfo.sourceFiles; } function createTabs() { @@ -876,6 +1043,7 @@ let gThreads; let gLibList; let gFunctionMap; let gSampleInfo; +let gSourceFiles; initGlobalObjects(); createTabs(); diff --git a/simpleperf/scripts/report_html.py b/simpleperf/scripts/report_html.py index 91b3ef99..d06cd10e 100644 --- a/simpleperf/scripts/report_html.py +++ b/simpleperf/scripts/report_html.py @@ -38,7 +38,7 @@ class HtmlWriter(object): def open_tag(self, tag, **attrs): attr_str = '' - for key in attrs.keys(): + for key in attrs: attr_str += ' %s="%s"' % (key, attrs[key]) self.fh.write('<%s%s>' % (tag, attr_str)) self.tag_stack.append(tag) @@ -113,16 +113,19 @@ class ThreadScope(object): self.tid = tid self.name = '' self.event_count = 0 - self.libs = {} # map from libId to LibScope + self.libs = {} # map from lib_id to LibScope - def add_callstack(self, event_count, callstack): - """ callstack is a list of (lib_id, func_id) pairs. + def add_callstack(self, event_count, callstack, build_addr_hit_map): + """ callstack is a list of tuple (lib_id, func_id, addr). For each i > 0, callstack[i] calls callstack[i-1].""" - # When a callstack contains recursive function, we should only add event count - # and callchain for each recursive function once. - hit_func_ids = {} + hit_func_ids = set() for i in range(len(callstack)): - lib_id, func_id = callstack[i] + lib_id, func_id, addr = callstack[i] + # When a callstack contains recursive function, only add for each function once. + if func_id in hit_func_ids: + continue + hit_func_ids.add(func_id) + lib = self.libs.get(lib_id) if not lib: lib = self.libs[lib_id] = LibScope(lib_id) @@ -130,17 +133,18 @@ class ThreadScope(object): if i == 0: lib.event_count += event_count function.sample_count += 1 - if func_id in hit_func_ids: - continue - hit_func_ids[func_id] = True function.add_reverse_callchain(callstack, i + 1, len(callstack), event_count) - hit_func_ids = {} + if build_addr_hit_map: + function.build_addr_hit_map(addr, event_count if i == 0 else 0, event_count) + + hit_func_ids.clear() for i in range(len(callstack) - 1, -1, -1): - lib_id, func_id = callstack[i] + lib_id, func_id, _ = callstack[i] + # When a callstack contains recursive function, only add for each function once. if func_id in hit_func_ids: continue - hit_func_ids[func_id] = True + hit_func_ids.add(func_id) lib = self.libs.get(lib_id) lib.get_function(func_id).add_callchain(callstack, i - 1, -1, event_count) @@ -179,6 +183,9 @@ class FunctionScope(object): self.sample_count = 0 self.call_graph = CallNode(func_id) self.reverse_call_graph = CallNode(func_id) + self.addr_hit_map = None # map from addr to [event_count, subtree_event_count]. + # map from (source_file_id, line) to [event_count, subtree_event_count]. + self.line_hit_map = None def add_callchain(self, callchain, start, end, event_count): node = self.call_graph @@ -192,24 +199,49 @@ class FunctionScope(object): node = node.get_child(callchain[i][1]) node.event_count += event_count + def build_addr_hit_map(self, addr, event_count, subtree_event_count): + if self.addr_hit_map is None: + self.addr_hit_map = {} + count_info = self.addr_hit_map.get(addr) + if count_info is None: + self.addr_hit_map[addr] = [event_count, subtree_event_count] + else: + count_info[0] += event_count + count_info[1] += subtree_event_count + + def build_line_hit_map(self, source_file_id, line, event_count, subtree_event_count): + if self.line_hit_map is None: + self.line_hit_map = {} + key = (source_file_id, line) + count_info = self.line_hit_map.get(key) + if count_info is None: + self.line_hit_map[key] = [event_count, subtree_event_count] + else: + count_info[0] += event_count + count_info[1] += subtree_event_count + def update_subtree_event_count(self): a = self.call_graph.update_subtree_event_count() b = self.reverse_call_graph.update_subtree_event_count() return max(a, b) - def limit_callchain_percent(self, min_callchain_percent): + def limit_callchain_percent(self, min_callchain_percent, hit_func_ids): min_limit = min_callchain_percent * 0.01 * self.call_graph.subtree_event_count - self.call_graph.cut_edge(min_limit) - - def hit_function(self, func_id_set): - self.call_graph.hit_function(func_id_set) - self.reverse_call_graph.hit_function(func_id_set) + self.call_graph.cut_edge(min_limit, hit_func_ids) + self.reverse_call_graph.cut_edge(min_limit, hit_func_ids) def gen_sample_info(self): result = {} result['c'] = self.sample_count result['g'] = self.call_graph.gen_sample_info() result['rg'] = self.reverse_call_graph.gen_sample_info() + if self.line_hit_map: + items = [] + for key in self.line_hit_map: + count_info = self.line_hit_map[key] + item = {'f': key[0], 'l': key[1], 'e': count_info[0], 's': count_info[1]} + items.append(item) + result['sc'] = items return result @@ -233,22 +265,18 @@ class CallNode(object): self.subtree_event_count += child.update_subtree_event_count() return self.subtree_event_count - def cut_edge(self, min_limit): + def cut_edge(self, min_limit, hit_func_ids): + hit_func_ids.add(self.func_id) to_del_children = [] for key in self.children: child = self.children[key] if child.subtree_event_count < min_limit: to_del_children.append(key) else: - child.cut_edge(min_limit) + child.cut_edge(min_limit, hit_func_ids) for key in to_del_children: del self.children[key] - def hit_function(self, func_id_set): - func_id_set.add(self.func_id) - for child in self.children.values(): - child.hit_function(func_id_set) - def gen_sample_info(self): result = {} result['e'] = self.event_count @@ -259,30 +287,165 @@ class CallNode(object): class LibSet(object): - + """ Collection of shared libraries used in perf.data. """ def __init__(self): - self.libs = {} + self.lib_name_to_id = {} + self.lib_id_to_name = [] def get_lib_id(self, lib_name): - lib_id = self.libs.get(lib_name) + lib_id = self.lib_name_to_id.get(lib_name) if lib_id is None: - lib_id = len(self.libs) - self.libs[lib_name] = lib_id + lib_id = len(self.lib_id_to_name) + self.lib_name_to_id[lib_name] = lib_id + self.lib_id_to_name.append(lib_name) return lib_id + def get_lib_name(self, lib_id): + return self.lib_id_to_name[lib_id] -class FunctionSet(object): - def __init__(self): - self.functions = {} +class Function(object): + """ Represent a function in a shared library. """ + def __init__(self, lib_id, func_name, func_id, start_addr, addr_len): + self.lib_id = lib_id + self.func_name = func_name + self.func_id = func_id + self.start_addr = start_addr + self.addr_len = addr_len + self.source_info = None - def get_func_id(self, lib_id, func_name): - key = (lib_id, func_name) - func_id = self.functions.get(key) - if func_id is None: - func_id = len(self.functions) - self.functions[key] = func_id - return func_id + +class FunctionSet(object): + """ Collection of functions used in perf.data. """ + def __init__(self): + self.name_to_func = {} + self.id_to_func = {} + + def get_func_id(self, lib_id, symbol): + key = (lib_id, symbol.symbol_name) + function = self.name_to_func.get(key) + if function is None: + func_id = len(self.id_to_func) + function = Function(lib_id, symbol.symbol_name, func_id, symbol.symbol_addr, + symbol.symbol_len) + self.name_to_func[key] = function + self.id_to_func[func_id] = function + return function.func_id + + def trim_functions(self, left_func_ids): + """ Remove functions excepts those in left_func_ids. """ + for function in self.name_to_func.values(): + if function.func_id not in left_func_ids: + del self.id_to_func[function.func_id] + # name_to_func will not be used. + self.name_to_func = None + + +class SourceFile(object): + """ A source file containing source code hit by samples. """ + def __init__(self, file_id, abstract_path): + self.file_id = file_id + self.abstract_path = abstract_path # path reported by addr2line + self.real_path = None # file path in the file system + self.requested_lines = set() + self.line_to_code = {} # map from line to code in that line. + + def request_lines(self, start_line, end_line): + self.requested_lines |= set(range(start_line, end_line + 1)) + + def add_source_code(self, real_path): + self.real_path = real_path + with open(real_path, 'r') as f: + source_code = f.readlines() + max_line = len(source_code) + for line in self.requested_lines: + if line > 0 and line <= max_line: + self.line_to_code[line] = source_code[line - 1] + # requested_lines is no longer used. + self.requested_lines = None + + +class SourceFileSet(object): + """ Collection of source files. """ + def __init__(self): + self.path_to_source_files = {} # map from file path to SourceFile. + + def get_source_file(self, file_path): + source_file = self.path_to_source_files.get(file_path) + if source_file is None: + source_file = SourceFile(len(self.path_to_source_files), file_path) + self.path_to_source_files[file_path] = source_file + return source_file + + def load_source_code(self, source_dirs): + file_searcher = SourceFileSearcher(source_dirs) + for source_file in self.path_to_source_files.values(): + real_path = file_searcher.get_real_path(source_file.abstract_path) + if real_path: + source_file.add_source_code(real_path) + + +class SourceFileSearcher(object): + + SOURCE_FILE_EXTS = {'.h', '.hh', '.H', '.hxx', '.hpp', '.h++', + '.c', '.cc', '.C', '.cxx', '.cpp', '.c++', + '.java', '.kt'} + + @classmethod + def is_source_filename(cls, filename): + ext = os.path.splitext(filename)[1] + return ext in cls.SOURCE_FILE_EXTS + + """" Find source file paths in the file system. + The file paths reported by addr2line are the paths stored in debug sections + of shared libraries. And we need to convert them to file paths in the file + system. It is done in below steps: + 1. Collect all file paths under the provided source_dirs. The suffix of a + source file should contain one of below: + h: for C/C++ header files. + c: for C/C++ source files. + java: for Java source files. + kt: for Kotlin source files. + 2. Given an abstract_path reported by addr2line, select the best real path + as below: + 2.1 Find all real paths with the same file name as the abstract path. + 2.2 Select the real path having the longest common suffix with the abstract path. + """ + def __init__(self, source_dirs): + # Map from filename to a list of reversed directory path containing filename. + self.filename_to_rparents = {} + self._collect_paths(source_dirs) + + def _collect_paths(self, source_dirs): + for source_dir in source_dirs: + for parent, _, file_names in os.walk(source_dir): + rparent = None + for file_name in file_names: + if self.is_source_filename(file_name): + rparents = self.filename_to_rparents.get(file_name) + if rparents is None: + rparents = self.filename_to_rparents[file_name] = [] + if rparent is None: + rparent = parent[::-1] + rparents.append(rparent) + + def get_real_path(self, abstract_path): + abstract_path = abstract_path.replace('/', os.sep) + abstract_parent, file_name = os.path.split(abstract_path) + abstract_rparent = abstract_parent[::-1] + real_rparents = self.filename_to_rparents.get(file_name) + if real_rparents is None: + return None + best_matched_rparent = None + best_common_length = -1 + for real_rparent in real_rparents: + length = len(os.path.commonprefix((real_rparent, abstract_rparent))) + if length > best_common_length: + best_common_length = length + best_matched_rparent = real_rparent + if best_matched_rparent is None: + return None + return os.path.join(best_matched_rparent[::-1], file_name) class RecordData(object): @@ -297,7 +460,13 @@ class RecordData(object): 6. processNames: map from pid to processName. 7. threadNames: map from tid to threadName. 8. libList: an array of libNames, indexed by libId. - 9. functionMap: map from functionId to [libId, functionName]. + 9. functionMap: map from functionId to funcData. + funcData = { + l: libId + f: functionName + s: [sourceFileId, startLine, endLine] [optional] + } + 10. sampleInfo = [eventInfo] eventInfo = { eventName @@ -323,6 +492,7 @@ class RecordData(object): c: sampleCount g: callGraph rg: reverseCallgraph + sc: [sourceCodeInfo] [optional] } callGraph and reverseCallGraph are both of type CallNode. callGraph shows how a function calls other functions. @@ -333,23 +503,42 @@ class RecordData(object): f: functionId c: [CallNode] # children } + + sourceCodeInfo { + f: sourceFileId + l: line + e: eventCount + s: subtreeEventCount + } + + 11. sourceFiles: an array of sourceFile, indexed by sourceFileId. + sourceFile { + path + code: # a map from line to code for that line. + } """ - def __init__(self, record_file, min_func_percent, min_callchain_percent): - self._load_record_file(record_file) - self._limit_percents(min_func_percent, min_callchain_percent) + def __init__(self, binary_cache_path, ndk_path, build_addr_hit_map): + self.binary_cache_path = binary_cache_path + self.ndk_path = ndk_path + self.build_addr_hit_map = build_addr_hit_map + self.meta_info = None + self.cmdline = None + self.arch = None + self.events = {} + self.libs = LibSet() + self.functions = FunctionSet() + self.total_samples = 0 + self.source_files = SourceFileSet() - def _load_record_file(self, record_file): + def load_record_file(self, record_file): lib = ReportLib() - lib.ShowIpForUnknownSymbol() lib.SetRecordFile(record_file) + if self.binary_cache_path: + lib.SetSymfs(self.binary_cache_path) self.meta_info = lib.MetaInfo() self.cmdline = lib.GetRecordCmd() self.arch = lib.GetArch() - self.events = {} - self.libs = LibSet() - self.functions = FunctionSet() - self.total_samples = 0 while True: raw_sample = lib.GetNextSample() if not raw_sample: @@ -368,39 +557,111 @@ class RecordData(object): thread.event_count += raw_sample.period lib_id = self.libs.get_lib_id(symbol.dso_name) - callstack = [(lib_id, self.functions.get_func_id(lib_id, symbol.symbol_name))] + func_id = self.functions.get_func_id(lib_id, symbol) + callstack = [(lib_id, func_id, symbol.vaddr_in_file)] for i in range(callchain.nr): symbol = callchain.entries[i].symbol lib_id = self.libs.get_lib_id(symbol.dso_name) - callstack.append((lib_id, self.functions.get_func_id(lib_id, symbol.symbol_name))) - thread.add_callstack(raw_sample.period, callstack) + func_id = self.functions.get_func_id(lib_id, symbol) + callstack.append((lib_id, func_id, symbol.vaddr_in_file)) + thread.add_callstack(raw_sample.period, callstack, self.build_addr_hit_map) for event in self.events.values(): for process in event.processes.values(): for thread in process.threads.values(): for lib in thread.libs.values(): - for funcId in lib.functions.keys(): - function = lib.functions[funcId] + for func_id in lib.functions: + function = lib.functions[func_id] function.update_subtree_event_count() - def _limit_percents(self, min_func_percent, min_callchain_percent): + def limit_percents(self, min_func_percent, min_callchain_percent): + hit_func_ids = set() for event in self.events.values(): min_limit = event.event_count * min_func_percent * 0.01 for process in event.processes.values(): for thread in process.threads.values(): for lib in thread.libs.values(): - for func_id in lib.functions.keys(): + to_del_func_ids = [] + for func_id in lib.functions: function = lib.functions[func_id] if function.call_graph.subtree_event_count < min_limit: - del lib.functions[func_id] + to_del_func_ids.append(func_id) else: - function.limit_callchain_percent(min_callchain_percent) + function.limit_callchain_percent(min_callchain_percent, + hit_func_ids) + for func_id in to_del_func_ids: + del lib.functions[func_id] + self.functions.trim_functions(hit_func_ids) def _get_event(self, event_name): if event_name not in self.events: self.events[event_name] = EventScope(event_name) return self.events[event_name] + def add_source_code(self, source_dirs): + """ Collect source code information: + 1. Find line ranges for each function in FunctionSet. + 2. Find line for each addr in FunctionScope.addr_hit_map. + 3. Collect needed source code in SourceFileSet. + """ + addr2line = Addr2Nearestline(self.ndk_path, self.binary_cache_path) + # Request line range for each function. + for function in self.functions.id_to_func.values(): + lib_name = self.libs.get_lib_name(function.lib_id) + addr2line.add_addr(lib_name, function.start_addr, function.start_addr) + addr2line.add_addr(lib_name, function.start_addr, + function.start_addr + function.addr_len - 1) + # Request line for each addr in FunctionScope.addr_hit_map. + for event in self.events.values(): + for process in event.processes.values(): + for thread in process.threads.values(): + for lib in thread.libs.values(): + lib_name = self.libs.get_lib_name(lib.lib_id) + for function in lib.functions.values(): + func_addr = self.functions.id_to_func[ + function.call_graph.func_id].start_addr + for addr in function.addr_hit_map: + addr2line.add_addr(lib_name, func_addr, addr) + addr2line.convert_addrs_to_lines() + + # Set line range for each function. + for function in self.functions.id_to_func.values(): + dso = addr2line.get_dso(self.libs.get_lib_name(function.lib_id)) + start_source = addr2line.get_addr_source(dso, function.start_addr) + end_source = addr2line.get_addr_source(dso, + function.start_addr + function.addr_len - 1) + if not start_source or not end_source: + continue + start_file_path, start_line = start_source[-1] + end_file_path, end_line = end_source[-1] + if start_file_path != end_file_path or start_line > end_line: + continue + source_file = self.source_files.get_source_file(start_file_path) + source_file.request_lines(start_line, end_line) + function.source_info = (source_file.file_id, start_line, end_line) + + # Build FunctionScope.line_hit_map. + for event in self.events.values(): + for process in event.processes.values(): + for thread in process.threads.values(): + for lib in thread.libs.values(): + dso = addr2line.get_dso(self.libs.get_lib_name(lib.lib_id)) + for function in lib.functions.values(): + for addr in function.addr_hit_map: + source = addr2line.get_addr_source(dso, addr) + if not source: + continue + for file_path, line in source: + source_file = self.source_files.get_source_file(file_path) + # Show [line - 5, line + 5] of the line hit by a sample. + source_file.request_lines(line - 5, line + 5) + count_info = function.addr_hit_map[addr] + function.build_line_hit_map(source_file.file_id, line, + count_info[0], count_info[1]) + + # Collect needed source code in SourceFileSet. + self.source_files.load_source_code(source_dirs) + def gen_record_info(self): record_info = {} timestamp = self.meta_info.get('timestamp') @@ -424,6 +685,7 @@ class RecordData(object): record_info['libList'] = self._gen_lib_list() record_info['functionMap'] = self._gen_function_map() record_info['sampleInfo'] = self._gen_sample_info() + record_info['sourceFiles'] = self._gen_source_files() return record_info def _gen_process_names(self): @@ -445,29 +707,38 @@ class RecordData(object): return name.replace('>', '>').replace('<', '<') def _gen_lib_list(self): - ret = sorted(self.libs.libs.keys(), key=lambda k: self.libs.libs[k]) - return [self._modify_name_for_html(x) for x in ret] + return [self._modify_name_for_html(x) for x in self.libs.lib_id_to_name] def _gen_function_map(self): - func_id_set = set() - for event in self.events.values(): - for process in event.processes.values(): - for thread in process.threads.values(): - for lib in thread.libs.values(): - for func_id in lib.functions.keys(): - lib.functions[func_id].hit_function(func_id_set) - - functions = self.functions.functions func_map = {} - for key in functions: - func_id = functions[key] - if func_id in func_id_set: - func_map[func_id] = [key[0], self._modify_name_for_html(key[1])] + for func_id in sorted(self.functions.id_to_func): + function = self.functions.id_to_func[func_id] + func_data = {} + func_data['l'] = function.lib_id + func_data['f'] = self._modify_name_for_html(function.func_name) + if function.source_info: + func_data['s'] = function.source_info + func_map[func_id] = func_data return func_map def _gen_sample_info(self): return [event.get_sample_info() for event in self.events.values()] + def _gen_source_files(self): + source_files = sorted(self.source_files.path_to_source_files.values(), + key=lambda x: x.file_id) + file_list = [] + for source_file in source_files: + file_data = {} + if not source_file.real_path: + file_data['path'] = '' + file_data['code'] = {} + else: + file_data['path'] = source_file.real_path + file_data['code'] = source_file.line_to_code + file_list.append(file_data) + return file_list + class ReportGenerator(object): @@ -484,7 +755,7 @@ class ReportGenerator(object): ).close_tag() self.hw.open_tag('script', src='https://www.gstatic.com/charts/loader.js').close_tag() self.hw.open_tag('script').add( - "google.charts.load('current', {'packages': ['corechart']});").close_tag() + "google.charts.load('current', {'packages': ['corechart', 'table']});").close_tag() self.hw.open_tag('script', src='https://code.jquery.com/jquery-3.2.1.js').close_tag() self.hw.open_tag('script', src='https://code.jquery.com/ui/1.12.1/jquery-ui.js' ).close_tag() @@ -492,6 +763,10 @@ class ReportGenerator(object): src='https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js').close_tag() self.hw.open_tag('script', src='https://cdn.datatables.net/1.10.16/js/dataTables.jqueryui.min.js').close_tag() + self.hw.open_tag('style', type='text/css').add(""" + .colForLine { width: 50px; } + .colForCount { width: 100px; } + """).close_tag() self.hw.close_tag('head') self.hw.open_tag('body') self.record_info = {} @@ -530,29 +805,57 @@ def gen_flamegraph(record_file): def main(): parser = argparse.ArgumentParser(description='report profiling data') - parser.add_argument('-i', '--record_file', default='perf.data', help=""" - Set profiling data file to report.""") + parser.add_argument('-i', '--record_file', nargs='+', default=['perf.data'], help=""" + Set profiling data file to report. Default is perf.data.""") parser.add_argument('-o', '--report_path', default='report.html', help=""" - Set output html file.""") + Set output html file. Default is report.html.""") parser.add_argument('--min_func_percent', default=0.01, type=float, help=""" Set min percentage of functions shown in the report. For example, when set to 0.01, only functions taking >= 0.01%% of total - event count are collected in the report.""") + event count are collected in the report. Default is 0.01.""") parser.add_argument('--min_callchain_percent', default=0.01, type=float, help=""" Set min percentage of callchains shown in the report. It is used to limit nodes shown in the function flamegraph. For example, when set to 0.01, only callchains taking >= 0.01%% of the event count of - the starting function are collected in the report.""") + the starting function are collected in the report. Default is 0.01.""") + parser.add_argument('--add_source_code', action='store_true', help='Add source code.') + parser.add_argument('--source_dirs', nargs='+', help='Source code directories.') + parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.') parser.add_argument('--no_browser', action='store_true', help="Don't open report in browser.") args = parser.parse_args() - record_data = RecordData(args.record_file, args.min_func_percent, args.min_callchain_percent) - + # 1. Process args. + binary_cache_path = 'binary_cache' + if not os.path.isdir(binary_cache_path): + if args.add_source_code: + log_exit("""binary_cache/ doesn't exist. Can't add source code or disassemble code + without collected binaries. Please run binary_cache_builder.py to + collect binaries for current profiling data, or run app_profiler.py + without -nb option.""") + binary_cache_path = None + + if args.add_source_code and not args.source_dirs: + log_exit('--source_dirs is needed to add source code.') + build_addr_hit_map = args.add_source_code + ndk_path = None if not args.ndk_path else args.ndk_path[0] + + # 2. Produce record data. + record_data = RecordData(binary_cache_path, ndk_path, build_addr_hit_map) + for record_file in args.record_file: + record_data.load_record_file(record_file) + record_data.limit_percents(args.min_func_percent, args.min_callchain_percent) + if args.add_source_code: + record_data.add_source_code(args.source_dirs) + + # 3. Generate report html. report_generator = ReportGenerator(args.report_path) report_generator.write_content_div() report_generator.write_record_data(record_data.gen_record_info()) report_generator.write_script() - flamegraph = gen_flamegraph(args.record_file) + # TODO: support multiple perf.data in flamegraph. + if len(args.record_file) > 1: + log_warning('flamegraph will only be shown for %s' % args.record_file[0]) + flamegraph = gen_flamegraph(args.record_file[0]) report_generator.write_flamegraph(flamegraph) report_generator.finish() diff --git a/simpleperf/scripts/test.py b/simpleperf/scripts/test.py index ab744d71..d4ff5125 100644 --- a/simpleperf/scripts/test.py +++ b/simpleperf/scripts/test.py @@ -29,6 +29,7 @@ Tested python scripts include: annotate.py report_sample.py pprof_proto_generator.py + report_html.py Test using both `adb root` and `adb unroot`. @@ -266,7 +267,6 @@ class TestExampleBase(TestBase): self.run_cmd(["report.py", "-i", "perf.data"]) self.run_cmd(["report.py", "-g"]) self.run_cmd(["report.py", "--self-kill-for-testing", "-g", "--gui"]) - self.run_cmd(["report_html.py"]) def common_test_annotate(self): self.run_cmd(["annotate.py", "-h"]) @@ -321,6 +321,17 @@ class TestExampleBase(TestBase): "-t", "1", "-nc"] + append_args) self.run_cmd([inferno_script, "-sc"]) + def common_test_report_html(self): + self.run_cmd(['report_html.py', '-h']) + self.run_app_profiler() + self.run_cmd(['report_html.py']) + self.run_cmd(['report_html.py', '--add_source_code', '--source_dirs', 'testdata']) + # Test with multiple perf.data. + shutil.move('perf.data', 'perf2.data') + self.run_app_profiler() + self.run_cmd(['report_html.py', '-i', 'perf.data', 'perf2.data']) + remove('perf2.data') + class TestExamplePureJava(TestExampleBase): @classmethod @@ -422,6 +433,9 @@ class TestExamplePureJava(TestExampleBase): os.chdir(saved_dir) remove(test_dir) + def test_report_html(self): + self.common_test_report_html() + class TestExamplePureJavaRoot(TestExampleBase): @classmethod @@ -525,6 +539,9 @@ class TestExampleWithNative(TestExampleBase): self.run_cmd([inferno_script, "-sc"]) self.check_inferno_report_html([('BusyLoopThread', 20)]) + def test_report_html(self): + self.common_test_report_html() + class TestExampleWithNativeRoot(TestExampleBase): @classmethod @@ -685,6 +702,9 @@ class TestExampleOfKotlin(TestExampleBase): [('com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()', 80)]) + def test_report_html(self): + self.common_test_report_html() + class TestExampleOfKotlinRoot(TestExampleBase): @classmethod diff --git a/simpleperf/scripts/utils.py b/simpleperf/scripts/utils.py index 4404c21c..aa097b8f 100644 --- a/simpleperf/scripts/utils.py +++ b/simpleperf/scripts/utils.py @@ -336,6 +336,13 @@ def remove(dir_or_file): def open_report_in_browser(report_path): + if is_darwin(): + # On darwin 10.12.6, webbrowser can't open browser, so try `open` cmd first. + try: + subprocess.check_call(['open', report_path]) + return + except: + pass import webbrowser try: # Try to open the report with Chrome @@ -412,9 +419,6 @@ class Addr2Nearestline(object): def __init__(self): self.addrs = {} - def get_addr_source(self, addr): - return self.addrs[addr].source_lines - class Addr(object): """ Info of an addr request. func_addr: start_addr of the function containing addr. @@ -560,12 +564,15 @@ class Addr2Nearestline(object): self.file_id_to_name.append(file_path) return file_id - def get_file_path(self, file_id): - return self.file_id_to_name[file_id] - def get_dso(self, dso_path): return self.dso_map.get(dso_path) + def get_addr_source(self, dso, addr): + source = dso.addrs[addr].source_lines + if source is None: + return None + return [(self.file_id_to_name[file_id], line) for (file_id, line) in source] + class Objdump(object): """ A wrapper of objdump to disassemble code. """ @@ -574,7 +581,7 @@ class Objdump(object): self.binary_cache_path = binary_cache_path self.readelf_path = find_tool_path('readelf', ndk_path) if not self.readelf_path: - log_exit("Can't find readelf. Please set ndk path by --ndk-path option.") + log_exit("Can't find readelf. Please set ndk path by --ndk_path option.") self.objdump_paths = {} def disassemble_code(self, dso_path, start_addr, addr_len): @@ -592,7 +599,7 @@ class Objdump(object): if not objdump_path: objdump_path = find_tool_path('objdump', self.ndk_path, arch) if not objdump_path: - log_exit("Can't find objdump. Please set ndk path by --ndk-path option.") + log_exit("Can't find objdump. Please set ndk path by --ndk_path option.") self.objdump_paths[arch] = objdump_path # 3. Run objdump. |