From b90c178d49805e8218d0a0d4f7304dc1f666d04b Mon Sep 17 00:00:00 2001 From: Yabin Cui Date: Tue, 24 Jul 2018 12:50:47 -0700 Subject: simpleperf: add --binary_filter option in report_html.py. Since disassemble binary is slow, and it is not useful to disassemble all binaries, add --binary_filter option to disassemble and add source code only for selected binaries. Also add is_elf_file() in utils.py to avoid the warning message of running readelf on files not in elf format. Also fix two small errors in report_html.js. Bug: none Test: run test.py TestExampleWithNative.test_report_html. Change-Id: I115543c30a409dc2d11c76491614804d097326b1 --- simpleperf/scripts/report_html.js | 13 ++--- simpleperf/scripts/report_html.py | 55 ++++++++++++++----- simpleperf/scripts/test.py | 11 ++-- simpleperf/scripts/utils.py | 108 +++++++++++++++++++++----------------- 4 files changed, 115 insertions(+), 72 deletions(-) diff --git a/simpleperf/scripts/report_html.js b/simpleperf/scripts/report_html.js index 4fc2a4a3..720f72f8 100644 --- a/simpleperf/scripts/report_html.js +++ b/simpleperf/scripts/report_html.js @@ -602,7 +602,7 @@ class SampleTableWeightSelectorView { if (e.clickEvent) { let button = $(e.clickEvent.target); let newOption = button.attr('key'); - if (this.curOption != newOption) { + if (newOption && this.curOption != newOption) { this.curOption = newOption; divContainer.find(`#${id}`).text(options.get(this.curOption)); onSelectChange(); @@ -638,7 +638,7 @@ class SampleTableView { this.div = $('
', {id: this.id}).appendTo(divContainer); this.eventInfo = eventInfo; this.selectorView = null; - this.tableView = null; + this.tableDiv = null; } drawAsync(totalProgress) { @@ -647,6 +647,7 @@ class SampleTableView { this.div.empty(); this.selectorView = new SampleTableWeightSelectorView( this.div, this.eventInfo, () => this.onSampleWeightChange()); + this.tableDiv = $('
').appendTo(this.div); })) .then(() => this._drawSampleTable(totalProgress)); } @@ -657,7 +658,7 @@ class SampleTableView { let data = []; return createPromise() .then(wait(() => { - this.div.find('table').remove(); + this.tableDiv.empty(); let getSampleWeight = this.selectorView.getSampleWeightFunction(); let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix(); // Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library', @@ -665,7 +666,7 @@ class SampleTableView { let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : ''; let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples', 'Process', 'Thread', 'Library', 'Function', 'HideKey']; - this.div.append(` + this.tableDiv.append(` ${getTableRow(titles, 'th')} @@ -691,7 +692,7 @@ class SampleTableView { })) .then(addProgress(totalProgress / 2)) .then(wait(() => { - let table = this.div.find('table'); + let table = this.tableDiv.find('table'); let dataTable = table.DataTable({ lengthMenu: [10, 20, 50, 100, -1], order: [0, 'desc'], @@ -1039,7 +1040,7 @@ class SampleWeightSelectorView { if (e.clickEvent) { let button = $(e.clickEvent.target); let newOption = button.attr('key'); - if (this.curOption != newOption) { + if (newOption && this.curOption != newOption) { this.curOption = newOption; divContainer.find(`#${id}`).text(options.get(this.curOption)); onSelectChange(); diff --git a/simpleperf/scripts/report_html.py b/simpleperf/scripts/report_html.py index c28a4299..c3ab0df2 100755 --- a/simpleperf/scripts/report_html.py +++ b/simpleperf/scripts/report_html.py @@ -635,7 +635,7 @@ class RecordData(object): self.events[event_name] = EventScope(event_name) return self.events[event_name] - def add_source_code(self, source_dirs): + def add_source_code(self, source_dirs, filter_lib): """ Collect source code information: 1. Find line ranges for each function in FunctionSet. 2. Find line for each addr in FunctionScope.addr_hit_map. @@ -647,17 +647,19 @@ class RecordData(object): if function.func_name == 'unknown': continue 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) + if filter_lib(lib_name): + 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 lib in event.libraries: lib_name = self.libs.get_lib_name(lib.lib_id) - for function in lib.functions.values(): - func_addr = self.functions.id_to_func[function.func_id].start_addr - for addr in function.addr_hit_map: - addr2line.add_addr(lib_name, func_addr, addr) + if filter_lib(lib_name): + for function in lib.functions.values(): + func_addr = self.functions.id_to_func[function.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. @@ -665,6 +667,8 @@ class RecordData(object): if function.func_name == 'unknown': continue dso = addr2line.get_dso(self.libs.get_lib_name(function.lib_id)) + if not dso: + continue 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: @@ -681,6 +685,8 @@ class RecordData(object): for event in self.events.values(): for lib in event.libraries: dso = addr2line.get_dso(self.libs.get_lib_name(lib.lib_id)) + if not dso: + continue for function in lib.functions.values(): for addr in function.addr_hit_map: source = addr2line.get_addr_source(dso, addr) @@ -697,18 +703,29 @@ class RecordData(object): # Collect needed source code in SourceFileSet. self.source_files.load_source_code(source_dirs) - def add_disassembly(self): + def add_disassembly(self, filter_lib): """ Collect disassembly information: 1. Use objdump to collect disassembly for each function in FunctionSet. 2. Set flag to dump addr_hit_map when generating record info. """ objdump = Objdump(self.ndk_path, self.binary_cache_path) - for function in self.functions.id_to_func.values(): + cur_lib_name = None + dso_info = None + for function in sorted(self.functions.id_to_func.values(), key=lambda a: a.lib_id): if function.func_name == 'unknown': continue lib_name = self.libs.get_lib_name(function.lib_id) - code = objdump.disassemble_code(lib_name, function.start_addr, function.addr_len) - function.disassembly = code + if lib_name != cur_lib_name: + cur_lib_name = lib_name + if filter_lib(lib_name): + dso_info = objdump.get_dso_info(lib_name) + else: + dso_info = None + if dso_info: + log_info('Disassemble %s' % dso_info[0]) + if dso_info: + code = objdump.disassemble_code(dso_info, function.start_addr, function.addr_len) + function.disassembly = code self.gen_addr_hit_map_in_record_info = True @@ -866,6 +883,8 @@ def main(): 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('--add_disassembly', action='store_true', help='Add disassembled code.') + parser.add_argument('--binary_filter', nargs='+', help="""Annotate source code and disassembly + only for selected binaries.""") 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.") parser.add_argument('--show_art_frames', action='store_true', @@ -892,10 +911,18 @@ def main(): for record_file in args.record_file: record_data.load_record_file(record_file, args.show_art_frames) record_data.limit_percents(args.min_func_percent, args.min_callchain_percent) + + def filter_lib(lib_name): + if not args.binary_filter: + return True + for binary in args.binary_filter: + if binary in lib_name: + return True + return False if args.add_source_code: - record_data.add_source_code(args.source_dirs) + record_data.add_source_code(args.source_dirs, filter_lib) if args.add_disassembly: - record_data.add_disassembly() + record_data.add_disassembly(filter_lib) # 3. Generate report html. report_generator = ReportGenerator(args.report_path) diff --git a/simpleperf/scripts/test.py b/simpleperf/scripts/test.py index 7eed5112..8424085b 100644 --- a/simpleperf/scripts/test.py +++ b/simpleperf/scripts/test.py @@ -577,6 +577,8 @@ class TestExampleWithNative(TestExampleBase): def test_report_html(self): self.common_test_report_html() + self.run_cmd(['report_html.py', '--add_source_code', '--source_dirs', 'testdata', + '--add_disassembly', '--binary_filter', "libnative-lib.so"]) class TestExampleWithNativeRoot(TestExampleBase): @@ -1063,11 +1065,12 @@ class TestTools(unittest.TestCase): } objdump = Objdump(None, binary_cache_path) for dso_path in test_map: - dso_info = test_map[dso_path] - disassemble_code = objdump.disassemble_code(dso_path, dso_info['start_addr'], - dso_info['len']) + dso = test_map[dso_path] + dso_info = objdump.get_dso_info(dso_path) + self.assertIsNotNone(dso_info) + disassemble_code = objdump.disassemble_code(dso_info, dso['start_addr'], dso['len']) self.assertTrue(disassemble_code) - for item in dso_info['expected_items']: + for item in dso['expected_items']: self.assertTrue(item in disassemble_code) def test_readelf(self): diff --git a/simpleperf/scripts/utils.py b/simpleperf/scripts/utils.py index 81f29a33..22554830 100644 --- a/simpleperf/scripts/utils.py +++ b/simpleperf/scripts/utils.py @@ -354,6 +354,13 @@ def open_report_in_browser(report_path): # webbrowser.get() doesn't work well on darwin/windows. webbrowser.open_new_tab(report_path) +def is_elf_file(path): + if os.path.isfile(path): + with open(path, 'rb') as fh: + data = fh.read(4) + if len(data) == 4 and bytes_to_str(data) == '\x7fELF': + return True + return False def find_real_dso_path(dso_path_in_record_file, binary_cache_path): """ Given the path of a shared library in perf.data, find its real path in the file system. """ @@ -361,9 +368,9 @@ def find_real_dso_path(dso_path_in_record_file, binary_cache_path): return None if binary_cache_path: tmp_path = os.path.join(binary_cache_path, dso_path_in_record_file[1:]) - if os.path.isfile(tmp_path): + if is_elf_file(tmp_path): return tmp_path - if os.path.isfile(dso_path_in_record_file): + if is_elf_file(dso_path_in_record_file): return dso_path_in_record_file return None @@ -559,19 +566,20 @@ class Objdump(object): self.readelf = ReadElf(ndk_path) self.objdump_paths = {} - def disassemble_code(self, dso_path, start_addr, addr_len): - """ Disassemble [start_addr, start_addr + addr_len] of dso_path. - Return a list of pair (disassemble_code_line, addr). - """ - # 1. Find real path. + def get_dso_info(self, dso_path): real_path = find_real_dso_path(dso_path, self.binary_cache_path) - if real_path is None: + if not real_path: return None - - # 2. Get path of objdump. arch = self.readelf.get_arch(real_path) if arch == 'unknown': return None + return (real_path, arch) + + def disassemble_code(self, dso_info, start_addr, addr_len): + """ Disassemble [start_addr, start_addr + addr_len] of dso_path. + Return a list of pair (disassemble_code_line, addr). + """ + real_path, arch = dso_info objdump_path = self.objdump_paths.get(arch) if not objdump_path: objdump_path = find_tool_path('objdump', self.ndk_path, arch) @@ -614,53 +622,57 @@ class ReadElf(object): def get_arch(self, elf_file_path): """ Get arch of an elf file. """ - try: - output = subprocess.check_output([self.readelf_path, '-h', elf_file_path]) - if output.find('AArch64') != -1: - return 'arm64' - if output.find('ARM') != -1: - return 'arm' - if output.find('X86-64') != -1: - return 'x86_64' - if output.find('80386') != -1: - return 'x86' - except subprocess.CalledProcessError: - pass + if is_elf_file(elf_file_path): + try: + output = subprocess.check_output([self.readelf_path, '-h', elf_file_path]) + output = bytes_to_str(output) + if output.find('AArch64') != -1: + return 'arm64' + if output.find('ARM') != -1: + return 'arm' + if output.find('X86-64') != -1: + return 'x86_64' + if output.find('80386') != -1: + return 'x86' + except subprocess.CalledProcessError: + pass return 'unknown' def get_build_id(self, elf_file_path): """ Get build id of an elf file. """ - try: - output = subprocess.check_output([self.readelf_path, '-n', elf_file_path]) - output = bytes_to_str(output) - result = re.search(r'Build ID:\s*(\S+)', output) - if result: - build_id = result.group(1) - if len(build_id) < 40: - build_id += '0' * (40 - len(build_id)) - else: - build_id = build_id[:40] - build_id = '0x' + build_id - return build_id - except subprocess.CalledProcessError: - pass + if is_elf_file(elf_file_path): + try: + output = subprocess.check_output([self.readelf_path, '-n', elf_file_path]) + output = bytes_to_str(output) + result = re.search(r'Build ID:\s*(\S+)', output) + if result: + build_id = result.group(1) + if len(build_id) < 40: + build_id += '0' * (40 - len(build_id)) + else: + build_id = build_id[:40] + build_id = '0x' + build_id + return build_id + except subprocess.CalledProcessError: + pass return "" def get_sections(self, elf_file_path): """ Get sections of an elf file. """ section_names = [] - try: - output = subprocess.check_output([self.readelf_path, '-SW', elf_file_path]) - output = bytes_to_str(output) - for line in output.split('\n'): - # Parse line like:" [ 1] .note.android.ident NOTE 0000000000400190 ...". - result = re.search(r'^\s+\[\s*\d+\]\s(.+?)\s', line) - if result: - section_name = result.group(1).strip() - if section_name: - section_names.append(section_name) - except subprocess.CalledProcessError: - pass + if is_elf_file(elf_file_path): + try: + output = subprocess.check_output([self.readelf_path, '-SW', elf_file_path]) + output = bytes_to_str(output) + for line in output.split('\n'): + # Parse line like:" [ 1] .note.android.ident NOTE 0000000000400190 ...". + result = re.search(r'^\s+\[\s*\d+\]\s(.+?)\s', line) + if result: + section_name = result.group(1).strip() + if section_name: + section_names.append(section_name) + except subprocess.CalledProcessError: + pass return section_names def extant_dir(arg): -- cgit v1.2.3