diff options
Diffstat (limited to 'simpleperf/scripts/report_html.py')
-rwxr-xr-x | simpleperf/scripts/report_html.py | 352 |
1 files changed, 141 insertions, 211 deletions
diff --git a/simpleperf/scripts/report_html.py b/simpleperf/scripts/report_html.py index 6e6a90e1..f2292720 100755 --- a/simpleperf/scripts/report_html.py +++ b/simpleperf/scripts/report_html.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # # Copyright (C) 2017 The Android Open Source Project # @@ -15,36 +15,30 @@ # limitations under the License. # -from __future__ import annotations import argparse import collections -from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass import datetime import json import os -from pathlib import Path import sys -from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Union -from simpleperf_report_lib import ReportLib, SymbolStruct -from simpleperf_utils import ( - Addr2Nearestline, ArgParseFormatter, BinaryFinder, get_script_dir, log_exit, log_info, Objdump, - open_report_in_browser, ReadElf, SourceFileSearcher) +from simpleperf_report_lib import ReportLib +from utils import log_info, log_exit +from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser +from utils import SourceFileSearcher MAX_CALLSTACK_LENGTH = 750 - class HtmlWriter(object): - def __init__(self, output_path: Union[Path, str]): + def __init__(self, output_path): self.fh = open(output_path, 'w') self.tag_stack = [] def close(self): self.fh.close() - def open_tag(self, tag: str, **attrs: Dict[str, str]) -> HtmlWriter: + def open_tag(self, tag, **attrs): attr_str = '' for key in attrs: attr_str += ' %s="%s"' % (key, attrs[key]) @@ -52,47 +46,39 @@ class HtmlWriter(object): self.tag_stack.append(tag) return self - def close_tag(self, tag: Optional[str] = None): + def close_tag(self, tag=None): if tag: assert tag == self.tag_stack[-1] self.fh.write('</%s>\n' % self.tag_stack.pop()) - def add(self, text: str) -> HtmlWriter: + def add(self, text): self.fh.write(text) return self - def add_file(self, file_path: Union[Path, str]) -> HtmlWriter: + def add_file(self, file_path): file_path = os.path.join(get_script_dir(), file_path) with open(file_path, 'r') as f: self.add(f.read()) return self - -def modify_text_for_html(text: str) -> str: +def modify_text_for_html(text): return text.replace('>', '>').replace('<', '<') - -def hex_address_for_json(addr: int) -> str: - """ To handle big addrs (nears uint64_max) in Javascript, store addrs as hex strings in Json. - """ - return '0x%x' % addr - - class EventScope(object): - def __init__(self, name: str): + def __init__(self, name): self.name = name - self.processes: Dict[int, ProcessScope] = {} # map from pid to ProcessScope + self.processes = {} # map from pid to ProcessScope self.sample_count = 0 self.event_count = 0 - def get_process(self, pid: int) -> ProcessScope: + def get_process(self, pid): process = self.processes.get(pid) if not process: process = self.processes[pid] = ProcessScope(pid) return process - def get_sample_info(self, gen_addr_hit_map: bool) -> Dict[str, Any]: + def get_sample_info(self, gen_addr_hit_map): result = {} result['eventName'] = self.name result['eventCount'] = self.event_count @@ -102,13 +88,13 @@ class EventScope(object): return result @property - def threads(self) -> Iterator[ThreadScope]: + def threads(self): for process in self.processes.values(): for thread in process.threads.values(): yield thread @property - def libraries(self) -> Iterator[LibScope]: + def libraries(self): for process in self.processes.values(): for thread in process.threads.values(): for lib in thread.libs.values(): @@ -117,13 +103,13 @@ class EventScope(object): class ProcessScope(object): - def __init__(self, pid: int): + def __init__(self, pid): self.pid = pid self.name = '' self.event_count = 0 - self.threads: Dict[int, ThreadScope] = {} # map from tid to ThreadScope + self.threads = {} # map from tid to ThreadScope - def get_thread(self, tid: int, thread_name: str) -> ThreadScope: + def get_thread(self, tid, thread_name): thread = self.threads.get(tid) if not thread: thread = self.threads[tid] = ThreadScope(tid) @@ -132,7 +118,7 @@ class ProcessScope(object): self.name = thread_name return thread - def get_sample_info(self, gen_addr_hit_map: bool) -> Dict[str, Any]: + def get_sample_info(self, gen_addr_hit_map): result = {} result['pid'] = self.pid result['eventCount'] = self.event_count @@ -141,11 +127,10 @@ class ProcessScope(object): for thread in threads] return result - def merge_by_thread_name(self, process: ProcessScope): + def merge_by_thread_name(self, process): self.event_count += process.event_count - thread_list: List[ThreadScope] = list( - self.threads.values()) + list(process.threads.values()) - new_threads: Dict[str, ThreadScope] = {} # map from thread name to ThreadScope + thread_list = list(self.threads.values()) + list(process.threads.values()) + new_threads = {} # map from thread name to ThreadScope for thread in thread_list: cur_thread = new_threads.get(thread.name) if cur_thread is None: @@ -159,21 +144,19 @@ class ProcessScope(object): class ThreadScope(object): - def __init__(self, tid: int): + def __init__(self, tid): self.tid = tid self.name = '' self.event_count = 0 self.sample_count = 0 - self.libs: Dict[int, LibScope] = {} # map from lib_id to LibScope + self.libs = {} # map from lib_id to LibScope self.call_graph = CallNode(-1) self.reverse_call_graph = CallNode(-1) - def add_callstack( - self, event_count: int, callstack: List[Tuple[int, int, int]], - build_addr_hit_map: bool): + 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].""" - hit_func_ids: Set[int] = set() + hit_func_ids = set() for i, (lib_id, func_id, addr) in enumerate(callstack): # When a callstack contains recursive function, only add for each function once. if func_id in hit_func_ids: @@ -206,8 +189,7 @@ class ThreadScope(object): self.call_graph.update_subtree_event_count() self.reverse_call_graph.update_subtree_event_count() - def limit_percents(self, min_func_limit: float, min_callchain_percent: float, - hit_func_ids: Set[int]): + def limit_percents(self, min_func_limit, min_callchain_percent, hit_func_ids): for lib in self.libs.values(): to_del_funcs = [] for function in lib.functions.values(): @@ -221,7 +203,7 @@ class ThreadScope(object): self.call_graph.cut_edge(min_limit, hit_func_ids) self.reverse_call_graph.cut_edge(min_limit, hit_func_ids) - def get_sample_info(self, gen_addr_hit_map: bool) -> Dict[str, Any]: + def get_sample_info(self, gen_addr_hit_map): result = {} result['tid'] = self.tid result['eventCount'] = self.event_count @@ -232,7 +214,7 @@ class ThreadScope(object): result['rg'] = self.reverse_call_graph.gen_sample_info() return result - def merge(self, thread: ThreadScope): + def merge(self, thread): self.event_count += thread.event_count self.sample_count += thread.sample_count for lib_id, lib in thread.libs.items(): @@ -247,18 +229,18 @@ class ThreadScope(object): class LibScope(object): - def __init__(self, lib_id: int): + def __init__(self, lib_id): self.lib_id = lib_id self.event_count = 0 - self.functions: Dict[int, FunctionScope] = {} # map from func_id to FunctionScope. + self.functions = {} # map from func_id to FunctionScope. - def get_function(self, func_id: int) -> FunctionScope: + def get_function(self, func_id): function = self.functions.get(func_id) if not function: function = self.functions[func_id] = FunctionScope(func_id) return function - def gen_sample_info(self, gen_addr_hit_map: bool) -> Dict[str, Any]: + def gen_sample_info(self, gen_addr_hit_map): result = {} result['libId'] = self.lib_id result['eventCount'] = self.event_count @@ -266,7 +248,7 @@ class LibScope(object): for func in self.functions.values()] return result - def merge(self, lib: LibScope): + def merge(self, lib): self.event_count += lib.event_count for func_id, function in lib.functions.items(): cur_function = self.functions.get(func_id) @@ -278,7 +260,7 @@ class LibScope(object): class FunctionScope(object): - def __init__(self, func_id: int): + def __init__(self, func_id): self.func_id = func_id self.sample_count = 0 self.event_count = 0 @@ -287,7 +269,7 @@ class FunctionScope(object): # map from (source_file_id, line) to [event_count, subtree_event_count]. self.line_hit_map = None - def build_addr_hit_map(self, addr: int, event_count: int, subtree_event_count: int): + 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) @@ -297,8 +279,7 @@ class FunctionScope(object): count_info[0] += event_count count_info[1] += subtree_event_count - def build_line_hit_map(self, source_file_id: int, line: int, event_count: int, - subtree_event_count: int): + 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) @@ -309,7 +290,7 @@ class FunctionScope(object): count_info[0] += event_count count_info[1] += subtree_event_count - def gen_sample_info(self, gen_addr_hit_map: bool) -> Dict[str, Any]: + def gen_sample_info(self, gen_addr_hit_map): result = {} result['f'] = self.func_id result['c'] = [self.sample_count, self.event_count, self.subtree_event_count] @@ -324,14 +305,11 @@ class FunctionScope(object): items = [] for addr in sorted(self.addr_hit_map): count_info = self.addr_hit_map[addr] - items.append( - {'a': hex_address_for_json(addr), - 'e': count_info[0], - 's': count_info[1]}) + items.append({'a': addr, 'e': count_info[0], 's': count_info[1]}) result['a'] = items return result - def merge(self, function: FunctionScope): + def merge(self, function): self.sample_count += function.sample_count self.event_count += function.event_count self.subtree_event_count += function.subtree_event_count @@ -339,8 +317,7 @@ class FunctionScope(object): self.line_hit_map = self.__merge_hit_map(self.line_hit_map, function.line_hit_map) @staticmethod - def __merge_hit_map(map1: Optional[Dict[int, List[int]]], - map2: Optional[Dict[int, List[int]]]) -> Optional[Dict[int, List[int]]]: + def __merge_hit_map(map1, map2): if not map1: return map2 if not map2: @@ -357,14 +334,13 @@ class FunctionScope(object): class CallNode(object): - def __init__(self, func_id: int): + def __init__(self, func_id): self.event_count = 0 self.subtree_event_count = 0 self.func_id = func_id - # map from func_id to CallNode - self.children: Dict[int, CallNode] = collections.OrderedDict() + self.children = collections.OrderedDict() # map from func_id to CallNode - def get_child(self, func_id: int) -> CallNode: + def get_child(self, func_id): child = self.children.get(func_id) if not child: child = self.children[func_id] = CallNode(func_id) @@ -376,7 +352,7 @@ class CallNode(object): self.subtree_event_count += child.update_subtree_event_count() return self.subtree_event_count - def cut_edge(self, min_limit: float, hit_func_ids: Set[int]): + def cut_edge(self, min_limit, hit_func_ids): hit_func_ids.add(self.func_id) to_del_children = [] for key in self.children: @@ -388,7 +364,7 @@ class CallNode(object): for key in to_del_children: del self.children[key] - def gen_sample_info(self) -> Dict[str, Any]: + def gen_sample_info(self): result = {} result['e'] = self.event_count result['s'] = self.subtree_event_count @@ -396,7 +372,7 @@ class CallNode(object): result['c'] = [child.gen_sample_info() for child in self.children.values()] return result - def merge(self, node: CallNode): + def merge(self, node): self.event_count += node.event_count self.subtree_event_count += node.subtree_event_count for key, child in node.children.items(): @@ -407,37 +383,27 @@ class CallNode(object): cur_child.merge(child) -@dataclass -class LibInfo: - name: str - build_id: str - - class LibSet(object): """ Collection of shared libraries used in perf.data. """ - def __init__(self): - self.lib_name_to_id: Dict[str, int] = {} - self.libs: List[LibInfo] = [] - - def get_lib_id(self, lib_name: str) -> Optional[int]: - return self.lib_name_to_id.get(lib_name) - - def add_lib(self, lib_name: str, build_id: str) -> int: - """ Return lib_id of the newly added lib. """ - lib_id = len(self.libs) - self.libs.append(LibInfo(lib_name, build_id)) - self.lib_name_to_id[lib_name] = lib_id + self.lib_name_to_id = {} + self.lib_id_to_name = [] + + def get_lib_id(self, lib_name): + lib_id = self.lib_name_to_id.get(lib_name) + if lib_id is None: + 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(self, lib_id: int) -> LibInfo: - return self.libs[lib_id] + def get_lib_name(self, lib_id): + return self.lib_id_to_name[lib_id] class Function(object): """ Represent a function in a shared library. """ - - def __init__(self, lib_id: int, func_name: str, func_id: int, start_addr: int, addr_len: int): + 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 @@ -449,12 +415,11 @@ class Function(object): class FunctionSet(object): """ Collection of functions used in perf.data. """ - def __init__(self): - self.name_to_func: Dict[Tuple[int, str], Function] = {} - self.id_to_func: Dict[int, Function] = {} + self.name_to_func = {} + self.id_to_func = {} - def get_func_id(self, lib_id: int, symbol: SymbolStruct) -> int: + 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: @@ -465,7 +430,7 @@ class FunctionSet(object): self.id_to_func[func_id] = function return function.func_id - def trim_functions(self, left_func_ids: Set[int]): + 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: @@ -476,18 +441,17 @@ class FunctionSet(object): class SourceFile(object): """ A source file containing source code hit by samples. """ - - def __init__(self, file_id: int, abstract_path: str): + def __init__(self, file_id, abstract_path): self.file_id = file_id self.abstract_path = abstract_path # path reported by addr2line - self.real_path: Optional[str] = None # file path in the file system - self.requested_lines: Optional[Set[int]] = set() - self.line_to_code: Dict[int, str] = {} # map from line to code in that line. + 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: int, end_line: int): + 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: str): + def add_source_code(self, real_path): self.real_path = real_path with open(real_path, 'r') as f: source_code = f.readlines() @@ -501,18 +465,17 @@ class SourceFile(object): class SourceFileSet(object): """ Collection of source files. """ - def __init__(self): - self.path_to_source_files: Dict[str, SourceFile] = {} # map from file path to SourceFile. + self.path_to_source_files = {} # map from file path to SourceFile. - def get_source_file(self, file_path: str) -> SourceFile: + def get_source_file(self, file_path): source_file = self.path_to_source_files.get(file_path) - if not source_file: + 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: List[str]): + 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) @@ -520,9 +483,10 @@ class SourceFileSet(object): source_file.add_source_code(real_path) + class RecordData(object): - """RecordData reads perf.data, and generates data used by report_html.js in json format. + """RecordData reads perf.data, and generates data used by report.js in json format. All generated items are listed as below: 1. recordTime: string 2. machineType: string @@ -600,26 +564,21 @@ class RecordData(object): } """ - def __init__( - self, binary_cache_path: Optional[str], - ndk_path: Optional[str], - build_addr_hit_map: bool, proguard_mapping_files: Optional[List[str]] = None): + 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.proguard_mapping_files = proguard_mapping_files - self.meta_info: Optional[Dict[str, str]] = None - self.cmdline: Optional[str] = None - self.arch: Optional[str] = None - self.events: Dict[str, EventScope] = {} + 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() self.gen_addr_hit_map_in_record_info = False - self.binary_finder = BinaryFinder(binary_cache_path, ReadElf(ndk_path)) - def load_record_file(self, record_file: str, show_art_frames: bool): + def load_record_file(self, record_file, show_art_frames): lib = ReportLib() lib.SetRecordFile(record_file) # If not showing ip for unknown symbols, the percent of the unknown symbol may be @@ -629,8 +588,6 @@ class RecordData(object): lib.ShowArtFrames() if self.binary_cache_path: lib.SetSymfs(self.binary_cache_path) - for file_path in self.proguard_mapping_files or []: - lib.AddProguardMappingFile(file_path) self.meta_info = lib.MetaInfo() self.cmdline = lib.GetRecordCmd() self.arch = lib.GetArch() @@ -653,16 +610,11 @@ class RecordData(object): thread.sample_count += 1 lib_id = self.libs.get_lib_id(symbol.dso_name) - if lib_id is None: - lib_id = self.libs.add_lib(symbol.dso_name, lib.GetBuildIdForPath(symbol.dso_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) - if lib_id is None: - lib_id = self.libs.add_lib( - symbol.dso_name, lib.GetBuildIdForPath(symbol.dso_name)) func_id = self.functions.get_func_id(lib_id, symbol) callstack.append((lib_id, func_id, symbol.vaddr_in_file)) if len(callstack) > MAX_CALLSTACK_LENGTH: @@ -686,8 +638,8 @@ class RecordData(object): for process in new_processes.values(): event.processes[process.pid] = process - def limit_percents(self, min_func_percent: float, min_callchain_percent: float): - hit_func_ids: Set[int] = set() + 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 to_del_processes = [] @@ -706,44 +658,43 @@ class RecordData(object): del event.processes[process] self.functions.trim_functions(hit_func_ids) - def _get_event(self, event_name: str) -> EventScope: + 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: List[str], filter_lib: Callable[[str], bool]): + 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. 3. Collect needed source code in SourceFileSet. """ - addr2line = Addr2Nearestline(self.ndk_path, self.binary_finder, False) + addr2line = Addr2Nearestline(self.ndk_path, self.binary_cache_path, False) # Request line range for each function. for function in self.functions.id_to_func.values(): if function.func_name == 'unknown': continue - lib_info = self.libs.get_lib(function.lib_id) - if filter_lib(lib_info.name): - addr2line.add_addr(lib_info.name, lib_info.build_id, - function.start_addr, function.start_addr) - addr2line.add_addr(lib_info.name, lib_info.build_id, function.start_addr, + lib_name = self.libs.get_lib_name(function.lib_id) + 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_info = self.libs.get_lib(lib.lib_id) - if filter_lib(lib_info.name): + lib_name = self.libs.get_lib_name(lib.lib_id) + 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_info.name, lib_info.build_id, func_addr, addr) + 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(): if function.func_name == 'unknown': continue - dso = addr2line.get_dso(self.libs.get_lib(function.lib_id).name) + 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) @@ -761,7 +712,7 @@ class RecordData(object): # Build FunctionScope.line_hit_map. for event in self.events.values(): for lib in event.libraries: - dso = addr2line.get_dso(self.libs.get_lib(lib.lib_id).name) + dso = addr2line.get_dso(self.libs.get_lib_name(lib.lib_id)) if not dso: continue for function in lib.functions.values(): @@ -780,38 +731,33 @@ class RecordData(object): # Collect needed source code in SourceFileSet. self.source_files.load_source_code(source_dirs) - def add_disassembly(self, filter_lib: Callable[[str], bool], jobs: int): + 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_finder) - executor = ThreadPoolExecutor(jobs) - lib_functions: Dict[int, List[Function]] = collections.defaultdict(list) - - for function in self.functions.id_to_func.values(): + objdump = Objdump(self.ndk_path, self.binary_cache_path) + 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_functions[function.lib_id].append(function) + lib_name = self.libs.get_lib_name(function.lib_id) + 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 - for lib_id, functions in lib_functions.items(): - lib = self.libs.get_lib(lib_id) - if not filter_lib(lib.name): - continue - dso_info = objdump.get_dso_info(lib.name, lib.build_id) - if not dso_info: - continue - log_info('Disassemble %s' % dso_info[0]) - for function in functions: - def task(function, dso_info): - function.disassembly = objdump.disassemble_code( - dso_info, function.start_addr, function.addr_len) - executor.submit(task, function, dso_info) - executor.shutdown(wait=True) self.gen_addr_hit_map_in_record_info = True - def gen_record_info(self) -> Dict[str, Any]: - """ Return json data which will be used by report_html.js. """ + def gen_record_info(self): record_info = {} timestamp = self.meta_info.get('timestamp') if timestamp: @@ -837,26 +783,26 @@ class RecordData(object): record_info['sourceFiles'] = self._gen_source_files() return record_info - def _gen_process_names(self) -> Dict[int, str]: - process_names: Dict[int, str] = {} + def _gen_process_names(self): + process_names = {} for event in self.events.values(): for process in event.processes.values(): process_names[process.pid] = process.name return process_names - def _gen_thread_names(self) -> Dict[int, str]: - thread_names: Dict[int, str] = {} + def _gen_thread_names(self): + thread_names = {} for event in self.events.values(): for process in event.processes.values(): for thread in process.threads.values(): thread_names[thread.tid] = thread.name return thread_names - def _gen_lib_list(self) -> List[str]: - return [modify_text_for_html(lib.name) for lib in self.libs.libs] + def _gen_lib_list(self): + return [modify_text_for_html(x) for x in self.libs.lib_id_to_name] - def _gen_function_map(self) -> Dict[int, Any]: - func_map: Dict[int, Any] = {} + def _gen_function_map(self): + func_map = {} for func_id in sorted(self.functions.id_to_func): function = self.functions.id_to_func[func_id] func_data = {} @@ -867,18 +813,16 @@ class RecordData(object): if function.disassembly: disassembly_list = [] for code, addr in function.disassembly: - disassembly_list.append( - [modify_text_for_html(code), - hex_address_for_json(addr)]) + disassembly_list.append([modify_text_for_html(code), addr]) func_data['d'] = disassembly_list func_map[func_id] = func_data return func_map - def _gen_sample_info(self) -> List[Dict[str, Any]]: + def _gen_sample_info(self): return [event.get_sample_info(self.gen_addr_hit_map_in_record_info) for event in self.events.values()] - def _gen_source_files(self) -> List[Dict[str, Any]]: + def _gen_source_files(self): source_files = sorted(self.source_files.path_to_source_files.values(), key=lambda x: x.file_id) file_list = [] @@ -896,7 +840,6 @@ class RecordData(object): file_list.append(file_data) return file_list - URLS = { 'jquery': 'https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js', 'bootstrap4-css': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css', @@ -909,10 +852,9 @@ URLS = { 'gstatic-charts': 'https://www.gstatic.com/charts/loader.js', } - class ReportGenerator(object): - def __init__(self, html_path: Union[Path, str]): + def __init__(self, html_path): self.hw = HtmlWriter(html_path) self.hw.open_tag('html') self.hw.open_tag('head') @@ -932,11 +874,12 @@ class ReportGenerator(object): """).close_tag() self.hw.close_tag('head') self.hw.open_tag('body') + self.record_info = {} def write_content_div(self): self.hw.open_tag('div', id='report_content').close_tag() - def write_record_data(self, record_data: Dict[str, Any]): + def write_record_data(self, record_data): self.hw.open_tag('script', id='record_data', type='application/json') self.hw.add(json.dumps(record_data)) self.hw.close_tag() @@ -950,29 +893,27 @@ class ReportGenerator(object): self.hw.close() -def get_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description='report profiling data', formatter_class=ArgParseFormatter) +def main(): + sys.setrecursionlimit(MAX_CALLSTACK_LENGTH * 2 + 50) + parser = argparse.ArgumentParser(description='report profiling data') parser.add_argument('-i', '--record_file', nargs='+', default=['perf.data'], help=""" - Set profiling data file to report.""") - parser.add_argument('-o', '--report_path', default='report.html', help='Set output html file') + Set profiling data file to report. Default is perf.data.""") + parser.add_argument('-o', '--report_path', default='report.html', help=""" + 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('--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( - '-j', '--jobs', type=int, default=os.cpu_count(), - help='Use multithreading to speed up disassembly and source code annotation.') 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', @@ -980,15 +921,7 @@ def get_args() -> argparse.Namespace: parser.add_argument('--aggregate-by-thread-name', action='store_true', help="""aggregate samples by thread name instead of thread id. This is useful for showing multiple perf.data generated for the same app.""") - parser.add_argument( - '--proguard-mapping-file', nargs='+', - help='Add proguard mapping file to de-obfuscate symbols') - return parser.parse_args() - - -def main(): - sys.setrecursionlimit(MAX_CALLSTACK_LENGTH * 2 + 50) - args = get_args() + args = parser.parse_args() # 1. Process args. binary_cache_path = 'binary_cache' @@ -1004,19 +937,16 @@ def main(): log_exit('--source_dirs is needed to add source code.') build_addr_hit_map = args.add_source_code or args.add_disassembly ndk_path = None if not args.ndk_path else args.ndk_path[0] - if args.jobs < 1: - log_exit('Invalid --jobs option.') # 2. Produce record data. - record_data = RecordData(binary_cache_path, ndk_path, - build_addr_hit_map, args.proguard_mapping_file) + 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, args.show_art_frames) if args.aggregate_by_thread_name: record_data.aggregate_by_thread_name() record_data.limit_percents(args.min_func_percent, args.min_callchain_percent) - def filter_lib(lib_name: str) -> bool: + def filter_lib(lib_name): if not args.binary_filter: return True for binary in args.binary_filter: @@ -1026,7 +956,7 @@ def main(): if args.add_source_code: record_data.add_source_code(args.source_dirs, filter_lib) if args.add_disassembly: - record_data.add_disassembly(filter_lib, args.jobs) + record_data.add_disassembly(filter_lib) # 3. Generate report html. report_generator = ReportGenerator(args.report_path) |