| /* |
| * 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 = ` |
| <div class="modal" tabindex="-1" role="dialog"> |
| <div class="modal-dialog" role="document"> |
| <div class="modal-content"> |
| <div class="modal-header"><h5 class="modal-title">Loading page...</h5></div> |
| <div class="modal-body"> |
| <div class="progress"> |
| <div class="progress-bar" role="progressbar" |
| style="width: 0%" aria-valuenow="0" aria-valuemin="0" |
| aria-valuemax="100">0%</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| `; |
| 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 `</${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 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(`<ul class="nav nav-pills mb-3 mt-3 ml-3" id="${id}" role="tablist"> |
| </ul><hr/><div class="tab-content" id="${id}Content"></div>`); |
| 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(`<div class="tab-pane" id="${id}" role="tabpanel" |
| aria-labelledby="${id}-tab"></div>`); |
| this.ul.append(` |
| <li class="nav-item"> |
| <a class="nav-link" id="${id}-tab" data-toggle="pill" href="#${id}" role="tab" |
| aria-controls="${id}" aria-selected="false">${title}</a> |
| </li>`); |
| 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 = `<ul class="nav nav-pills mb-3 mt-3 ml-3" id="${id}" role="tablist">`; |
| let content = `<div class="tab-content" id="${id}Content">`; |
| for (let i = 0; i < gSampleInfo.length; ++i) { |
| let subId = id + '_' + i; |
| let title = gSampleInfo[i].eventName; |
| ul += ` |
| <li class="nav-item"> |
| <a class="nav-link" id="${subId}-tab" data-toggle="pill" href="#${subId}" role="tab" |
| aria-controls="${subId}" aria-selected="${i == 0 ? "true" : "false"}">${title}</a> |
| </li>`; |
| content += ` |
| <div class="tab-pane" id="${subId}" role="tabpanel" aria-labelledby="${subId}-tab"> |
| </div>`; |
| } |
| ul += '</ul>'; |
| content += '</div>'; |
| 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 = $('<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.androidBuildFingerprint) { |
| rows.push(['Build Fingerprint', gRecordInfo.androidBuildFingerprint]); |
| } |
| if (gRecordInfo.kernelVersion) { |
| rows.push(['Kernel Version', gRecordInfo.kernelVersion]); |
| } |
| 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 = $('<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) { |
| $('<button type="button" class="btn btn-primary">Back</button>').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 = $('<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 { |
| 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(`<button type="button" class="dropdown-item" key="${key}">${value} |
| </button>`); |
| }); |
| this.curOption = 'percent'; |
| this.eventCount = eventInfo.eventCount; |
| let id = createId(); |
| let str = ` |
| <div class="dropdown"> |
| <button type="button" class="btn btn-primary dropdown-toggle" id="${id}" |
| data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" |
| >${options.get(this.curOption)}</button> |
| <div class="dropdown-menu" aria-labelledby="${id}">${buttons.join('')}</div> |
| </div> |
| `; |
| 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 = $('<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 = $('<div>').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(` |
| <table cellspacing="0" class="table table-striped table-bordered" |
| style="width:100%"> |
| <thead>${getTableRow(titles, 'th')}</thead> |
| <tbody></tbody> |
| <tfoot>${getTableRow(titles, 'th')}</tfoot> |
| </table>`); |
| 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], |
| pageLength: 100, |
| order: [[0, 'desc'], [1, 'desc'], [2, 'desc']], |
| data: data, |
| responsive: true, |
| columnDefs: [ |
| { orderSequence: [ 'desc' ], targets: [0, 1, 2] }, |
| ], |
| }); |
| 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 = $('<div>').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(` |
| <div style="text-align:center"> |
| <button type="button" class="btn btn-primary">More</button> |
| </div>`); |
| 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:'}) + '<br/>'); |
| 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:'}) + '<br/>'); |
| 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 = $('<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() { |
| 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(`<button type="button" class="dropdown-item" key="${key}">${value} |
| </button>`); |
| }); |
| this.curOption = 'percent_to_all'; |
| let id = createId(); |
| let str = ` |
| <div class="dropdown"> |
| <button type="button" class="btn btn-primary dropdown-toggle" id="${id}" |
| data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" |
| >${options.get(this.curOption)}</button> |
| <div class="dropdown-menu" aria-labelledby="${id}">${buttons.join('')}</div> |
| </div> |
| `; |
| 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 = $('<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(`<p><b>${this.title}</b></p>`); |
| this.svgStr = []; |
| this._renderBackground(); |
| this.svgStr.push('</svg></div>'); |
| this.div.append(this.svgStr.join('')); |
| this.svgDiv = this.div.children().last(); |
| this.div.append('<br/><br/>'); |
| } |
| |
| 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(` |
| <div style="width: 100%; height: ${this.svgHeight}px;"> |
| <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; "> |
| <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.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 funcName = getFuncName(child.f); |
| let subNodes = map.get(funcName); |
| if (subNodes) { |
| subNodes.push(child); |
| } else { |
| map.set(funcName, [child]); |
| } |
| } |
| } |
| const funcNames = [...map.keys()].sort(); |
| let res = []; |
| funcNames.forEach(function (funcName) { |
| const subNodes = map.get(funcName); |
| 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(`<g><title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" |
| depth="${depth}" width="${width}%" owidth="${width}" height="15.0" |
| ofill="rgb(${color.r},${color.g},${color.b})" |
| fill="rgb(${color.r},${color.g},${color.b})" |
| style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> |
| <text x="${x}%" y="${y + 12}"></text></g>`); |
| |
| let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c; |
| let childXOffset = xOffset; |
| for (let child of children) { |
| childXOffset = this._renderSvgNodesWithSameRoot(child, depth + 1, childXOffset); |
| } |
| return xOffset + width; |
| } |
| |
| _renderUnzoomNode() { |
| this.svgStr.push(`<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.svgStr.push(`<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.svgStr.push(`<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>`); |
| } |
| |
| _renderSearchNode() { |
| this.svgStr.push(`<rect style="stroke:rgb(0,0,0); rx="10" ry="10" |
| x="1150" y="10" width="80" height="30" |
| fill="rgb(255,255,255)" class="search"/> |
| <text x="1160" y="30" class="search">Search</text>`); |
| } |
| |
| _adjustTextSizeForNode(g) { |
| let text = g.find('text'); |
| let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01; |
| 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 = $('<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 = $('<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.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 && BigInt(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 = BigInt(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 = $('<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 = $('<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() { |
| 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()); |
| })(); |