1733 lines
623 KiB
HTML
1733 lines
623 KiB
HTML
|
<html><head><link href="https://code.jquery.com/ui/1.12.0/themes/smoothness/jquery-ui.css" type="text/css" rel="stylesheet"></link>
|
||
|
<link href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css" type="text/css" rel="stylesheet"></link>
|
||
|
<script src="https://www.gstatic.com/charts/loader.js"></script>
|
||
|
<script>google.charts.load('current', {'packages': ['corechart', 'table']});</script>
|
||
|
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
|
||
|
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
||
|
<script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js"></script>
|
||
|
<script src="https://cdn.datatables.net/1.10.16/js/dataTables.jqueryui.min.js"></script>
|
||
|
<style type="text/css">
|
||
|
.colForLine { width: 50px; }
|
||
|
.colForCount { width: 100px; }
|
||
|
.tableCell { font-size: 17px; }
|
||
|
.boldTableCell { font-weight: bold; font-size: 17px; }
|
||
|
</style>
|
||
|
</head>
|
||
|
<body><div id="report_content"></div>
|
||
|
<script type="application/json" id="record_data">{"functionMap": {"0": {"d": [["", 0], ["binary_cache/system/lib64/libc.so: file format elf64-littleaarch64", 0], ["", 0], ["", 0], ["Disassembly of section .text:", 0], ["", 0], ["0000000000065a88 <strtoimax>:", 0], ["strtoq():", 0], [" 65a88:\tsub\tsp, sp, #0x70", 416392], [" 65a8c:\tstp\tx28, x27, [sp,#16]", 416396], [" 65a90:\tstp\tx26, x25, [sp,#32]", 416400], [" 65a94:\tstp\tx24, x23, [sp,#48]", 416404], [" 65a98:\tstp\tx22, x21, [sp,#64]", 416408], [" 65a9c:\tstp\tx20, x19, [sp,#80]", 416412], [" 65aa0:\tstp\tx29, x30, [sp,#96]", 416416], [" 65aa4:\tadd\tx29, sp, #0x60", 416420], [" 65aa8:\tmov\tw22, w2", 416424], [" 65aac:\tmov\tx19, x1", 416428], [" 65ab0:\tmov\tx20, x0", 416432], [" 65ab4:\tcmp\tw22, #0x24", 416436], [" 65ab8:\tb.hi\t65af0 <strtoimax+0x68>", 416440], [" 65abc:\tcmp\tw22, #0x1", 416444], [" 65ac0:\tb.eq\t65af0 <strtoimax+0x68>", 416448], [" 65ac4:\tmov\tx27, x20", 416452], [" 65ac8:\tldrb\tw21, [x27],#1", 416456], [" 65acc:\tmov\tw0, w21", 416460], [" 65ad0:\tbl\t19200 <isspace@plt>", 416464], [" 65ad4:\tcbnz\tw0, 65ac8 <strtoimax+0x40>", 416468], [" 65ad8:\tcmp\tw21, #0x2b", 416472], [" 65adc:\tb.eq\t65b0c <strtoimax+0x84>", 416476], [" 65ae0:\tcmp\tw21, #0x2d", 416480], [" 65ae4:\tb.ne\t65b18 <strtoimax+0x90>", 416484], [" 65ae8:\torr\tw23, wzr, #0x1", 416488], [" 65aec:\tb\t65b10 <strtoimax+0x88>", 416492], [" 65af0:\tcbz\tx19, 65af8 <strtoimax+0x70>", 416496], [" 65af4:\tstr\tx20, [x19]", 416500], [" 65af8:\tbl\t18e10 <__errno@plt>", 416504], [" 65afc:\tmov\tw8, #0x16 \t// #22", 416508], [" 65b00:\tmov\tx22, xzr", 416512], [" 65b04:\tstr\tw8, [x0]", 416516], [" 65b08:\tb\t65c78 <strtoimax+0x1f0>", 416520], [" 65b0c:\tmov\tw23, wzr", 416524], [" 65b10:\tldrb\tw21, [x27],#1", 416528], [" 65b14:\tb\t65b1c <strtoimax+0x94>", 416532], [" 65b18:\tmov\tw23, wzr", 416536], [" 65b1c:\torr\tw8, w22, #0x10", 416540], [" 65b20:\tcmp\tw8, #0x10", 416544], [" 65b24:\tb.ne\t65b54 <strtoimax+0xcc>", 416548], [" 65b28:\tcmp\tw21, #0x30", 416552], [" 65b2c:\tb.ne\t65b54 <strtoimax+0xcc>", 416556], [" 65b30:\tldrb\tw8, [x27]", 416560], [" 65b34:\torr\tw8, w8, #0x20", 416564], [" 65b38:\tcmp\tw8, #0x78", 416568], [" 65b3c:\tb.ne\t65b50 <strtoimax+0xc8>", 416572], [" 65b40:\tldrb\tw21, [x27,#1]", 416576], [" 65b44:\tadd\tx27, x27, #0x2", 416580], [" 65b48:\torr\tw22, wzr, #0x10", 416584], [" 65b4c:\tb\t65b54 <strtoimax+0xcc>", 416588], [" 65b50:\torr\tw21, wzr, #0x30", 416592], [" 65b54:\tcmp\tw21, #0x30", 416596], [" 65b58:\tstp\tx20, x19, [sp]", 416600], [" 65b5c:\tmov\tw8, #0xa \t// #10", 416604], [" 65b60:\torr\tw9, wzr, #0x8", 416608], [" 65b64:\tcsel\tw8, w9, w8, eq", 416612], [" 65b68:\tcmp\tw22, #0x0", 416616], [" 65b6c:\tcsel\tw8, w8, w22, eq", 416620], [" 65b70:\tcmp\tw23, #0x0", 416624], [" 65b74:\torr\tx9, xzr, #0x8000000000000000", 416628], [" 65b78:\tcinv\tx9, x9, eq", 416632], [" 65b7c:\tsxtw\tx24, w8", 416636], [" 65b80:\tsdiv\tx25, x9, x24", 416640], [" 65b84:\tmsub\tw26, w25, w8, w9", 416644], [" 65b88:\tcbz\tw23, 65b9c <strtoimax+0x114>", 416648], [" 65b8c:\tcmp\tw26, #0x0", 416652], [" 65b90:\tcsel\tw8, w24, wzr, gt", 416656], [" 65b94:\tcinc\tx25, x25, gt", 416660], [" 65b98:\tsub\tw26, w8, w26", 416664], [" 65b9c:\tmov\tx22, xzr", 416668], [" 65ba0:\tmov\tw20, wzr", 416672], [" 65ba4:\tsub\tx27, x27, #0x1", 416676], [" 65ba8:\tmov\tw19, #0x57 \t// #87", 416680], [" 65bac:\tmov\tw28, #0x37 \t// #55", 416684], [" 65bb0:\tb\t65bb8 <strtoimax+0x130>", 416688], [" 65bb4:\tldrb\tw21, [x27,#1]!", 416692], [" 65bb8:\tsub\tw8, w21, #0x30", 416696], [" 65bbc:\tcmp\tw8, #0xa", 416700], [" 65bc0:\tb.cc\t65be4 <strtoimax+0x15c>", 416704], [" 65bc4:\tmov\tw0, w21", 416708], [" 65bc8:\tbl\t19190 <isalpha@plt>
|
||
|
<script>/*
|
||
|
* Copyright (C) 2017 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
'use strict';
|
||
|
|
||
|
// Use IIFE to avoid leaking names to other scripts.
|
||
|
$(document).ready(function() {
|
||
|
|
||
|
function openHtml(name, attrs={}) {
|
||
|
let s = `<${name} `;
|
||
|
for (let key in attrs) {
|
||
|
s += `${key}="${attrs[key]}" `;
|
||
|
}
|
||
|
s += '>';
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
function closeHtml(name) {
|
||
|
return `</${name}>`;
|
||
|
}
|
||
|
|
||
|
function getHtml(name, attrs={}) {
|
||
|
let text;
|
||
|
if ('text' in attrs) {
|
||
|
text = attrs.text;
|
||
|
delete attrs.text;
|
||
|
}
|
||
|
let s = openHtml(name, attrs);
|
||
|
if (text) {
|
||
|
s += text;
|
||
|
}
|
||
|
s += closeHtml(name);
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
function getTableRow(cols, colName, attrs={}) {
|
||
|
let s = openHtml('tr', attrs);
|
||
|
for (let col of cols) {
|
||
|
s += `<${colName}>${col}</${colName}>`;
|
||
|
}
|
||
|
s += '</tr>';
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
function toPercentageStr(percentage) {
|
||
|
return percentage.toFixed(2) + '%';
|
||
|
}
|
||
|
|
||
|
function getProcessName(pid) {
|
||
|
let name = gProcesses[pid];
|
||
|
return name ? `${pid} (${name})`: pid.toString();
|
||
|
}
|
||
|
|
||
|
function getThreadName(tid) {
|
||
|
let name = gThreads[tid];
|
||
|
return name ? `${tid} (${name})`: tid.toString();
|
||
|
}
|
||
|
|
||
|
function getLibName(libId) {
|
||
|
return gLibList[libId];
|
||
|
}
|
||
|
|
||
|
function getFuncName(funcId) {
|
||
|
return gFunctionMap[funcId].f;
|
||
|
}
|
||
|
|
||
|
function getLibNameOfFunction(funcId) {
|
||
|
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 getFuncDisassembly(funcId) {
|
||
|
let func = gFunctionMap[funcId];
|
||
|
return func.hasOwnProperty('d') ? func.d : null;
|
||
|
}
|
||
|
|
||
|
function getSourceFilePath(sourceFileId) {
|
||
|
return gSourceFiles[sourceFileId].path;
|
||
|
}
|
||
|
|
||
|
function getSourceCode(sourceFileId) {
|
||
|
return gSourceFiles[sourceFileId].code;
|
||
|
}
|
||
|
|
||
|
function isClockEvent(eventInfo) {
|
||
|
return eventInfo.eventName.includes('task-clock') ||
|
||
|
eventInfo.eventName.includes('cpu-clock');
|
||
|
}
|
||
|
|
||
|
class TabManager {
|
||
|
constructor(divContainer) {
|
||
|
this.div = $('<div>', {id: 'tabs'});
|
||
|
this.div.appendTo(divContainer);
|
||
|
this.div.append(getHtml('ul'));
|
||
|
this.tabs = [];
|
||
|
this.isDrawCalled = false;
|
||
|
}
|
||
|
|
||
|
addTab(title, tabObj) {
|
||
|
let id = 'tab_' + this.div.children().length;
|
||
|
let tabDiv = $('<div>', {id: id});
|
||
|
tabDiv.appendTo(this.div);
|
||
|
this.div.children().first().append(
|
||
|
getHtml('li', {text: getHtml('a', {href: '#' + id, text: title})}));
|
||
|
tabObj.init(tabDiv);
|
||
|
this.tabs.push(tabObj);
|
||
|
if (this.isDrawCalled) {
|
||
|
this.div.tabs('refresh');
|
||
|
}
|
||
|
return tabObj;
|
||
|
}
|
||
|
|
||
|
findTab(title) {
|
||
|
let links = this.div.find('li a');
|
||
|
for (let i = 0; i < links.length; ++i) {
|
||
|
if (links.eq(i).text() == title) {
|
||
|
return this.tabs[i];
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
this.div.tabs({
|
||
|
active: 0,
|
||
|
});
|
||
|
this.tabs.forEach(function(tab) {
|
||
|
tab.draw();
|
||
|
});
|
||
|
this.isDrawCalled = true;
|
||
|
}
|
||
|
|
||
|
setActive(tabObj) {
|
||
|
for (let i = 0; i < this.tabs.length; ++i) {
|
||
|
if (this.tabs[i] == tabObj) {
|
||
|
this.div.tabs('option', 'active', i);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Show global information retrieved from the record file, including:
|
||
|
// record time
|
||
|
// machine type
|
||
|
// Android version
|
||
|
// record cmdline
|
||
|
// total samples
|
||
|
class RecordFileView {
|
||
|
constructor(divContainer) {
|
||
|
this.div = $('<div>');
|
||
|
this.div.appendTo(divContainer);
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
google.charts.setOnLoadCallback(() => this.realDraw());
|
||
|
}
|
||
|
|
||
|
realDraw() {
|
||
|
this.div.empty();
|
||
|
// Draw a table of 'Name', 'Value'.
|
||
|
let rows = [];
|
||
|
if (gRecordInfo.recordTime) {
|
||
|
rows.push(['Record Time', gRecordInfo.recordTime]);
|
||
|
}
|
||
|
if (gRecordInfo.machineType) {
|
||
|
rows.push(['Machine Type', gRecordInfo.machineType]);
|
||
|
}
|
||
|
if (gRecordInfo.androidVersion) {
|
||
|
rows.push(['Android Version', gRecordInfo.androidVersion]);
|
||
|
}
|
||
|
if (gRecordInfo.recordCmdline) {
|
||
|
rows.push(['Record cmdline', gRecordInfo.recordCmdline]);
|
||
|
}
|
||
|
rows.push(['Total Samples', '' + gRecordInfo.totalSamples]);
|
||
|
|
||
|
let data = new google.visualization.DataTable();
|
||
|
data.addColumn('string', '');
|
||
|
data.addColumn('string', '');
|
||
|
data.addRows(rows);
|
||
|
for (let i = 0; i < rows.length; ++i) {
|
||
|
data.setProperty(i, 0, 'className', 'boldTableCell');
|
||
|
}
|
||
|
let table = new google.visualization.Table(this.div.get(0));
|
||
|
table.draw(data, {
|
||
|
width: '100%',
|
||
|
sort: 'disable',
|
||
|
allowHtml: true,
|
||
|
cssClassNames: {
|
||
|
'tableCell': 'tableCell',
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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.eventInfo = eventInfo;
|
||
|
this.processInfo = null;
|
||
|
this.threadInfo = null;
|
||
|
this.libInfo = null;
|
||
|
this.states = {
|
||
|
SHOW_EVENT_INFO: 1,
|
||
|
SHOW_PROCESS_INFO: 2,
|
||
|
SHOW_THREAD_INFO: 3,
|
||
|
SHOW_LIB_INFO: 4,
|
||
|
};
|
||
|
if (isClockEvent(this.eventInfo)) {
|
||
|
this.getSampleWeight = function (eventCount) {
|
||
|
return (eventCount / 1000000.0).toFixed(3) + ' ms';
|
||
|
}
|
||
|
} else {
|
||
|
this.getSampleWeight = (eventCount) => '' + eventCount;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_getState() {
|
||
|
if (this.libInfo) {
|
||
|
return this.states.SHOW_LIB_INFO;
|
||
|
}
|
||
|
if (this.threadInfo) {
|
||
|
return this.states.SHOW_THREAD_INFO;
|
||
|
}
|
||
|
if (this.processInfo) {
|
||
|
return this.states.SHOW_PROCESS_INFO;
|
||
|
}
|
||
|
return this.states.SHOW_EVENT_INFO;
|
||
|
}
|
||
|
|
||
|
_goBack() {
|
||
|
let state = this._getState();
|
||
|
if (state == this.states.SHOW_PROCESS_INFO) {
|
||
|
this.processInfo = null;
|
||
|
} else if (state == this.states.SHOW_THREAD_INFO) {
|
||
|
this.threadInfo = null;
|
||
|
} else if (state == this.states.SHOW_LIB_INFO) {
|
||
|
this.libInfo = null;
|
||
|
}
|
||
|
this.draw();
|
||
|
}
|
||
|
|
||
|
_selectHandler(chart) {
|
||
|
let selectedItem = chart.getSelection()[0];
|
||
|
if (selectedItem) {
|
||
|
let state = this._getState();
|
||
|
if (state == this.states.SHOW_EVENT_INFO) {
|
||
|
this.processInfo = this.eventInfo.processes[selectedItem.row];
|
||
|
} else if (state == this.states.SHOW_PROCESS_INFO) {
|
||
|
this.threadInfo = this.processInfo.threads[selectedItem.row];
|
||
|
} else if (state == this.states.SHOW_THREAD_INFO) {
|
||
|
this.libInfo = this.threadInfo.libs[selectedItem.row];
|
||
|
}
|
||
|
this.draw();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
google.charts.setOnLoadCallback(() => this.realDraw());
|
||
|
}
|
||
|
|
||
|
realDraw() {
|
||
|
this.div.empty();
|
||
|
this._drawTitle();
|
||
|
this._drawPieChart();
|
||
|
}
|
||
|
|
||
|
_drawTitle() {
|
||
|
// Draw a table of 'Name', 'Event Count'.
|
||
|
let rows = [];
|
||
|
rows.push(['Event Type: ' + this.eventInfo.eventName,
|
||
|
this.getSampleWeight(this.eventInfo.eventCount)]);
|
||
|
if (this.processInfo) {
|
||
|
rows.push(['Process: ' + getProcessName(this.processInfo.pid),
|
||
|
this.getSampleWeight(this.processInfo.eventCount)]);
|
||
|
}
|
||
|
if (this.threadInfo) {
|
||
|
rows.push(['Thread: ' + getThreadName(this.threadInfo.tid),
|
||
|
this.getSampleWeight(this.threadInfo.eventCount)]);
|
||
|
}
|
||
|
if (this.libInfo) {
|
||
|
rows.push(['Library: ' + getLibName(this.libInfo.libId),
|
||
|
this.getSampleWeight(this.libInfo.eventCount)]);
|
||
|
}
|
||
|
let data = new google.visualization.DataTable();
|
||
|
data.addColumn('string', '');
|
||
|
data.addColumn('string', '');
|
||
|
data.addRows(rows);
|
||
|
for (let i = 0; i < rows.length; ++i) {
|
||
|
data.setProperty(i, 0, 'className', 'boldTableCell');
|
||
|
}
|
||
|
let wrapperDiv = $('<div>');
|
||
|
wrapperDiv.appendTo(this.div);
|
||
|
let table = new google.visualization.Table(wrapperDiv.get(0));
|
||
|
table.draw(data, {
|
||
|
width: '100%',
|
||
|
sort: 'disable',
|
||
|
allowHtml: true,
|
||
|
cssClassNames: {
|
||
|
'tableCell': 'tableCell',
|
||
|
},
|
||
|
});
|
||
|
if (this._getState() != this.states.SHOW_EVENT_INFO) {
|
||
|
let button = $('<button>', {text: 'Back'});
|
||
|
button.appendTo(this.div);
|
||
|
button.button().click(() => this._goBack());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_drawPieChart() {
|
||
|
let state = this._getState();
|
||
|
let title = null;
|
||
|
let firstColumn = null;
|
||
|
let rows = [];
|
||
|
let thisObj = this;
|
||
|
function getItem(name, eventCount, totalEventCount) {
|
||
|
let sampleWeight = thisObj.getSampleWeight(eventCount);
|
||
|
let percent = (eventCount * 100.0 / totalEventCount).toFixed(2) + '%';
|
||
|
return [name, eventCount, getHtml('pre', {text: name}) +
|
||
|
getHtml('b', {text: `${sampleWeight} (${percent})`})];
|
||
|
}
|
||
|
|
||
|
if (state == this.states.SHOW_EVENT_INFO) {
|
||
|
title = 'Processes in event type ' + this.eventInfo.eventName;
|
||
|
firstColumn = 'Process';
|
||
|
for (let process of this.eventInfo.processes) {
|
||
|
rows.push(getItem('Process: ' + getProcessName(process.pid), process.eventCount,
|
||
|
this.eventInfo.eventCount));
|
||
|
}
|
||
|
} else if (state == this.states.SHOW_PROCESS_INFO) {
|
||
|
title = 'Threads in process ' + getProcessName(this.processInfo.pid);
|
||
|
firstColumn = 'Thread';
|
||
|
for (let thread of this.processInfo.threads) {
|
||
|
rows.push(getItem('Thread: ' + getThreadName(thread.tid), thread.eventCount,
|
||
|
this.processInfo.eventCount));
|
||
|
}
|
||
|
} else if (state == this.states.SHOW_THREAD_INFO) {
|
||
|
title = 'Libraries in thread ' + getThreadName(this.threadInfo.tid);
|
||
|
firstColumn = 'Library';
|
||
|
for (let lib of this.threadInfo.libs) {
|
||
|
rows.push(getItem('Library: ' + getLibName(lib.libId), lib.eventCount,
|
||
|
this.threadInfo.eventCount));
|
||
|
}
|
||
|
} else if (state == this.states.SHOW_LIB_INFO) {
|
||
|
title = 'Functions in library ' + getLibName(this.libInfo.libId);
|
||
|
firstColumn = 'Function';
|
||
|
for (let func of this.libInfo.functions) {
|
||
|
rows.push(getItem('Function: ' + getFuncName(func.g.f), func.g.e,
|
||
|
this.libInfo.eventCount));
|
||
|
}
|
||
|
}
|
||
|
let data = new google.visualization.DataTable();
|
||
|
data.addColumn('string', firstColumn);
|
||
|
data.addColumn('number', 'EventCount');
|
||
|
data.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
|
||
|
data.addRows(rows);
|
||
|
|
||
|
let wrapperDiv = $('<div>');
|
||
|
wrapperDiv.appendTo(this.div);
|
||
|
let chart = new google.visualization.PieChart(wrapperDiv.get(0));
|
||
|
chart.draw(data, {
|
||
|
title: title,
|
||
|
width: 1000,
|
||
|
height: 600,
|
||
|
tooltip: {isHtml: true},
|
||
|
});
|
||
|
google.visualization.events.addListener(chart, 'select', () => this._selectHandler(chart));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
class ChartStatTab {
|
||
|
constructor() {
|
||
|
}
|
||
|
|
||
|
init(div) {
|
||
|
this.div = div;
|
||
|
this.recordFileView = new RecordFileView(this.div);
|
||
|
this.chartViews = [];
|
||
|
for (let eventInfo of gSampleInfo) {
|
||
|
this.chartViews.push(new ChartView(this.div, eventInfo));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
this.recordFileView.draw();
|
||
|
for (let charView of this.chartViews) {
|
||
|
charView.draw();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
class SampleTableTab {
|
||
|
constructor() {
|
||
|
}
|
||
|
|
||
|
init(div) {
|
||
|
this.div = div;
|
||
|
this.selectorView = null;
|
||
|
this.sampleTableViews = [];
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
this.selectorView = new SampleTableWeightSelectorView(this.div, gSampleInfo[0],
|
||
|
() => this.onSampleWeightChange());
|
||
|
this.selectorView.draw();
|
||
|
for (let eventInfo of gSampleInfo) {
|
||
|
this.div.append(getHtml('hr'));
|
||
|
this.sampleTableViews.push(new SampleTableView(this.div, eventInfo));
|
||
|
}
|
||
|
this.onSampleWeightChange();
|
||
|
}
|
||
|
|
||
|
onSampleWeightChange() {
|
||
|
for (let i = 0; i < gSampleInfo.length; ++i) {
|
||
|
let sampleWeightFunction = this.selectorView.getSampleWeightFunction(gSampleInfo[i]);
|
||
|
let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix(gSampleInfo[i]);
|
||
|
this.sampleTableViews[i].draw(sampleWeightFunction, sampleWeightSuffix);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Select the way to show sample weight in SampleTableTab.
|
||
|
// 1. Show percentage of event count.
|
||
|
// 2. Show event count (For cpu-clock and task-clock events, it is time in ms).
|
||
|
class SampleTableWeightSelectorView {
|
||
|
constructor(divContainer, firstEventInfo, onSelectChange) {
|
||
|
this.div = $('<div>');
|
||
|
this.div.appendTo(divContainer);
|
||
|
this.onSelectChange = onSelectChange;
|
||
|
this.options = {
|
||
|
SHOW_PERCENT: 0,
|
||
|
SHOW_EVENT_COUNT: 1,
|
||
|
};
|
||
|
if (isClockEvent(firstEventInfo)) {
|
||
|
this.curOption = this.options.SHOW_EVENT_COUNT;
|
||
|
} else {
|
||
|
this.curOption = this.options.SHOW_PERCENT;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
let options = ['Show percentage of event count', 'Show event count'];
|
||
|
let optionStr = '';
|
||
|
for (let i = 0; i < options.length; ++i) {
|
||
|
optionStr += getHtml('option', {value: i, text: options[i]});
|
||
|
}
|
||
|
this.div.append(getHtml('select', {text: optionStr}));
|
||
|
let selectMenu = this.div.children().last();
|
||
|
selectMenu.children().eq(this.curOption).attr('selected', 'selected');
|
||
|
let thisObj = this;
|
||
|
selectMenu.selectmenu({
|
||
|
change: function() {
|
||
|
thisObj.curOption = this.value;
|
||
|
thisObj.onSelectChange();
|
||
|
},
|
||
|
width: '100%',
|
||
|
});
|
||
|
}
|
||
|
|
||
|
getSampleWeightFunction(eventInfo) {
|
||
|
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);
|
||
|
}
|
||
|
return (eventCount) => '' + eventCount;
|
||
|
}
|
||
|
|
||
|
getSampleWeightSuffix(eventInfo) {
|
||
|
if (this.curOption == this.options.SHOW_EVENT_COUNT && isClockEvent(eventInfo)) {
|
||
|
return ' ms';
|
||
|
}
|
||
|
return '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
class SampleTableView {
|
||
|
constructor(divContainer, eventInfo) {
|
||
|
this.id = divContainer.children().length;
|
||
|
this.div = $('<div>');
|
||
|
this.div.appendTo(divContainer);
|
||
|
this.eventInfo = eventInfo;
|
||
|
}
|
||
|
|
||
|
draw(getSampleWeight, sampleWeightSuffix) {
|
||
|
// Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library', 'Function'.
|
||
|
this.div.empty();
|
||
|
let eventInfo = this.eventInfo;
|
||
|
let sampleWeight = getSampleWeight(eventInfo.eventCount);
|
||
|
this.div.append(getHtml('p', {text: `Sample table for event ${eventInfo.eventName}, ` +
|
||
|
`total count ${sampleWeight}${sampleWeightSuffix}`}));
|
||
|
let tableId = 'sampleTable_' + this.id;
|
||
|
let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : '';
|
||
|
let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples',
|
||
|
'Process', 'Thread', 'Library', 'Function'];
|
||
|
let tableStr = openHtml('table', {id: tableId, cellspacing: '0', width: '100%'}) +
|
||
|
getHtml('thead', {text: getTableRow(titles, 'th')}) +
|
||
|
getHtml('tfoot', {text: getTableRow(titles, 'th')}) +
|
||
|
openHtml('tbody');
|
||
|
for (let i = 0; i < eventInfo.processes.length; ++i) {
|
||
|
let processInfo = eventInfo.processes[i];
|
||
|
let processName = getProcessName(processInfo.pid);
|
||
|
for (let j = 0; j < processInfo.threads.length; ++j) {
|
||
|
let threadInfo = processInfo.threads[j];
|
||
|
let threadName = getThreadName(threadInfo.tid);
|
||
|
for (let k = 0; k < threadInfo.libs.length; ++k) {
|
||
|
let lib = threadInfo.libs[k];
|
||
|
let libName = getLibName(lib.libId);
|
||
|
for (let t = 0; t < lib.functions.length; ++t) {
|
||
|
let func = lib.functions[t];
|
||
|
let key = [i, j, k, t].join('_');
|
||
|
let totalValue = getSampleWeight(func.g.s);
|
||
|
let selfValue = getSampleWeight(func.g.e);
|
||
|
tableStr += getTableRow([totalValue, selfValue, func.c,
|
||
|
processName, threadName, libName,
|
||
|
getFuncName(func.g.f)], 'td', {key: key});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
tableStr += closeHtml('tbody') + closeHtml('table');
|
||
|
this.div.append(tableStr);
|
||
|
let table = this.div.find(`table#${tableId}`).dataTable({
|
||
|
lengthMenu: [10, 20, 50, 100, -1],
|
||
|
processing: true,
|
||
|
order: [0, 'desc'],
|
||
|
responsive: true,
|
||
|
});
|
||
|
|
||
|
table.find('tr').css('cursor', 'pointer');
|
||
|
table.on('click', 'tr', function() {
|
||
|
let key = this.getAttribute('key');
|
||
|
if (!key) {
|
||
|
return;
|
||
|
}
|
||
|
let indexes = key.split('_');
|
||
|
let processInfo = eventInfo.processes[indexes[0]];
|
||
|
let threadInfo = processInfo.threads[indexes[1]];
|
||
|
let lib = threadInfo.libs[indexes[2]];
|
||
|
let func = lib.functions[indexes[3]];
|
||
|
FunctionTab.showFunction(eventInfo, processInfo, threadInfo, lib, func);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// 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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// 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';
|
||
|
let tab = gTabs.findTab(title);
|
||
|
if (!tab) {
|
||
|
tab = gTabs.addTab(title, new FunctionTab());
|
||
|
}
|
||
|
tab.setFunction(eventInfo, processInfo, threadInfo, lib, func);
|
||
|
}
|
||
|
|
||
|
constructor() {
|
||
|
this.func = null;
|
||
|
this.selectPercent = 'thread';
|
||
|
}
|
||
|
|
||
|
init(div) {
|
||
|
this.div = div;
|
||
|
}
|
||
|
|
||
|
setFunction(eventInfo, processInfo, threadInfo, lib, func) {
|
||
|
this.eventInfo = eventInfo;
|
||
|
this.processInfo = processInfo;
|
||
|
this.threadInfo = threadInfo;
|
||
|
this.lib = lib;
|
||
|
this.func = func;
|
||
|
this.selectorView = null;
|
||
|
this.callgraphView = null;
|
||
|
this.reverseCallgraphView = null;
|
||
|
this.sourceCodeView = null;
|
||
|
this.disassemblyView = null;
|
||
|
this.draw();
|
||
|
gTabs.setActive(this);
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
if (!this.func) {
|
||
|
return;
|
||
|
}
|
||
|
this.div.empty();
|
||
|
this._drawTitle();
|
||
|
|
||
|
this.selectorView = new FunctionSampleWeightSelectorView(this.div, this.eventInfo,
|
||
|
this.processInfo, this.threadInfo, () => this.onSampleWeightChange());
|
||
|
this.selectorView.draw();
|
||
|
|
||
|
this.div.append(getHtml('hr'));
|
||
|
let funcName = getFuncName(this.func.g.f);
|
||
|
this.div.append(getHtml('b', {text: `Functions called by ${funcName}`}) + '<br/>');
|
||
|
this.callgraphView = new FlameGraphView(this.div, this.func.g, false);
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
let disassembly = collectDisassemblyForFunction(this.func);
|
||
|
if (disassembly) {
|
||
|
this.div.append(getHtml('hr'));
|
||
|
this.div.append(getHtml('b', {text: 'Disassembly:'}) + '<br/>');
|
||
|
this.disassemblyView = new DisassemblyView(this.div, disassembly);
|
||
|
}
|
||
|
|
||
|
this.onSampleWeightChange(); // Manually set sample weight function for the first time.
|
||
|
}
|
||
|
|
||
|
_drawTitle() {
|
||
|
let eventName = this.eventInfo.eventName;
|
||
|
let processName = getProcessName(this.processInfo.pid);
|
||
|
let threadName = getThreadName(this.threadInfo.tid);
|
||
|
let libName = getLibName(this.lib.libId);
|
||
|
let funcName = getFuncName(this.func.g.f);
|
||
|
// Draw a table of 'Name', 'Value'.
|
||
|
let rows = [];
|
||
|
rows.push(['Event Type', eventName]);
|
||
|
rows.push(['Process', processName]);
|
||
|
rows.push(['Thread', threadName]);
|
||
|
rows.push(['Library', libName]);
|
||
|
rows.push(['Function', getHtml('pre', {text: funcName})]);
|
||
|
let data = new google.visualization.DataTable();
|
||
|
data.addColumn('string', '');
|
||
|
data.addColumn('string', '');
|
||
|
data.addRows(rows);
|
||
|
for (let i = 0; i < rows.length; ++i) {
|
||
|
data.setProperty(i, 0, 'className', 'boldTableCell');
|
||
|
}
|
||
|
let wrapperDiv = $('<div>');
|
||
|
wrapperDiv.appendTo(this.div);
|
||
|
let table = new google.visualization.Table(wrapperDiv.get(0));
|
||
|
table.draw(data, {
|
||
|
width: '100%',
|
||
|
sort: 'disable',
|
||
|
allowHtml: true,
|
||
|
cssClassNames: {
|
||
|
'tableCell': 'tableCell',
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
onSampleWeightChange() {
|
||
|
let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
|
||
|
if (this.callgraphView) {
|
||
|
this.callgraphView.draw(sampleWeightFunction);
|
||
|
}
|
||
|
if (this.reverseCallgraphView) {
|
||
|
this.reverseCallgraphView.draw(sampleWeightFunction);
|
||
|
}
|
||
|
if (this.sourceCodeView) {
|
||
|
this.sourceCodeView.draw(sampleWeightFunction);
|
||
|
}
|
||
|
if (this.disassemblyView) {
|
||
|
this.disassemblyView.draw(sampleWeightFunction);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Select the way to show sample weight in 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) {
|
||
|
this.div = $('<div>');
|
||
|
this.div.appendTo(divContainer);
|
||
|
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,
|
||
|
PERCENT_TO_CUR_THREAD: 2,
|
||
|
RAW_EVENT_COUNT: 3,
|
||
|
EVENT_COUNT_IN_TIME: 4,
|
||
|
};
|
||
|
let name = eventInfo.eventName;
|
||
|
this.supportEventCountInTime = isClockEvent(eventInfo);
|
||
|
if (this.supportEventCountInTime) {
|
||
|
this.curOption = this.options.EVENT_COUNT_IN_TIME;
|
||
|
} else {
|
||
|
this.curOption = this.options.PERCENT_TO_CUR_THREAD;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
draw() {
|
||
|
let options = [];
|
||
|
options.push('Show percentage of event count relative to all processes.');
|
||
|
options.push('Show percentage of event count relative to the current process.');
|
||
|
options.push('Show percentage of event count relative to the current thread.');
|
||
|
options.push('Show event count.');
|
||
|
if (this.supportEventCountInTime) {
|
||
|
options.push('Show event count in milliseconds.');
|
||
|
}
|
||
|
let optionStr = '';
|
||
|
for (let i = 0; i < options.length; ++i) {
|
||
|
optionStr += getHtml('option', {value: i, text: options[i]});
|
||
|
}
|
||
|
this.div.append(getHtml('select', {text: optionStr}));
|
||
|
let selectMenu = this.div.children().last();
|
||
|
selectMenu.children().eq(this.curOption).attr('selected', 'selected');
|
||
|
let thisObj = this;
|
||
|
selectMenu.selectmenu({
|
||
|
change: function() {
|
||
|
thisObj.curOption = this.value;
|
||
|
thisObj.onSelectChange();
|
||
|
},
|
||
|
width: '100%',
|
||
|
});
|
||
|
}
|
||
|
|
||
|
getSampleWeightFunction() {
|
||
|
let thisObj = this;
|
||
|
if (this.curOption == this.options.PERCENT_TO_ALL_PROCESSES) {
|
||
|
return function(eventCount) {
|
||
|
let percent = eventCount * 100.0 / thisObj.eventCountForAllProcesses;
|
||
|
return percent.toFixed(2) + '%';
|
||
|
};
|
||
|
}
|
||
|
if (this.curOption == this.options.PERCENT_TO_CUR_PROCESS) {
|
||
|
return function(eventCount) {
|
||
|
let percent = eventCount * 100.0 / thisObj.eventCountForProcess;
|
||
|
return percent.toFixed(2) + '%';
|
||
|
};
|
||
|
}
|
||
|
if (this.curOption == this.options.PERCENT_TO_CUR_THREAD) {
|
||
|
return function(eventCount) {
|
||
|
let percent = eventCount * 100.0 / thisObj.eventCountForThread;
|
||
|
return percent.toFixed(2) + '%';
|
||
|
};
|
||
|
}
|
||
|
if (this.curOption == this.options.RAW_EVENT_COUNT) {
|
||
|
return function(eventCount) {
|
||
|
return '' + eventCount;
|
||
|
};
|
||
|
}
|
||
|
if (this.curOption == this.options.EVENT_COUNT_IN_TIME) {
|
||
|
return function(eventCount) {
|
||
|
let timeInMs = eventCount / 1000000.0;
|
||
|
return timeInMs.toFixed(3) + ' ms';
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// 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, reverseOrder) {
|
||
|
this.id = divContainer.children().length;
|
||
|
this.div = $('<div>', {id: 'fg_' + this.id});
|
||
|
this.div.appendTo(divContainer);
|
||
|
this.callgraph = callgraph;
|
||
|
this.reverseOrder = reverseOrder;
|
||
|
this.sampleWeightFunction = null;
|
||
|
this.svgWidth = $(window).width();
|
||
|
this.svgNodeHeight = 17;
|
||
|
this.fontSize = 12;
|
||
|
|
||
|
function getMaxDepth(node) {
|
||
|
let depth = 0;
|
||
|
for (let child of node.c) {
|
||
|
depth = Math.max(depth, getMaxDepth(child));
|
||
|
}
|
||
|
return depth + 1;
|
||
|
}
|
||
|
this.maxDepth = getMaxDepth(this.callgraph);
|
||
|
this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3);
|
||
|
}
|
||
|
|
||
|
draw(sampleWeightFunction) {
|
||
|
this.sampleWeightFunction = sampleWeightFunction;
|
||
|
this.div.empty();
|
||
|
this.div.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;"> \
|
||
|
</svg>';
|
||
|
this.div.append(svgStr);
|
||
|
this.svg = this.div.find('svg');
|
||
|
this._renderBackground();
|
||
|
this._renderSvgNodes(this.callgraph, 0, 0);
|
||
|
this._renderUnzoomNode();
|
||
|
this._renderInfoNode();
|
||
|
this._renderPercentNode();
|
||
|
// Make the added nodes in the svg visible.
|
||
|
this.div.html(this.div.html());
|
||
|
this.svg = this.div.find('svg');
|
||
|
this._adjustTextSize();
|
||
|
this._enableZoom();
|
||
|
this._enableInfo();
|
||
|
this._adjustTextSizeOnResize();
|
||
|
}
|
||
|
|
||
|
_renderBackground() {
|
||
|
this.svg.append(`<defs > <linearGradient id="background_gradient_${this.id}"
|
||
|
y1="0" y2="1" x1="0" x2="0" > \
|
||
|
<stop stop-color="#eeeeee" offset="5%" /> \
|
||
|
<stop stop-color="#efefb1" offset="90%" /> \
|
||
|
</linearGradient> \
|
||
|
</defs> \
|
||
|
<rect x="0" y="0" width="100%" height="100%" \
|
||
|
fill="url(#background_gradient_${this.id})" />`);
|
||
|
}
|
||
|
|
||
|
_getYForDepth(depth) {
|
||
|
if (this.reverseOrder) {
|
||
|
return (depth + 3) * this.svgNodeHeight;
|
||
|
}
|
||
|
return this.svgHeight - (depth + 1) * this.svgNodeHeight;
|
||
|
}
|
||
|
|
||
|
_getWidthPercentage(eventCount) {
|
||
|
return eventCount * 100.0 / this.callgraph.s;
|
||
|
}
|
||
|
|
||
|
_getHeatColor(widthPercentage) {
|
||
|
return {
|
||
|
r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)),
|
||
|
g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)),
|
||
|
b: 100,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
_renderSvgNodes(callNode, depth, xOffset) {
|
||
|
let x = xOffset;
|
||
|
let y = this._getYForDepth(depth);
|
||
|
let width = this._getWidthPercentage(callNode.s);
|
||
|
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(callNode.f);
|
||
|
let libName = getLibNameOfFunction(callNode.f);
|
||
|
let sampleWeight = this.sampleWeightFunction(callNode.s);
|
||
|
let title = funcName + ' | ' + libName + ' (' + callNode.s + ' 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;
|
||
|
for (let child of callNode.c) {
|
||
|
childXOffset = this._renderSvgNodes(child, depth + 1, childXOffset);
|
||
|
}
|
||
|
return xOffset + width;
|
||
|
}
|
||
|
|
||
|
_renderUnzoomNode() {
|
||
|
this.svg.append(`<rect id="zoom_rect_${this.id}" style="display:none;stroke:rgb(0,0,0);" \
|
||
|
rx="10" ry="10" x="10" y="10" width="80" height="30" \
|
||
|
fill="rgb(255,255,255)"/> \
|
||
|
<text id="zoom_text_${this.id}" x="19" y="30" style="display:none">Zoom out</text>`);
|
||
|
}
|
||
|
|
||
|
_renderInfoNode() {
|
||
|
this.svg.append(`<clipPath id="info_clip_path_${this.id}"> \
|
||
|
<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \
|
||
|
width="789" height="30" fill="rgb(255,255,255)"/> \
|
||
|
</clipPath> \
|
||
|
<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \
|
||
|
width="799" height="30" fill="rgb(255,255,255)"/> \
|
||
|
<text clip-path="url(#info_clip_path_${this.id})" \
|
||
|
id="info_text_${this.id}" x="128" y="30"></text>`);
|
||
|
}
|
||
|
|
||
|
_renderPercentNode() {
|
||
|
this.svg.append(`<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" \
|
||
|
x="934" y="10" width="150" height="30" \
|
||
|
fill="rgb(255,255,255)"/> \
|
||
|
<text id="percent_text_${this.id}" text-anchor="end" \
|
||
|
x="1074" y="30"></text>`);
|
||
|
}
|
||
|
|
||
|
_adjustTextSizeForNode(g) {
|
||
|
let text = g.find('text');
|
||
|
let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01;
|
||
|
if (width < 28) {
|
||
|
text.text('');
|
||
|
return;
|
||
|
}
|
||
|
let methodName = g.find('title').text().split(' | ')[0];
|
||
|
let numCharacters;
|
||
|
for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) {
|
||
|
if (numCharacters * 7.5 <= width) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (numCharacters == methodName.length) {
|
||
|
text.text(methodName);
|
||
|
} else {
|
||
|
text.text(methodName.substring(0, numCharacters - 2) + '..');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_adjustTextSize() {
|
||
|
this.svgWidth = $(window).width();
|
||
|
let thisObj = this;
|
||
|
this.svg.find('g').each(function(_, g) {
|
||
|
thisObj._adjustTextSizeForNode($(g));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_enableZoom() {
|
||
|
this.zoomStack = [this.svg.find('g').first().get(0)];
|
||
|
this.svg.find('g').css('cursor', 'pointer').click(zoom);
|
||
|
this.svg.find(`#zoom_rect_${this.id}`).css('cursor', 'pointer').click(unzoom);
|
||
|
this.svg.find(`#zoom_text_${this.id}`).css('cursor', 'pointer').click(unzoom);
|
||
|
|
||
|
let thisObj = this;
|
||
|
function zoom() {
|
||
|
thisObj.zoomStack.push(this);
|
||
|
displayFromElement(this);
|
||
|
thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'block');
|
||
|
thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'block');
|
||
|
}
|
||
|
|
||
|
function unzoom() {
|
||
|
if (thisObj.zoomStack.length > 1) {
|
||
|
thisObj.zoomStack.pop();
|
||
|
displayFromElement(thisObj.zoomStack[thisObj.zoomStack.length - 1]);
|
||
|
if (thisObj.zoomStack.length == 1) {
|
||
|
thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'none');
|
||
|
thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'none');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function displayFromElement(g) {
|
||
|
g = $(g);
|
||
|
let clickedRect = g.find('rect');
|
||
|
let clickedOriginX = parseFloat(clickedRect.attr('ox'));
|
||
|
let clickedDepth = parseInt(clickedRect.attr('depth'));
|
||
|
let clickedOriginWidth = parseFloat(clickedRect.attr('owidth'));
|
||
|
let scaleFactor = 100.0 / clickedOriginWidth;
|
||
|
thisObj.svg.find('g').each(function(_, g) {
|
||
|
g = $(g);
|
||
|
let text = g.find('text');
|
||
|
let rect = g.find('rect');
|
||
|
let depth = parseInt(rect.attr('depth'));
|
||
|
let ox = parseFloat(rect.attr('ox'));
|
||
|
let owidth = parseFloat(rect.attr('owidth'));
|
||
|
if (depth < clickedDepth || ox < clickedOriginX - 1e-9 ||
|
||
|
ox + owidth > clickedOriginX + clickedOriginWidth + 1e-9) {
|
||
|
rect.css('display', 'none');
|
||
|
text.css('display', 'none');
|
||
|
} else {
|
||
|
rect.css('display', 'block');
|
||
|
text.css('display', 'block');
|
||
|
let nx = (ox - clickedOriginX) * scaleFactor + '%';
|
||
|
let ny = thisObj._getYForDepth(depth - clickedDepth);
|
||
|
rect.attr('x', nx);
|
||
|
rect.attr('y', ny);
|
||
|
rect.attr('width', owidth * scaleFactor + '%');
|
||
|
text.attr('x', nx);
|
||
|
text.attr('y', ny + 12);
|
||
|
thisObj._adjustTextSizeForNode(g);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_enableInfo() {
|
||
|
this.selected = null;
|
||
|
let thisObj = this;
|
||
|
this.svg.find('g').on('mouseenter', function() {
|
||
|
if (thisObj.selected) {
|
||
|
thisObj.selected.css('stroke-width', '0');
|
||
|
}
|
||
|
// Mark current node.
|
||
|
let g = $(this);
|
||
|
thisObj.selected = g;
|
||
|
g.css('stroke', 'black').css('stroke-width', '0.5');
|
||
|
|
||
|
// Parse title.
|
||
|
let title = g.find('title').text();
|
||
|
let methodAndInfo = title.split(' | ');
|
||
|
thisObj.svg.find(`#info_text_${thisObj.id}`).text(methodAndInfo[0]);
|
||
|
|
||
|
// Parse percentage.
|
||
|
// '/system/lib64/libhwbinder.so (4 events: 0.28%)'
|
||
|
let regexp = /.* \(.*:\s+(.*)\)/g;
|
||
|
let match = regexp.exec(methodAndInfo[1]);
|
||
|
let percentage = '';
|
||
|
if (match && match.length > 1) {
|
||
|
percentage = match[1];
|
||
|
}
|
||
|
thisObj.svg.find(`#percent_text_${thisObj.id}`).text(percentage);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_adjustTextSizeOnResize() {
|
||
|
function throttle(callback) {
|
||
|
let running = false;
|
||
|
return function() {
|
||
|
if (!running) {
|
||
|
running = true;
|
||
|
window.requestAnimationFrame(function () {
|
||
|
callback();
|
||
|
running = false;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
$(window).resize(throttle(() => this._adjustTextSize()));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
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('s')) {
|
||
|
return null;
|
||
|
}
|
||
|
let hitLines = func.s;
|
||
|
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 < rows.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',
|
||
|
frozenColumns: 3,
|
||
|
allowHtml: true,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Return a list of disassembly related to a function.
|
||
|
function collectDisassemblyForFunction(func) {
|
||
|
if (!func.hasOwnProperty('a')) {
|
||
|
return null;
|
||
|
}
|
||
|
let hitAddrs = func.a;
|
||
|
let rawCode = getFuncDisassembly(func.g.f);
|
||
|
if (!rawCode) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Annotate disassembly with event count information.
|
||
|
let annotatedCode = [];
|
||
|
let codeForLastAddr = null;
|
||
|
let hitAddrPos = 0;
|
||
|
let hasCount = false;
|
||
|
|
||
|
function addEventCount(addr) {
|
||
|
while (hitAddrPos < hitAddrs.length && hitAddrs[hitAddrPos].a < addr) {
|
||
|
if (codeForLastAddr) {
|
||
|
codeForLastAddr.eventCount += hitAddrs[hitAddrPos].e;
|
||
|
codeForLastAddr.subtreeEventCount += hitAddrs[hitAddrPos].s;
|
||
|
hasCount = true;
|
||
|
}
|
||
|
hitAddrPos++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (let line of rawCode) {
|
||
|
let code = line[0];
|
||
|
let addr = line[1];
|
||
|
|
||
|
addEventCount(addr);
|
||
|
let item = {code: code, eventCount: 0, subtreeEventCount: 0};
|
||
|
annotatedCode.push(item);
|
||
|
// Objdump sets addr to 0 when a disassembly line is not associated with an addr.
|
||
|
if (addr != 0) {
|
||
|
codeForLastAddr = item;
|
||
|
}
|
||
|
}
|
||
|
addEventCount(Number.MAX_VALUE);
|
||
|
return hasCount ? annotatedCode : null;
|
||
|
}
|
||
|
|
||
|
// Show annotated disassembly of a function.
|
||
|
class DisassemblyView {
|
||
|
|
||
|
constructor(divContainer, disassembly) {
|
||
|
this.div = $('<div>');
|
||
|
this.div.appendTo(divContainer);
|
||
|
this.disassembly = disassembly;
|
||
|
}
|
||
|
|
||
|
draw(sampleWeightFunction) {
|
||
|
google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction));
|
||
|
}
|
||
|
|
||
|
realDraw(sampleWeightFunction) {
|
||
|
this.div.empty();
|
||
|
// Draw a table of 'Total', 'Self', 'Code'.
|
||
|
let rows = [];
|
||
|
for (let line of this.disassembly) {
|
||
|
let code = getHtml('pre', {text: line.code});
|
||
|
let totalValue = '';
|
||
|
let selfValue = '';
|
||
|
if (line.subtreeEventCount != 0) {
|
||
|
totalValue = sampleWeightFunction(line.subtreeEventCount);
|
||
|
selfValue = sampleWeightFunction(line.eventCount);
|
||
|
}
|
||
|
rows.push([totalValue, selfValue, code]);
|
||
|
}
|
||
|
let data = new google.visualization.DataTable();
|
||
|
data.addColumn('string', 'Total');
|
||
|
data.addColumn('string', 'Self');
|
||
|
data.addColumn('string', 'Code');
|
||
|
data.addRows(rows);
|
||
|
for (let i = 0; i < rows.length; ++i) {
|
||
|
for (let j = 0; j < 2; ++j) {
|
||
|
data.setProperty(i, j, 'className', 'colForCount');
|
||
|
}
|
||
|
}
|
||
|
let wrapperDiv = $('<div>');
|
||
|
wrapperDiv.appendTo(this.div);
|
||
|
let table = new google.visualization.Table(wrapperDiv.get(0));
|
||
|
table.draw(data, {
|
||
|
width: '100%',
|
||
|
sort: 'disable',
|
||
|
frozenColumns: 2,
|
||
|
allowHtml: true,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function initGlobalObjects() {
|
||
|
gTabs = new TabManager($('div#report_content'));
|
||
|
let recordData = $('#record_data').text();
|
||
|
gRecordInfo = JSON.parse(recordData);
|
||
|
gProcesses = gRecordInfo.processNames;
|
||
|
gThreads = gRecordInfo.threadNames;
|
||
|
gLibList = gRecordInfo.libList;
|
||
|
gFunctionMap = gRecordInfo.functionMap;
|
||
|
gSampleInfo = gRecordInfo.sampleInfo;
|
||
|
gSourceFiles = gRecordInfo.sourceFiles;
|
||
|
}
|
||
|
|
||
|
function createTabs() {
|
||
|
gTabs.addTab('Chart Statistics', new ChartStatTab());
|
||
|
gTabs.addTab('Sample Table', new SampleTableTab());
|
||
|
gTabs.addTab('Flamegraph', new FlameGraphTab());
|
||
|
gTabs.draw();
|
||
|
}
|
||
|
|
||
|
let gTabs;
|
||
|
let gRecordInfo;
|
||
|
let gProcesses;
|
||
|
let gThreads;
|
||
|
let gLibList;
|
||
|
let gFunctionMap;
|
||
|
let gSampleInfo;
|
||
|
let gSourceFiles;
|
||
|
|
||
|
initGlobalObjects();
|
||
|
createTabs();
|
||
|
|
||
|
});</script>
|
||
|
<div id='flamegraph_id' style='font-family: Monospace; display: none;'><style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;}
|
||
|
</style><style type="text/css"> .t:hover { cursor:pointer; } </style><img height="180" alt = "Embedded Image" src ="data:image/png;base64,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
|
||
|
<font size='8'>
|
||
|
Inferno Flamegraph Report</font><br/><br/>
|
||
|
|
||
|
Date : 2018-01-10 (Wednesday) 14:48:31<br/>
|
||
|
Threads : 1 <br/>
|
||
|
Samples : 9982</br>
|
||
|
Event count: 9982000000</br>
|
||
|
Machine : Pixel 2 (walleye) by Google<br/>Capture : /data/local/tmp/simpleperf record -e task-clock:u -g -f 1000 --duration 10 -o /data/local/tmp/perf.data --app com.example.simpleperf.simpleperfexamplewithnative<br/><br/></div><br/><br/>
|
||
|
<div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div><script>/*
|
||
|
* Copyright (C) 2017 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
'use strict';
|
||
|
|
||
|
function flamegraphInit() {
|
||
|
let flamegraph = document.getElementById('flamegraph_id');
|
||
|
let svgs = flamegraph.getElementsByTagName('svg');
|
||
|
for (let i = 0; i < svgs.length; ++i) {
|
||
|
createZoomHistoryStack(svgs[i]);
|
||
|
adjust_text_size(svgs[i]);
|
||
|
}
|
||
|
|
||
|
function throttle(callback) {
|
||
|
let running = false;
|
||
|
return function() {
|
||
|
if (!running) {
|
||
|
running = true;
|
||
|
window.requestAnimationFrame(function () {
|
||
|
callback();
|
||
|
running = false;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
window.addEventListener('resize', throttle(function() {
|
||
|
let flamegraph = document.getElementById('flamegraph_id');
|
||
|
let svgs = flamegraph.getElementsByTagName('svg');
|
||
|
for (let i = 0; i < svgs.length; ++i) {
|
||
|
adjust_text_size(svgs[i]);
|
||
|
}
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
// Create a stack add the root svg element in it.
|
||
|
function createZoomHistoryStack(svgElement) {
|
||
|
svgElement.zoomStack = [svgElement.getElementById(svgElement.attributes['rootid'].value)];
|
||
|
}
|
||
|
|
||
|
function adjust_node_text_size(x, svgWidth) {
|
||
|
let title = x.getElementsByTagName('title')[0];
|
||
|
let text = x.getElementsByTagName('text')[0];
|
||
|
let rect = x.getElementsByTagName('rect')[0];
|
||
|
|
||
|
let width = parseFloat(rect.attributes['width'].value) * svgWidth * 0.01;
|
||
|
|
||
|
// Don't even bother trying to find a best fit. The area is too small.
|
||
|
if (width < 28) {
|
||
|
text.textContent = '';
|
||
|
return;
|
||
|
}
|
||
|
// Remove dso and #samples which are here only for mouseover purposes.
|
||
|
let methodName = title.textContent.split(' | ')[0];
|
||
|
|
||
|
let numCharacters;
|
||
|
for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) {
|
||
|
// Avoid reflow by using hard-coded estimate instead of
|
||
|
// text.getSubStringLength(0, numCharacters).
|
||
|
if (numCharacters * 7.5 <= width) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (numCharacters == methodName.length) {
|
||
|
text.textContent = methodName;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
text.textContent = methodName.substring(0, numCharacters-2) + '..';
|
||
|
}
|
||
|
|
||
|
function adjust_text_size(svgElement) {
|
||
|
let svgWidth = window.innerWidth;
|
||
|
let x = svgElement.getElementsByTagName('g');
|
||
|
for (let i = 0; i < x.length; i++) {
|
||
|
adjust_node_text_size(x[i], svgWidth);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function zoom(e) {
|
||
|
let svgElement = e.ownerSVGElement;
|
||
|
let zoomStack = svgElement.zoomStack;
|
||
|
zoomStack.push(e);
|
||
|
displaySVGElement(svgElement);
|
||
|
select(e);
|
||
|
|
||
|
// Show zoom out button.
|
||
|
svgElement.getElementById('zoom_rect').style.display = 'block';
|
||
|
svgElement.getElementById('zoom_text').style.display = 'block';
|
||
|
}
|
||
|
|
||
|
function displaySVGElement(svgElement) {
|
||
|
let zoomStack = svgElement.zoomStack;
|
||
|
let e = zoomStack[zoomStack.length - 1];
|
||
|
let clicked_rect = e.getElementsByTagName('rect')[0];
|
||
|
let clicked_origin_x;
|
||
|
let clicked_origin_y = clicked_rect.attributes['oy'].value;
|
||
|
let clicked_origin_width;
|
||
|
|
||
|
if (zoomStack.length == 1) {
|
||
|
// Show all nodes when zoomStack only contains the root node.
|
||
|
// This is needed to show flamegraph containing more than one node at the root level.
|
||
|
clicked_origin_x = 0;
|
||
|
clicked_origin_width = 100;
|
||
|
} else {
|
||
|
clicked_origin_x = clicked_rect.attributes['ox'].value;
|
||
|
clicked_origin_width = clicked_rect.attributes['owidth'].value;
|
||
|
}
|
||
|
|
||
|
|
||
|
let svgBox = svgElement.getBoundingClientRect();
|
||
|
let svgBoxHeight = svgBox.height;
|
||
|
let svgBoxWidth = 100;
|
||
|
let scaleFactor = svgBoxWidth / clicked_origin_width;
|
||
|
|
||
|
let callsites = svgElement.getElementsByTagName('g');
|
||
|
for (let i = 0; i < callsites.length; i++) {
|
||
|
let text = callsites[i].getElementsByTagName('text')[0];
|
||
|
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||
|
|
||
|
let rect_o_x = parseFloat(rect.attributes['ox'].value);
|
||
|
let rect_o_y = parseFloat(rect.attributes['oy'].value);
|
||
|
|
||
|
// Avoid multiple forced reflow by hiding nodes.
|
||
|
if (rect_o_y > clicked_origin_y) {
|
||
|
rect.style.display = 'none';
|
||
|
text.style.display = 'none';
|
||
|
continue;
|
||
|
}
|
||
|
rect.style.display = 'block';
|
||
|
text.style.display = 'block';
|
||
|
|
||
|
let newrec_x = rect.attributes['x'].value = (rect_o_x - clicked_origin_x) * scaleFactor +
|
||
|
'%';
|
||
|
let newrec_y = rect.attributes['y'].value = rect_o_y + (svgBoxHeight - clicked_origin_y
|
||
|
- 17 - 2);
|
||
|
|
||
|
text.attributes['y'].value = newrec_y + 12;
|
||
|
text.attributes['x'].value = newrec_x;
|
||
|
|
||
|
rect.attributes['width'].value = (rect.attributes['owidth'].value * scaleFactor) + '%';
|
||
|
}
|
||
|
|
||
|
adjust_text_size(svgElement);
|
||
|
}
|
||
|
|
||
|
function unzoom(e) {
|
||
|
let svgOwner = e.ownerSVGElement;
|
||
|
let stack = svgOwner.zoomStack;
|
||
|
|
||
|
// Unhighlight whatever was selected.
|
||
|
if (selected) {
|
||
|
selected.classList.remove('s');
|
||
|
}
|
||
|
|
||
|
// Stack management: Never remove the last element which is the flamegraph root.
|
||
|
if (stack.length > 1) {
|
||
|
let previouslySelected = stack.pop();
|
||
|
select(previouslySelected);
|
||
|
}
|
||
|
|
||
|
// Hide zoom out button.
|
||
|
if (stack.length == 1) {
|
||
|
svgOwner.getElementById('zoom_rect').style.display = 'none';
|
||
|
svgOwner.getElementById('zoom_text').style.display = 'none';
|
||
|
}
|
||
|
|
||
|
displaySVGElement(svgOwner);
|
||
|
}
|
||
|
|
||
|
function search(e) {
|
||
|
let term = prompt('Search for:', '');
|
||
|
let callsites = e.ownerSVGElement.getElementsByTagName('g');
|
||
|
|
||
|
if (!term) {
|
||
|
for (let i = 0; i < callsites.length; i++) {
|
||
|
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||
|
rect.attributes['fill'].value = rect.attributes['ofill'].value;
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < callsites.length; i++) {
|
||
|
let title = callsites[i].getElementsByTagName('title')[0];
|
||
|
let rect = callsites[i].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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let selected;
|
||
|
document.onkeydown = function handle_keyboard_input(e) {
|
||
|
if (!selected) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let nav = selected.attributes['nav'].value.split(',');
|
||
|
let navigation_index;
|
||
|
switch (e.keyCode) {
|
||
|
// case 38: // ARROW UP
|
||
|
case 87: navigation_index = 0; break; // W
|
||
|
|
||
|
// case 32 : // ARROW LEFT
|
||
|
case 65: navigation_index = 1; break; // A
|
||
|
|
||
|
// case 43: // ARROW DOWN
|
||
|
case 68: navigation_index = 3; break; // S
|
||
|
|
||
|
// case 39: // ARROW RIGHT
|
||
|
case 83: navigation_index = 2; break; // D
|
||
|
|
||
|
case 32: zoom(selected); return false; // SPACE
|
||
|
|
||
|
case 8: // BACKSPACE
|
||
|
unzoom(selected); return false;
|
||
|
default: return true;
|
||
|
}
|
||
|
|
||
|
if (nav[navigation_index] == '0') {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
let target_element = selected.ownerSVGElement.getElementById(nav[navigation_index]);
|
||
|
select(target_element);
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
function select(e) {
|
||
|
if (selected) {
|
||
|
selected.classList.remove('s');
|
||
|
}
|
||
|
selected = e;
|
||
|
selected.classList.add('s');
|
||
|
|
||
|
// Update info bar
|
||
|
let titleElement = selected.getElementsByTagName('title')[0];
|
||
|
let text = titleElement.textContent;
|
||
|
|
||
|
// Parse title
|
||
|
let method_and_info = text.split(' | ');
|
||
|
let methodName = method_and_info[0];
|
||
|
let info = method_and_info[1];
|
||
|
|
||
|
// Parse info
|
||
|
// '/system/lib64/libhwbinder.so (4 events: 0.28%)'
|
||
|
let regexp = /(.*) \((.*)\)/g;
|
||
|
let match = regexp.exec(info);
|
||
|
if (match.length > 2) {
|
||
|
let percentage = match[2];
|
||
|
// Write percentage
|
||
|
let percentageTextElement = selected.ownerSVGElement.getElementById('percent_text');
|
||
|
percentageTextElement.textContent = percentage;
|
||
|
// console.log("'" + percentage + "'")
|
||
|
}
|
||
|
|
||
|
// Set fields
|
||
|
let barTextElement = selected.ownerSVGElement.getElementById('info_text');
|
||
|
barTextElement.textContent = methodName;
|
||
|
}</script><br/><br/><b>Thread 17426 (amplewithnative) (9982 samples):</b><br/>
|
||
|
|
||
|
|
||
|
|
||
|
<div class="flamegraph_block" style="width:100%; height:153px;">
|
||
|
<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;"
|
||
|
rootid="1">
|
||
|
<defs > <linearGradient id="background_gradiant" y1="0" y2="1" x1="0" x2="0" >
|
||
|
<stop stop-color="#eeeeee" offset="5%" /> <stop stop-color="#efefb1" offset="90%" />
|
||
|
</linearGradient> </defs><rect x="0.0" y="0" width="100%" height="100%" fill="url(#background_gradiant)" />
|
||
|
<g id=1 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="2,0,0,0">
|
||
|
<title>__start_thread | /system/lib64/libc.so (9982000000 events: 100.00%)</title>
|
||
|
<rect x="0.000000%" y="136.000000" ox="0.000000" oy="136.000000" width="100.000000%" owidth="100.000000" height="15.0"
|
||
|
ofill="rgb(245,110,100)" fill="rgb(245,110,100)" style="stroke:rgb(195,60,50)"/>
|
||
|
<text x="0.000000%" y="148.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=2 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="3,0,1,0">
|
||
|
<title>__pthread_start(void*) | /system/lib64/libc.so (9982000000 events: 100.00%)</title>
|
||
|
<rect x="0.000000%" y="119.000000" ox="0.000000" oy="119.000000" width="100.000000%" owidth="100.000000" height="15.0"
|
||
|
ofill="rgb(245,110,100)" fill="rgb(245,110,100)" style="stroke:rgb(195,60,50)"/>
|
||
|
<text x="0.000000%" y="131.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=8 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,0,2,3">
|
||
|
<title>atoi | /system/lib64/libc.so (170000000 events: 1.70%)</title>
|
||
|
<rect x="0.000000%" y="102.000000" ox="0.000000" oy="102.000000" width="1.703066%" owidth="1.703066" height="15.0"
|
||
|
ofill="rgb(254,213,100)" fill="rgb(254,213,100)" style="stroke:rgb(204,163,50)"/>
|
||
|
<text x="0.000000%" y="114.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=3 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="4,8,2,0">
|
||
|
<title>BusyLoopThread(void*) | /data/app/com.example.simpleperf.simpleperfexamplewithnative-U6p3nGhRH8eOOZdL37VPqQ==/lib/arm64/libnative-lib.so (9812000000 events: 98.30%)</title>
|
||
|
<rect x="1.703066%" y="102.000000" ox="1.703066" oy="102.000000" width="98.296934%" owidth="98.296934" height="15.0"
|
||
|
ofill="rgb(245,111,100)" fill="rgb(245,111,100)" style="stroke:rgb(195,61,50)"/>
|
||
|
<text x="1.703066%" y="114.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=7 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,0,3,4">
|
||
|
<title>strtol | /system/lib64/libc.so (564000000 events: 5.65%)</title>
|
||
|
<rect x="1.703066%" y="85.000000" ox="1.703066" oy="85.000000" width="5.650170%" owidth="5.650170" height="15.0"
|
||
|
ofill="rgb(254,209,100)" fill="rgb(254,209,100)" style="stroke:rgb(204,159,50)"/>
|
||
|
<text x="1.703066%" y="97.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=4 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="5,7,3,9">
|
||
|
<title>atoi | /system/lib64/libc.so (8221000000 events: 82.36%)</title>
|
||
|
<rect x="7.353236%" y="85.000000" ox="7.353236" oy="85.000000" width="82.358245%" owidth="82.358245" height="15.0"
|
||
|
ofill="rgb(246,128,100)" fill="rgb(246,128,100)" style="stroke:rgb(196,78,50)"/>
|
||
|
<text x="7.353236%" y="97.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=5 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="10,0,4,11">
|
||
|
<title>strtol | /system/lib64/libc.so (7499000000 events: 75.13%)</title>
|
||
|
<rect x="7.353236%" y="68.000000" ox="7.353236" oy="68.000000" width="75.125225%" owidth="75.125225" height="15.0"
|
||
|
ofill="rgb(247,136,100)" fill="rgb(247,136,100)" style="stroke:rgb(197,86,50)"/>
|
||
|
<text x="7.353236%" y="80.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=6 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,0,5,10">
|
||
|
<title>isspace | /system/lib64/libc.so (680000000 events: 6.81%)</title>
|
||
|
<rect x="7.353236%" y="51.000000" ox="7.353236" oy="51.000000" width="6.812262%" owidth="6.812262" height="15.0"
|
||
|
ofill="rgb(254,207,100)" fill="rgb(254,207,100)" style="stroke:rgb(204,157,50)"/>
|
||
|
<text x="7.353236%" y="63.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=10 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,6,5,12">
|
||
|
<title>isalpha | /system/lib64/libc.so (810000000 events: 8.11%)</title>
|
||
|
<rect x="14.165498%" y="51.000000" ox="14.165498" oy="51.000000" width="8.114606%" owidth="8.114606" height="15.0"
|
||
|
ofill="rgb(254,206,100)" fill="rgb(254,206,100)" style="stroke:rgb(204,156,50)"/>
|
||
|
<text x="14.165498%" y="63.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=12 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,10,5,0">
|
||
|
<title>@plt | /system/lib64/libc.so (561000000 events: 5.62%)</title>
|
||
|
<rect x="22.280104%" y="51.000000" ox="22.280104" oy="51.000000" width="5.620116%" owidth="5.620116" height="15.0"
|
||
|
ofill="rgb(254,209,100)" fill="rgb(254,209,100)" style="stroke:rgb(204,159,50)"/>
|
||
|
<text x="22.280104%" y="63.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=11 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,5,4,0">
|
||
|
<title>@plt | /system/lib64/libc.so (286000000 events: 2.87%)</title>
|
||
|
<rect x="82.478461%" y="68.000000" ox="82.478461" oy="68.000000" width="2.865157%" owidth="2.865157" height="15.0"
|
||
|
ofill="rgb(254,211,100)" fill="rgb(254,211,100)" style="stroke:rgb(204,161,50)"/>
|
||
|
<text x="82.478461%" y="80.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><g id=9 class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="0,4,3,0">
|
||
|
<title>@plt | /data/app/com.example.simpleperf.simpleperfexamplewithnative-U6p3nGhRH8eOOZdL37VPqQ==/lib/arm64/libnative-lib.so (295000000 events: 2.96%)</title>
|
||
|
<rect x="89.711481%" y="85.000000" ox="89.711481" oy="85.000000" width="2.955320%" owidth="2.955320" height="15.0"
|
||
|
ofill="rgb(254,211,100)" fill="rgb(254,211,100)" style="stroke:rgb(204,161,50)"/>
|
||
|
<text x="89.711481%" y="97.000000" font-size="12" font-family="Monospace"></text>
|
||
|
</g><rect id="search_rect" style="stroke:rgb(0,0,0);" onclick="search(this);" class="t"
|
||
|
rx="10" ry="10" x="1190" y="10" width="80" height="30" fill="rgb(255,255,255)""/>
|
||
|
<text id="search_text" class="t" x="1200" y="30" onclick="search(this);">Search</text>
|
||
|
<rect id="zoom_rect" style="display:none;stroke:rgb(0,0,0);" class="t"
|
||
|
onclick="unzoom(this);" rx="10" ry="10" x="10" y="10" width="80" height="30"
|
||
|
fill="rgb(255,255,255)"/>
|
||
|
<text id="zoom_text" style="display:none;" class="t" x="20" y="30"
|
||
|
onclick="unzoom(this);">Zoom out</text>
|
||
|
<clipPath id="info_clip_path"> <rect id="info_rect" style="stroke:rgb(0,0,0);"
|
||
|
rx="10" ry="10" x="120" y="10" width="800" height="30" fill="rgb(255,255,255)"/>
|
||
|
</clipPath>
|
||
|
<rect id="info_rect" style="stroke:rgb(0,0,0);"
|
||
|
rx="10" ry="10" x="120" y="10" width="800" height="30" fill="rgb(255,255,255)"/>
|
||
|
<text clip-path="url(#info_clip_path)" id="info_text" x="130" y="30"></text>
|
||
|
<rect id="percent_rect" style="stroke:rgb(0,0,0);"
|
||
|
rx="10" ry="10" x="930" y="10" width="250" height="30" fill="rgb(255,255,255)"/>
|
||
|
<text id="percent_text" text-anchor="end" x="1170" y="30">100.00%</text>
|
||
|
</svg></div><br/>
|
||
|
|
||
|
</div></body>
|
||
|
</html>
|