summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYabin Cui <yabinc@google.com>2017-11-01 17:42:19 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2017-11-01 17:42:19 +0000
commit29c0043b5334432afc01584f435585c2d2da49fe (patch)
tree58ca0118623351c876838f068fbf583749fcf0ca
parent2b217ca55557c4834993c4dbee2fc452a19defaf (diff)
parent706c3dfe4a7159c110cfc92823f978f2629a9b06 (diff)
downloadextras-29c0043b5334432afc01584f435585c2d2da49fe.tar.gz
Merge "simpleperf: support source code in html report interface."
-rw-r--r--simpleperf/scripts/report_html.js176
-rw-r--r--simpleperf/scripts/report_html.py473
-rw-r--r--simpleperf/scripts/test.py22
-rw-r--r--simpleperf/scripts/utils.py23
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('>', '&gt;').replace('<', '&lt;')
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.