Merge "simpleperf: draw thread flamegraphs using javascript in report_html.py."
diff --git a/simpleperf/scripts/report_html.js b/simpleperf/scripts/report_html.js
index e35bba6..1e000b1 100644
--- a/simpleperf/scripts/report_html.js
+++ b/simpleperf/scripts/report_html.js
@@ -102,17 +102,21 @@
eventInfo.eventName.includes('cpu-clock');
}
+let createId = function() {
+ let currentId = 0;
+ return () => `id${++currentId}`;
+}();
+
class TabManager {
constructor(divContainer) {
- this.div = $('<div>', {id: 'tabs'});
- this.div.appendTo(divContainer);
+ this.div = $('<div>').appendTo(divContainer);
this.div.append(getHtml('ul'));
this.tabs = [];
this.isDrawCalled = false;
}
addTab(title, tabObj) {
- let id = 'tab_' + this.div.children().length;
+ let id = createId();
let tabDiv = $('<div>', {id: id});
tabDiv.appendTo(this.div);
this.div.children().first().append(
@@ -211,9 +215,7 @@
// Show pieChart of event count percentage of each process, thread, library and function.
class ChartView {
constructor(divContainer, eventInfo) {
- this.id = divContainer.children().length;
- this.div = $('<div>', {id: 'chartstat_' + this.id});
- this.div.appendTo(divContainer);
+ this.div = $('<div>').appendTo(divContainer);
this.eventInfo = eventInfo;
this.processInfo = null;
this.threadInfo = null;
@@ -227,7 +229,7 @@
if (isClockEvent(this.eventInfo)) {
this.getSampleWeight = function (eventCount) {
return (eventCount / 1000000.0).toFixed(3) + ' ms';
- }
+ };
} else {
this.getSampleWeight = (eventCount) => '' + eventCount;
}
@@ -388,9 +390,6 @@
class ChartStatTab {
- constructor() {
- }
-
init(div) {
this.div = div;
this.recordFileView = new RecordFileView(this.div);
@@ -410,9 +409,6 @@
class SampleTableTab {
- constructor() {
- }
-
init(div) {
this.div = div;
this.selectorView = null;
@@ -481,7 +477,7 @@
if (this.curOption == this.options.SHOW_PERCENT) {
return function(eventCount) {
return (eventCount * 100.0 / eventInfo.eventCount).toFixed(2) + '%';
- }
+ };
}
if (isClockEvent(eventInfo)) {
return (eventCount) => (eventCount / 1000000.0).toFixed(3);
@@ -500,9 +496,8 @@
class SampleTableView {
constructor(divContainer, eventInfo) {
- this.id = divContainer.children().length;
- this.div = $('<div>');
- this.div.appendTo(divContainer);
+ this.id = createId();
+ this.div = $('<div>', {id: this.id}).appendTo(divContainer);
this.eventInfo = eventInfo;
}
@@ -570,19 +565,73 @@
// Show embedded flamegraph generated by inferno.
class FlameGraphTab {
- constructor() {
- }
-
init(div) {
this.div = div;
}
draw() {
- $('div#flamegraph_id').appendTo(this.div).css('display', 'block');
- flamegraphInit();
+ this.div.empty();
+ if (gSampleInfo.length == 0) {
+ return;
+ }
+ let id = this.div.attr('id');
+ if (gSampleInfo.length == 1) {
+ new FlameGraphViewList(this.div, `${id}_0`, gSampleInfo[0]).draw();
+ } else {
+ // If more than one event, draw them in tabs.
+ this.div.append(getHtml('ul'));
+ let ul = this.div.children().first();
+ for (let i = 0; i < gSampleInfo.length; ++i) {
+ let subId = id + '_' + i;
+ let title = gSampleInfo[i].eventName;
+ ul.append(getHtml('li', {text: getHtml('a', {href: '#' + subId, text: title})}));
+ new FlameGraphViewList(this.div, subId, gSampleInfo[i]).draw();
+ }
+ this.div.tabs({active: 0});
+ }
}
}
+// Show FlameGraphs for samples in an event type, used in FlameGraphTab.
+class FlameGraphViewList {
+ constructor(divContainer, divId, eventInfo) {
+ this.div = $('<div>', {id: divId});
+ this.div.appendTo(divContainer);
+ this.eventInfo = eventInfo;
+ }
+
+ draw() {
+ this.div.empty();
+ this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo,
+ () => this.onSampleWeightChange());
+ this.selectorView.draw();
+ this.flamegraphs = [];
+ for (let process of this.eventInfo.processes) {
+ for (let thread of process.threads) {
+ let title = `Process ${getProcessName(process.pid)} ` +
+ `Thread ${getThreadName(thread.tid)} ` +
+ `(Samples: ${thread.sampleCount})`;
+ let flamegraphView = new FlameGraphView(this.div, title, thread.g.c, false);
+ this.flamegraphs.push(flamegraphView);
+ }
+ }
+ this.onSampleWeightChange();
+ }
+
+ onSampleWeightChange() {
+ let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
+ let totalCount = {};
+ let graphId = 0;
+ for (let process of this.eventInfo.processes) {
+ totalCount.countForProcess = process.eventCount;
+ for (let thread of process.threads) {
+ totalCount.countForThread = thread.eventCount;
+ this.flamegraphs[graphId++].draw(
+ (eventCount) => sampleWeightFunction(eventCount, totalCount));
+ }
+ }
+ }
+}
// FunctionTab: show information of a function.
// 1. Show the callgrpah and reverse callgraph of a function as flamegraphs.
@@ -628,20 +677,31 @@
this.div.empty();
this._drawTitle();
- this.selectorView = new FunctionSampleWeightSelectorView(this.div, this.eventInfo,
- this.processInfo, this.threadInfo, () => this.onSampleWeightChange());
+ this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo,
+ () => this.onSampleWeightChange());
this.selectorView.draw();
- this.div.append(getHtml('hr'));
- let funcName = getFuncName(this.func.f);
- this.div.append(getHtml('b', {text: `Functions called by ${funcName}`}) + '<br/>');
- this.callgraphView = new FlameGraphView(this.div, this.threadInfo.g, this.func, false);
-
- this.div.append(getHtml('hr'));
- this.div.append(getHtml('b', {text: `Functions calling ${funcName}`}) + '<br/>');
- this.reverseCallgraphView = new FlameGraphView(this.div, this.threadInfo.rg, this.func,
+ let funcId = this.func.f;
+ let funcName = getFuncName(funcId);
+ function getNodesMatchingFuncId(root) {
+ let nodes = [];
+ function recursiveFn(node) {
+ if (node.f == funcId) {
+ nodes.push(node);
+ } else {
+ for (let child of node.c) {
+ recursiveFn(child);
+ }
+ }
+ }
+ recursiveFn(root);
+ return nodes;
+ }
+ this.callgraphView = new FlameGraphView(this.div, `Functions called by ${funcName}`,
+ getNodesMatchingFuncId(this.threadInfo.g), false);
+ this.reverseCallgraphView = new FlameGraphView(this.div, `Functions calling ${funcName}`,
+ getNodesMatchingFuncId(this.threadInfo.rg),
true);
-
let sourceFiles = collectSourceFilesForFunction(this.func);
if (sourceFiles) {
this.div.append(getHtml('hr'));
@@ -693,7 +753,12 @@
}
onSampleWeightChange() {
- let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
+ let rawSampleWeightFunction = this.selectorView.getSampleWeightFunction();
+ let totalCount = {
+ countForProcess: this.processInfo.eventCount,
+ countForThread: this.threadInfo.eventCount,
+ };
+ let sampleWeightFunction = (eventCount) => rawSampleWeightFunction(eventCount, totalCount);
if (this.callgraphView) {
this.callgraphView.draw(sampleWeightFunction);
}
@@ -710,20 +775,18 @@
}
-// Select the way to show sample weight in FunctionTab.
+// Select the way to show sample weight in FlamegraphTab and FunctionTab.
// 1. Show percentage of event count relative to all processes.
// 2. Show percentage of event count relative to the current process.
// 3. Show percentage of event count relative to the current thread.
// 4. Show absolute event count.
// 5. Show event count in milliseconds, only possible for cpu-clock or task-clock events.
-class FunctionSampleWeightSelectorView {
- constructor(divContainer, eventInfo, processInfo, threadInfo, onSelectChange) {
+class SampleWeightSelectorView {
+ constructor(divContainer, eventInfo, onSelectChange) {
this.div = $('<div>');
this.div.appendTo(divContainer);
+ this.countForAllProcesses = eventInfo.eventCount;
this.onSelectChange = onSelectChange;
- this.eventCountForAllProcesses = eventInfo.eventCount;
- this.eventCountForProcess = processInfo.eventCount;
- this.eventCountForThread = threadInfo.eventCount;
this.options = {
PERCENT_TO_ALL_PROCESSES: 0,
PERCENT_TO_CUR_PROCESS: 1,
@@ -766,32 +829,32 @@
}
getSampleWeightFunction() {
- let thisObj = this;
if (this.curOption == this.options.PERCENT_TO_ALL_PROCESSES) {
- return function(eventCount) {
- let percent = eventCount * 100.0 / thisObj.eventCountForAllProcesses;
+ let countForAllProcesses = this.countForAllProcesses;
+ return function(eventCount, _) {
+ let percent = eventCount * 100.0 / countForAllProcesses;
return percent.toFixed(2) + '%';
};
}
if (this.curOption == this.options.PERCENT_TO_CUR_PROCESS) {
- return function(eventCount) {
- let percent = eventCount * 100.0 / thisObj.eventCountForProcess;
+ return function(eventCount, totalCount) {
+ let percent = eventCount * 100.0 / totalCount.countForProcess;
return percent.toFixed(2) + '%';
};
}
if (this.curOption == this.options.PERCENT_TO_CUR_THREAD) {
- return function(eventCount) {
- let percent = eventCount * 100.0 / thisObj.eventCountForThread;
+ return function(eventCount, totalCount) {
+ let percent = eventCount * 100.0 / totalCount.countForThread;
return percent.toFixed(2) + '%';
};
}
if (this.curOption == this.options.RAW_EVENT_COUNT) {
- return function(eventCount) {
+ return function(eventCount, _) {
return '' + eventCount;
};
}
if (this.curOption == this.options.EVENT_COUNT_IN_TIME) {
- return function(eventCount) {
+ return function(eventCount, _) {
let timeInMs = eventCount / 1000000.0;
return timeInMs.toFixed(3) + ' ms';
};
@@ -799,39 +862,22 @@
}
}
-
// Given a callgraph, show the flamegraph.
class FlameGraphView {
- // If reverseOrder is false, the root of the flamegraph is at the bottom,
- // otherwise it is at the top.
- constructor(divContainer, callgraph, funcInfo, reverseOrder) {
- this.id = divContainer.children().length;
- this.div = $('<div>', {id: 'fg_' + this.id});
+ constructor(divContainer, title, initNodes, reverseOrder) {
+ this.id = createId();
+ this.div = $('<div>', {id: this.id,
+ style: 'font-family: Monospace; font-size: 12px'});
this.div.appendTo(divContainer);
+ this.title = title;
this.reverseOrder = reverseOrder;
this.sampleWeightFunction = null;
- this.svgWidth = $(window).width();
this.svgNodeHeight = 17;
- this.fontSize = 12;
-
- let funcId = funcInfo.f;
- let initNodes = [];
- function findNodesMatchingFuncId(node) {
- if (node.f == funcId) {
- initNodes.push(node);
- } else {
- for (let child of node.c) {
- findNodesMatchingFuncId(child);
- }
- }
- }
- findNodesMatchingFuncId(callgraph);
this.initNodes = initNodes;
this.sumCount = 0;
for (let node of initNodes) {
this.sumCount += node.s;
}
-
function getMaxDepth(node) {
let depth = 0;
for (let child of node.c) {
@@ -849,24 +895,29 @@
draw(sampleWeightFunction) {
this.sampleWeightFunction = sampleWeightFunction;
this.div.empty();
- this.div.css('width', '100%').css('height', this.svgHeight + 'px');
+ this.div.append(`<p><b>${this.title}</b></p>`);
+ let svgDiv = $('<div>').appendTo(this.div);
+ this.div.append('<br/><br/>');
+ svgDiv.css('width', '100%').css('height', this.svgHeight + 'px');
let svgStr = '<svg xmlns="http://www.w3.org/2000/svg" \
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" \
- width="100%" height="100%" style="border: 1px solid black; font-family: Monospace;"> \
+ width="100%" height="100%" style="border: 1px solid black; "> \
</svg>';
- this.div.append(svgStr);
- this.svg = this.div.find('svg');
+ svgDiv.append(svgStr);
+ this.svg = svgDiv.find('svg');
this._renderBackground();
- this._renderSvgNodes(this.initNodes, 0, 0);
+ this._renderSvgNodes(this.initNodes);
this._renderUnzoomNode();
this._renderInfoNode();
this._renderPercentNode();
+ this._renderSearchNode();
// Make the added nodes in the svg visible.
- this.div.html(this.div.html());
- this.svg = this.div.find('svg');
+ svgDiv.html(svgDiv.html());
+ this.svg = svgDiv.find('svg');
this._adjustTextSize();
this._enableZoom();
this._enableInfo();
+ this._enableSearch();
this._adjustTextSizeOnResize();
}
@@ -900,36 +951,18 @@
};
}
- _renderSvgNodes(nodes, depth, xOffset) {
- let x = xOffset;
- let y = this._getYForDepth(depth);
- let sumCount = 0;
- for (let node of nodes) {
- sumCount += node.s;
+ _renderSvgNodes() {
+ let fakeNodes = [{c: this.initNodes}];
+ let children = this._splitChildrenForNodes(fakeNodes);
+ let xOffset = 0;
+ for (let child of children) {
+ xOffset = this._renderSvgNodesWithSameRoot(child, 0, xOffset);
}
- let width = this._getWidthPercentage(sumCount);
- if (width < 0.1) {
- return xOffset;
- }
- let color = this._getHeatColor(width);
- let borderColor = {};
- for (let key in color) {
- borderColor[key] = Math.max(0, color[key] - 50);
- }
- let funcName = getFuncName(nodes[0].f);
- let libName = getLibNameOfFunction(nodes[0].f);
- let sampleWeight = this.sampleWeightFunction(sumCount);
- let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' +
- sampleWeight + ')';
- this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \
- depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \
- ofill="rgb(${color.r},${color.g},${color.b})" \
- fill="rgb(${color.r},${color.g},${color.b})" \
- style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \
- <text x="${x}%" y="${y + 12}" font-size="${this.fontSize}" \
- font-family="Monospace"></text></g>`);
+ }
- let childXOffset = xOffset;
+ // Return an array of children nodes, with children having the same functionId merged in a
+ // subarray.
+ _splitChildrenForNodes(nodes) {
let map = new Map();
for (let node of nodes) {
for (let child of node.c) {
@@ -941,8 +974,52 @@
}
}
}
+ let res = [];
for (let subNodes of map.values()) {
- childXOffset = this._renderSvgNodes(subNodes, depth + 1, childXOffset);
+ res.push(subNodes.length == 1 ? subNodes[0] : subNodes);
+ }
+ return res;
+ }
+
+ // nodes can be a CallNode, or an array of CallNodes with the same functionId.
+ _renderSvgNodesWithSameRoot(nodes, depth, xOffset) {
+ let x = xOffset;
+ let y = this._getYForDepth(depth);
+ let isArray = Array.isArray(nodes);
+ let funcId;
+ let sumCount;
+ if (isArray) {
+ funcId = nodes[0].f;
+ sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0);
+ } else {
+ funcId = nodes.f;
+ sumCount = nodes.s;
+ }
+ let width = this._getWidthPercentage(sumCount);
+ if (width < 0.1) {
+ return xOffset;
+ }
+ let color = this._getHeatColor(width);
+ let borderColor = {};
+ for (let key in color) {
+ borderColor[key] = Math.max(0, color[key] - 50);
+ }
+ let funcName = getFuncName(funcId);
+ let libName = getLibNameOfFunction(funcId);
+ let sampleWeight = this.sampleWeightFunction(sumCount);
+ let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' +
+ sampleWeight + ')';
+ this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \
+ depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \
+ ofill="rgb(${color.r},${color.g},${color.b})" \
+ fill="rgb(${color.r},${color.g},${color.b})" \
+ style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \
+ <text x="${x}%" y="${y + 12}"></text></g>`);
+
+ let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c;
+ let childXOffset = xOffset;
+ for (let child of children) {
+ childXOffset = this._renderSvgNodesWithSameRoot(child, depth + 1, childXOffset);
}
return xOffset + width;
}
@@ -973,6 +1050,13 @@
x="1074" y="30"></text>`);
}
+ _renderSearchNode() {
+ this.svg.append('<rect style="stroke:rgb(0,0,0); rx="10" ry="10" \
+ x="1150" y="10" width="80" height="30" \
+ fill="rgb(255,255,255)" class="search"/> \
+ <text x="1160" y="30" class="search">Search</text>');
+ }
+
_adjustTextSizeForNode(g) {
let text = g.find('text');
let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01;
@@ -1090,6 +1174,27 @@
});
}
+ _enableSearch() {
+ this.svg.find('.search').css('cursor', 'pointer').click(() => {
+ let term = prompt('Search for:', '');
+ if (!term) {
+ this.svg.find('g > rect').each(function() {
+ this.attributes['fill'].value = this.attributes['ofill'].value;
+ });
+ } else {
+ this.svg.find('g').each(function() {
+ let title = this.getElementsByTagName('title')[0];
+ let rect = this.getElementsByTagName('rect')[0];
+ if (title.textContent.indexOf(term) != -1) {
+ rect.attributes['fill'].value = 'rgb(230,100,230)';
+ } else {
+ rect.attributes['fill'].value = rect.attributes['ofill'].value;
+ }
+ });
+ }
+ });
+ }
+
_adjustTextSizeOnResize() {
function throttle(callback) {
let running = false;
diff --git a/simpleperf/scripts/report_html.py b/simpleperf/scripts/report_html.py
index 2eaa6e9..9c8843a 100644
--- a/simpleperf/scripts/report_html.py
+++ b/simpleperf/scripts/report_html.py
@@ -16,16 +16,14 @@
#
import argparse
+import collections
import datetime
import json
import os
-import subprocess
-import sys
-import tempfile
from simpleperf_report_lib import ReportLib
-from utils import log_info, log_warning, log_exit
-from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser, remove
+from utils import log_info, log_exit
+from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser
class HtmlWriter(object):
@@ -81,8 +79,9 @@
result = {}
result['eventName'] = self.name
result['eventCount'] = self.event_count
+ processes = sorted(self.processes.values(), key=lambda a: a.event_count, reverse=True)
result['processes'] = [process.get_sample_info(gen_addr_hit_map)
- for process in self.processes.values()]
+ for process in processes]
return result
@property
@@ -120,8 +119,9 @@
result = {}
result['pid'] = self.pid
result['eventCount'] = self.event_count
+ threads = sorted(self.threads.values(), key=lambda a: a.event_count, reverse=True)
result['threads'] = [thread.get_sample_info(gen_addr_hit_map)
- for thread in self.threads.values()]
+ for thread in threads]
return result
@@ -131,6 +131,7 @@
self.tid = tid
self.name = ''
self.event_count = 0
+ self.sample_count = 0
self.libs = {} # map from lib_id to LibScope
self.call_graph = CallNode(-1)
self.reverse_call_graph = CallNode(-1)
@@ -189,6 +190,7 @@
result = {}
result['tid'] = self.tid
result['eventCount'] = self.event_count
+ result['sampleCount'] = self.sample_count
result['libs'] = [lib.gen_sample_info(gen_addr_hit_map)
for lib in self.libs.values()]
result['g'] = self.call_graph.gen_sample_info()
@@ -276,7 +278,7 @@
self.event_count = 0
self.subtree_event_count = 0
self.func_id = func_id
- self.children = {} # map from func_id to CallNode
+ self.children = collections.OrderedDict() # map from func_id to CallNode
def get_child(self, func_id):
child = self.children.get(func_id)
@@ -508,6 +510,7 @@
threadInfo = {
tid
eventCount
+ sampleCount
libs: [libInfo],
g: callGraph,
rg: reverseCallgraph
@@ -596,6 +599,7 @@
process.event_count += raw_sample.period
thread = process.get_thread(raw_sample.tid, raw_sample.thread_comm)
thread.event_count += raw_sample.period
+ thread.sample_count += 1
lib_id = self.libs.get_lib_id(symbol.dso_name)
func_id = self.functions.get_func_id(lib_id, symbol)
@@ -834,9 +838,6 @@
self.hw.add(json.dumps(record_data))
self.hw.close_tag()
- def write_flamegraph(self, flamegraph):
- self.hw.add(flamegraph)
-
def write_script(self):
self.hw.open_tag('script').add_file('report_html.js').close_tag()
@@ -846,21 +847,6 @@
self.hw.close()
-def gen_flamegraph(record_file, show_art_frames):
- fd, flamegraph_path = tempfile.mkstemp()
- os.close(fd)
- inferno_script_path = os.path.join(get_script_dir(), 'inferno', 'inferno.py')
- args = [sys.executable, inferno_script_path, '-sc', '-o', flamegraph_path,
- '--record_file', record_file, '--embedded_flamegraph', '--no_browser']
- if show_art_frames:
- args.append('--show_art_frames')
- subprocess.check_call(args)
- with open(flamegraph_path, 'r') as fh:
- data = fh.read()
- remove(flamegraph_path)
- return data
-
-
def main():
parser = argparse.ArgumentParser(description='report profiling data')
parser.add_argument('-i', '--record_file', nargs='+', default=['perf.data'], help="""
@@ -915,11 +901,6 @@
report_generator.write_content_div()
report_generator.write_record_data(record_data.gen_record_info())
report_generator.write_script()
- # 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], args.show_art_frames)
- report_generator.write_flamegraph(flamegraph)
report_generator.finish()
if not args.no_browser: