summaryrefslogtreecommitdiff
path: root/simpleperf/scripts/report_html.py
diff options
context:
space:
mode:
Diffstat (limited to 'simpleperf/scripts/report_html.py')
-rwxr-xr-xsimpleperf/scripts/report_html.py352
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('>', '&gt;').replace('<', '&lt;')
-
-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)