/* * 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. (function () { function getTimeInMs() { return new Date().getTime(); } class TimeLog { constructor() { this.start = getTimeInMs(); } log(name) { let end = getTimeInMs(); console.log(name, end - this.start, 'ms'); this.start = end; } } class ProgressBar { constructor() { let str = ` `; this.modal = $(str).appendTo($('body')); this.progress = 0; this.shownCallback = null; this.modal.on('shown.bs.modal', () => this._onShown()); // Shorten progress bar update time. this.modal.find('.progress-bar').css('transition-duration', '0ms'); this.shown = false; } // progress is [0-100]. Return a Promise resolved when the update is shown. updateAsync(text, progress) { progress = parseInt(progress); // Truncate float number to integer. return this.showAsync().then(() => { if (text) { this.modal.find('.modal-title').text(text); } this.progress = progress; this.modal.find('.progress-bar').css('width', progress + '%') .attr('aria-valuenow', progress).text(progress + '%'); // Leave 100ms for the progess bar to update. return createPromise((resolve) => setTimeout(resolve, 100)); }); } showAsync() { if (this.shown) { return createPromise(); } return createPromise((resolve) => { this.shownCallback = resolve; this.modal.modal({ show: true, keyboard: false, backdrop: false, }); }); } _onShown() { this.shown = true; if (this.shownCallback) { let callback = this.shownCallback; this.shownCallback = null; callback(); } } hide() { this.shown = false; this.modal.modal('hide'); } } function openHtml(name, attrs={}) { let s = `<${name} `; for (let key in attrs) { s += `${key}="${attrs[key]}" `; } s += '>'; return s; } function closeHtml(name) { return ``; } 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}`; } s += ''; return s; } 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'); } let createId = function() { let currentId = 0; return () => `id${++currentId}`; }(); class TabManager { constructor(divContainer) { let id = createId(); divContainer.append(`
`); this.ul = divContainer.find(`#${id}`); this.content = divContainer.find(`#${id}Content`); // Map from title to [tabObj, drawn=false|true]. this.tabs = new Map(); this.tabActiveCallback = null; } addTab(title, tabObj) { let id = createId(); this.content.append(`
`); this.ul.append(` `); tabObj.init(this.content.find(`#${id}`)); this.tabs.set(title, [tabObj, false]); this.ul.find(`#${id}-tab`).on('shown.bs.tab', () => this.onTabActive(title)); return tabObj; } setActiveAsync(title) { let tabObj = this.findTab(title); return createPromise((resolve) => { this.tabActiveCallback = resolve; let id = tabObj.div.attr('id') + '-tab'; this.ul.find(`#${id}`).tab('show'); }); } onTabActive(title) { let array = this.tabs.get(title); let tabObj = array[0]; let drawn = array[1]; if (!drawn) { tabObj.draw(); array[1] = true; } if (this.tabActiveCallback) { let callback = this.tabActiveCallback; this.tabActiveCallback = null; callback(); } } findTab(title) { let array = this.tabs.get(title); return array ? array[0] : null; } } function createEventTabs(id) { let ul = `'; content += ''; return ul + content; } function createViewsForEvents(div, createViewCallback) { let views = []; if (gSampleInfo.length == 1) { views.push(createViewCallback(div, gSampleInfo[0])); } else if (gSampleInfo.length > 1) { // If more than one event, draw them in tabs. let id = createId(); div.append(createEventTabs(id)); for (let i = 0; i < gSampleInfo.length; ++i) { let subId = id + '_' + i; views.push(createViewCallback(div.find(`#${subId}`), gSampleInfo[i])); } div.find(`#${id}_0-tab`).tab('show'); } return views; } // Return a promise to draw views. function drawViewsAsync(views, totalProgress, drawViewCallback) { if (views.length == 0) { return createPromise(); } let drawPos = 0; let eachProgress = totalProgress / views.length; function drawAsync() { if (drawPos == views.length) { return createPromise(); } return drawViewCallback(views[drawPos++], eachProgress).then(drawAsync); } return drawAsync(); } // 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 = $('
'); 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.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 = $('
'); 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) { $('').appendTo(this.div) .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.f), func.c[1], 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 = $('
'); 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 { init(div) { this.div = div; } draw() { new RecordFileView(this.div).draw(); let views = createViewsForEvents(this.div, (div, eventInfo) => { return new ChartView(div, eventInfo); }); for (let view of views) { view.draw(); } } } class SampleTableTab { init(div) { this.div = div; } draw() { let views = []; createPromise() .then(updateProgress('Draw SampleTable...', 0)) .then(wait(() => { this.div.empty(); views = createViewsForEvents(this.div, (div, eventInfo) => { return new SampleTableView(div, eventInfo); }); })) .then(() => drawViewsAsync(views, 100, (view, progress) => view.drawAsync(progress))) .then(hideProgress()); } } // 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, eventInfo, onSelectChange) { let options = new Map(); options.set('percent', 'Show percentage of event count'); options.set('event_count', 'Show event count'); if (isClockEvent(eventInfo)) { options.set('event_count_in_ms', 'Show event count in milliseconds'); } let buttons = []; options.forEach((value, key) => { buttons.push(``); }); this.curOption = 'percent'; this.eventCount = eventInfo.eventCount; let id = createId(); let str = ` `; divContainer.append(str); divContainer.children().last().on('hidden.bs.dropdown', (e) => { if (e.clickEvent) { let button = $(e.clickEvent.target); let newOption = button.attr('key'); if (newOption && this.curOption != newOption) { this.curOption = newOption; divContainer.find(`#${id}`).text(options.get(this.curOption)); onSelectChange(); } } }); } getSampleWeightFunction() { if (this.curOption == 'percent') { return (eventCount) => (eventCount * 100.0 / this.eventCount).toFixed(2) + '%'; } if (this.curOption == 'event_count') { return (eventCount) => '' + eventCount; } if (this.curOption == 'event_count_in_ms') { return (eventCount) => (eventCount / 1000000.0).toFixed(3); } } getSampleWeightSuffix() { if (this.curOption == 'event_count_in_ms') { return ' ms'; } return ''; } } class SampleTableView { constructor(divContainer, eventInfo) { this.id = createId(); this.div = $('
', {id: this.id}).appendTo(divContainer); this.eventInfo = eventInfo; this.selectorView = null; this.tableDiv = null; } drawAsync(totalProgress) { return createPromise() .then(wait(() => { this.div.empty(); this.selectorView = new SampleTableWeightSelectorView( this.div, this.eventInfo, () => this.onSampleWeightChange()); this.tableDiv = $('
').appendTo(this.div); })) .then(() => this._drawSampleTable(totalProgress)); } // Return a promise to draw SampleTable. _drawSampleTable(totalProgress) { let eventInfo = this.eventInfo; let data = []; return createPromise() .then(wait(() => { this.tableDiv.empty(); let getSampleWeight = this.selectorView.getSampleWeightFunction(); let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix(); // Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library', // 'Function'. let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : ''; let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples', 'Process', 'Thread', 'Library', 'Function', 'HideKey']; this.tableDiv.append(` ${getTableRow(titles, 'th')}${getTableRow(titles, 'th')}
`); for (let [i, process] of eventInfo.processes.entries()) { let processName = getProcessName(process.pid); for (let [j, thread] of process.threads.entries()) { let threadName = getThreadName(thread.tid); for (let [k, lib] of thread.libs.entries()) { let libName = getLibName(lib.libId); for (let [t, func] of lib.functions.entries()) { let totalValue = getSampleWeight(func.c[2]); let selfValue = getSampleWeight(func.c[1]); let key = [i, j, k, t].join('_'); data.push([totalValue, selfValue, func.c[0], processName, threadName, libName, getFuncName(func.f), key]) } } } } })) .then(addProgress(totalProgress / 2)) .then(wait(() => { let table = this.tableDiv.find('table'); let dataTable = table.DataTable({ lengthMenu: [10, 20, 50, 100, -1], order: [0, 'desc'], data: data, responsive: true, }); dataTable.column(7).visible(false); table.find('tr').css('cursor', 'pointer'); table.on('click', 'tr', function() { let data = dataTable.row(this).data(); if (!data) { // A row in header or footer. return; } let key = data[7]; 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); }); })); } onSampleWeightChange() { createPromise() .then(updateProgress('Draw SampleTable...', 0)) .then(() => this._drawSampleTable(100)) .then(hideProgress()); } } // Show embedded flamegraph generated by inferno. class FlameGraphTab { init(div) { this.div = div; } draw() { let views = []; createPromise() .then(updateProgress('Draw Flamegraph...', 0)) .then(wait(() => { this.div.empty(); views = createViewsForEvents(this.div, (div, eventInfo) => { return new FlameGraphViewList(div, eventInfo); }); })) .then(() => drawViewsAsync(views, 100, (view, progress) => view.drawAsync(progress))) .then(hideProgress()); } } // Show FlameGraphs for samples in an event type, used in FlameGraphTab. // 1. Draw 10 FlameGraphs at one time, and use a "More" button to show more FlameGraphs. // 2. First draw background of Flamegraphs, then draw details in idle time. class FlameGraphViewList { constructor(div, eventInfo) { this.div = div; this.eventInfo = eventInfo; this.selectorView = null; this.flamegraphDiv = null; this.flamegraphs = []; this.moreButton = null; } drawAsync(totalProgress) { this.div.empty(); this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo, () => this.onSampleWeightChange()); this.flamegraphDiv = $('
').appendTo(this.div); return this._drawMoreFlameGraphs(10, totalProgress); } // Return a promise to draw flamegraphs. _drawMoreFlameGraphs(moreCount, progress) { let initProgress = progress / (1 + moreCount); let newFlamegraphs = []; return createPromise() .then(wait(() => { if (this.moreButton) { this.moreButton.hide(); } let pId = 0; let tId = 0; let newCount = this.flamegraphs.length + moreCount; for (let i = 0; i < newCount; ++i) { if (pId == this.eventInfo.processes.length) { break; } let process = this.eventInfo.processes[pId]; let thread = process.threads[tId]; if (i >= this.flamegraphs.length) { let title = `Process ${getProcessName(process.pid)} ` + `Thread ${getThreadName(thread.tid)} ` + `(Samples: ${thread.sampleCount})`; let totalCount = {countForProcess: process.eventCount, countForThread: thread.eventCount}; let flamegraph = new FlameGraphView(this.flamegraphDiv, title, totalCount, thread.g.c, false); flamegraph.draw(); newFlamegraphs.push(flamegraph); } tId++; if (tId == process.threads.length) { pId++; tId = 0; } } if (pId < this.eventInfo.processes.length) { // Show "More" Button. if (!this.moreButton) { this.div.append(`
`); this.moreButton = this.div.children().last().find('button'); this.moreButton.click(() => { createPromise().then(updateProgress('Draw FlameGraph...', 0)) .then(() => this._drawMoreFlameGraphs(10, 100)) .then(hideProgress()); }); this.moreButton.hide(); } } else if (this.moreButton) { this.moreButton.remove(); this.moreButton = null; } for (let flamegraph of newFlamegraphs) { this.flamegraphs.push(flamegraph); } })) .then(addProgress(initProgress)) .then(() => this.drawDetails(newFlamegraphs, progress - initProgress)); } drawDetails(flamegraphs, totalProgress) { return createPromise() .then(() => drawViewsAsync(flamegraphs, totalProgress, (view, progress) => { return createPromise() .then(wait(() => view.drawDetails(this.selectorView.getSampleWeightFunction()))) .then(addProgress(progress)); })) .then(wait(() => { if (this.moreButton) { this.moreButton.show(); } })); } onSampleWeightChange() { createPromise().then(updateProgress('Draw FlameGraph...', 0)) .then(() => this.drawDetails(this.flamegraphs, 100)) .then(hideProgress()); } } // 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()); } gTabs.setActiveAsync(title) .then(() => 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.views = []; this.redraw(); } redraw() { if (!this.func) { return; } createPromise() .then(updateProgress("Draw Function...", 0)) .then(wait(() => { this.div.empty(); this._drawTitle(); this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo, () => this.onSampleWeightChange()); 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; } let totalCount = {countForProcess: this.processInfo.eventCount, countForThread: this.threadInfo.eventCount}; let callgraphView = new FlameGraphView( this.div, `Functions called by ${funcName}`, totalCount, getNodesMatchingFuncId(this.threadInfo.g), false); callgraphView.draw(); this.views.push(callgraphView); let reverseCallgraphView = new FlameGraphView( this.div, `Functions calling ${funcName}`, totalCount, getNodesMatchingFuncId(this.threadInfo.rg), true); reverseCallgraphView.draw(); this.views.push(reverseCallgraphView); let sourceFiles = collectSourceFilesForFunction(this.func); if (sourceFiles) { this.div.append(getHtml('hr')); this.div.append(getHtml('b', {text: 'SourceCode:'}) + '
'); this.views.push(new SourceCodeView(this.div, sourceFiles, totalCount)); } let disassembly = collectDisassemblyForFunction(this.func); if (disassembly) { this.div.append(getHtml('hr')); this.div.append(getHtml('b', {text: 'Disassembly:'}) + '
'); this.views.push(new DisassemblyView(this.div, disassembly, totalCount)); } })) .then(addProgress(25)) .then(() => this.drawDetails(75)) .then(hideProgress()); } draw() {} _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.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 = $('
'); 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() { createPromise() .then(updateProgress("Draw Function...", 0)) .then(() => this.drawDetails(100)) .then(hideProgress()); } drawDetails(totalProgress) { let sampleWeightFunction = this.selectorView.getSampleWeightFunction(); return drawViewsAsync(this.views, totalProgress, (view, progress) => { return createPromise() .then(wait(() => view.drawDetails(sampleWeightFunction))) .then(addProgress(progress)); }); } } // 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 SampleWeightSelectorView { constructor(divContainer, eventInfo, onSelectChange) { let options = new Map(); options.set('percent_to_all', 'Show percentage of event count relative to all processes'); options.set('percent_to_process', 'Show percentage of event count relative to the current process'); options.set('percent_to_thread', 'Show percentage of event count relative to the current thread'); options.set('event_count', 'Show event count'); if (isClockEvent(eventInfo)) { options.set('event_count_in_ms', 'Show event count in milliseconds'); } let buttons = []; options.forEach((value, key) => { buttons.push(``); }); this.curOption = 'percent_to_all'; let id = createId(); let str = ` `; divContainer.append(str); divContainer.children().last().on('hidden.bs.dropdown', (e) => { if (e.clickEvent) { let button = $(e.clickEvent.target); let newOption = button.attr('key'); if (newOption && this.curOption != newOption) { this.curOption = newOption; divContainer.find(`#${id}`).text(options.get(this.curOption)); onSelectChange(); } } }); this.countForAllProcesses = eventInfo.eventCount; } getSampleWeightFunction() { if (this.curOption == 'percent_to_all') { let countForAllProcesses = this.countForAllProcesses; return function(eventCount, _) { let percent = eventCount * 100.0 / countForAllProcesses; return percent.toFixed(2) + '%'; }; } if (this.curOption == 'percent_to_process') { return function(eventCount, totalCount) { let percent = eventCount * 100.0 / totalCount.countForProcess; return percent.toFixed(2) + '%'; }; } if (this.curOption == 'percent_to_thread') { return function(eventCount, totalCount) { let percent = eventCount * 100.0 / totalCount.countForThread; return percent.toFixed(2) + '%'; }; } if (this.curOption == 'event_count') { return function(eventCount, _) { return '' + eventCount; }; } if (this.curOption == 'event_count_in_ms') { return function(eventCount, _) { let timeInMs = eventCount / 1000000.0; return timeInMs.toFixed(3) + ' ms'; }; } } } // Given a callgraph, show the flamegraph. class FlameGraphView { constructor(divContainer, title, totalCount, initNodes, reverseOrder) { this.id = createId(); this.div = $('
', {id: this.id, style: 'font-family: Monospace; font-size: 12px'}); this.div.appendTo(divContainer); this.title = title; this.totalCount = totalCount; this.reverseOrder = reverseOrder; this.sampleWeightFunction = null; this.svgNodeHeight = 17; this.initNodes = initNodes; this.sumCount = 0; for (let node of initNodes) { this.sumCount += node.s; } this.maxDepth = this._getMaxDepth(this.initNodes); this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3); this.svgStr = null; this.svgDiv = null; this.svg = null; } _getMaxDepth(nodes) { let isArray = Array.isArray(nodes); let sumCount; if (isArray) { sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0); } else { sumCount = nodes.s; } let width = this._getWidthPercentage(sumCount); if (width < 0.1) { return 0; } let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c; let childDepth = 0; for (let child of children) { childDepth = Math.max(childDepth, this._getMaxDepth(child)); } return childDepth + 1; } draw() { // Only draw skeleton. this.div.empty(); this.div.append(`

${this.title}

`); this.svgStr = []; this._renderBackground(); this.svgStr.push('
'); this.div.append(this.svgStr.join('')); this.svgDiv = this.div.children().last(); this.div.append('

'); } drawDetails(sampleWeightFunction) { this.sampleWeightFunction = sampleWeightFunction; this.svgStr = []; this._renderBackground(); this._renderSvgNodes(); this._renderUnzoomNode(); this._renderInfoNode(); this._renderPercentNode(); this._renderSearchNode(); // It is much faster to add html content to svgStr than adding it directly to svgDiv. this.svgDiv.html(this.svgStr.join('')); this.svgStr = []; this.svg = this.svgDiv.find('svg'); this._adjustTextSize(); this._enableZoom(); this._enableInfo(); this._enableSearch(); this._adjustTextSizeOnResize(); } _renderBackground() { this.svgStr.push(`
`); } _getYForDepth(depth) { if (this.reverseOrder) { return (depth + 3) * this.svgNodeHeight; } return this.svgHeight - (depth + 1) * this.svgNodeHeight; } _getWidthPercentage(eventCount) { return eventCount * 100.0 / this.sumCount; } _getHeatColor(widthPercentage) { return { r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)), g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)), b: 100, }; } _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); } } // 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) { let subNodes = map.get(child.f); if (subNodes) { subNodes.push(child); } else { map.set(child.f, [child]); } } } let res = []; for (let subNodes of map.values()) { 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, this.totalCount); let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' + sampleWeight + ')'; this.svgStr.push(`${title} `); 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; } _renderUnzoomNode() { this.svgStr.push(` `); } _renderInfoNode() { this.svgStr.push(` `); } _renderPercentNode() { this.svgStr.push(` `); } _renderSearchNode() { this.svgStr.push(` Search`); } _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 = [null]; 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) { let clickedOriginX = 0; let clickedDepth = 0; let clickedOriginWidth = 100; let scaleFactor = 1; if (g) { g = $(g); let clickedRect = g.find('rect'); clickedOriginX = parseFloat(clickedRect.attr('ox')); clickedDepth = parseInt(clickedRect.attr('depth')); clickedOriginWidth = parseFloat(clickedRect.attr('owidth')); 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); }); } _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; 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.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, totalCount) { this.div = $('
'); this.div.appendTo(divContainer); this.sourceFiles = sourceFiles; this.totalCount = totalCount; } drawDetails(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, this.totalCount); selfValue = sampleWeightFunction(countInfo.eventCount, this.totalCount); } 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 = $('
'); 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.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, totalCount) { this.div = $('
'); this.div.appendTo(divContainer); this.disassembly = disassembly; this.totalCount = totalCount; } drawDetails(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, this.totalCount); selfValue = sampleWeightFunction(line.eventCount, this.totalCount); } 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 = $('
'); 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() { 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 = new TabManager($('div#report_content')); gTabs.addTab('Chart Statistics', new ChartStatTab()); gTabs.addTab('Sample Table', new SampleTableTab()); gTabs.addTab('Flamegraph', new FlameGraphTab()); } // Global draw objects let gTabs; let gProgressBar = new ProgressBar(); // Gobal Json Data let gRecordInfo; let gProcesses; let gThreads; let gLibList; let gFunctionMap; let gSampleInfo; let gSourceFiles; function updateProgress(text, progress) { return () => gProgressBar.updateAsync(text, progress); } function addProgress(progress) { return () => gProgressBar.updateAsync(null, gProgressBar.progress + progress); } function hideProgress() { return () => gProgressBar.hide(); } function createPromise(callback) { if (callback) { return new Promise((resolve, _) => callback(resolve)); } return new Promise((resolve,_) => resolve()); } function waitDocumentReady() { return createPromise((resolve) => $(document).ready(resolve)); } function wait(functionCall) { return () => { functionCall(); return createPromise(); }; } createPromise() .then(updateProgress('Load page...', 0)) .then(waitDocumentReady) .then(updateProgress('Parse Json data...', 20)) .then(wait(initGlobalObjects)) .then(updateProgress('Create tabs...', 30)) .then(wait(createTabs)) .then(updateProgress('Draw ChartStat...', 40)) .then(() => gTabs.setActiveAsync('Chart Statistics')) .then(updateProgress(null, 100)) .then(hideProgress()); })();