| /* |
| * 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 openHtml(name, attrs={}) { |
| let s = `<${name} `; |
| for (let key in attrs) { |
| s += `${key}="${attrs[key]}" `; |
| } |
| s += '>'; |
| return s; |
| } |
| |
| function closeHtml(name) { |
| return `</${name}>`; |
| } |
| |
| function getHtml(name, attrs={}) { |
| let text; |
| if ('text' in attrs) { |
| text = attrs.text; |
| delete attrs.text; |
| } |
| let s = openHtml(name, attrs); |
| if (text) { |
| s += text; |
| } |
| s += closeHtml(name); |
| return s; |
| } |
| |
| function getTableRow(cols, colName, attrs={}) { |
| let s = openHtml('tr', attrs); |
| for (let col of cols) { |
| s += `<${colName}>${col}</${colName}>`; |
| } |
| s += '</tr>'; |
| return s; |
| } |
| |
| function toPercentageStr(percentage) { |
| return percentage.toFixed(2) + '%'; |
| } |
| |
| function getProcessName(pid) { |
| let name = gProcesses[pid]; |
| return name ? `${pid} (${name})`: pid.toString(); |
| } |
| |
| function getThreadName(tid) { |
| let name = gThreads[tid]; |
| return name ? `${tid} (${name})`: tid.toString(); |
| } |
| |
| function getLibName(libId) { |
| return gLibList[libId]; |
| } |
| |
| function getFuncName(funcId) { |
| return gFunctionMap[funcId][1]; |
| } |
| |
| function getLibNameOfFunction(funcId) { |
| return getLibName(gFunctionMap[funcId][0]); |
| } |
| |
| class TabManager { |
| constructor(divContainer) { |
| this.div = $('<div>', {id: 'tabs'}); |
| this.div.appendTo(divContainer); |
| this.div.append(getHtml('ul')); |
| this.tabs = []; |
| this.isDrawCalled = false; |
| } |
| |
| addTab(title, tabObj) { |
| let id = 'tab_' + this.div.children().length; |
| let tabDiv = $('<div>', {id: id}); |
| tabDiv.appendTo(this.div); |
| this.div.children().first().append( |
| getHtml('li', {text: getHtml('a', {href: '#' + id, text: title})})); |
| tabObj.init(tabDiv); |
| this.tabs.push(tabObj); |
| if (this.isDrawCalled) { |
| this.div.tabs('refresh'); |
| } |
| return tabObj; |
| } |
| |
| findTab(title) { |
| let links = this.div.find('li a'); |
| for (let i = 0; i < links.length; ++i) { |
| if (links.eq(i).text() == title) { |
| return this.tabs[i]; |
| } |
| } |
| return null; |
| } |
| |
| draw() { |
| this.div.tabs({ |
| active: 0, |
| }); |
| this.tabs.forEach(function(tab) { |
| tab.draw(); |
| }); |
| this.isDrawCalled = true; |
| } |
| |
| setActive(tabObj) { |
| for (let i = 0; i < this.tabs.length; ++i) { |
| if (this.tabs[i] == tabObj) { |
| this.div.tabs('option', 'active', i); |
| break; |
| } |
| } |
| } |
| } |
| |
| // Show global information retrieved from the record file, including: |
| // record time |
| // machine type |
| // Android version |
| // record cmdline |
| // total samples |
| class RecordFileView { |
| constructor(divContainer) { |
| this.div = $('<div>'); |
| this.div.appendTo(divContainer); |
| } |
| |
| draw() { |
| if (gRecordInfo.recordTime) { |
| this.div.append(getHtml('p', {text: 'Record Time: ' + gRecordInfo.recordTime})); |
| } |
| if (gRecordInfo.machineType) { |
| this.div.append(getHtml('p', {text: 'Machine Type: ' + gRecordInfo.machineType})); |
| } |
| if (gRecordInfo.androidVersion) { |
| this.div.append(getHtml('p', {text: 'Android Version: ' + gRecordInfo.androidVersion})); |
| } |
| if (gRecordInfo.recordCmdline) { |
| this.div.append(getHtml('p', {text: 'Record Cmdline: ' + gRecordInfo.recordCmdline})); |
| } |
| this.div.append(getHtml('p', {text: 'Total Samples: ' + gRecordInfo.totalSamples})); |
| } |
| } |
| |
| // Show pieChart of event count percentage of each process, thread, library and function. |
| class ChartView { |
| constructor(divContainer, eventInfo) { |
| this.id = divContainer.children().length; |
| this.div = $('<div>', {id: 'chartstat_' + this.id}); |
| this.div.appendTo(divContainer); |
| this.eventInfo = eventInfo; |
| this.processInfo = null; |
| this.threadInfo = null; |
| this.libInfo = null; |
| this.states = { |
| SHOW_EVENT_INFO: 1, |
| SHOW_PROCESS_INFO: 2, |
| SHOW_THREAD_INFO: 3, |
| SHOW_LIB_INFO: 4, |
| }; |
| } |
| |
| _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; |
| } |
| |
| _drawTitle() { |
| if (this.eventInfo) { |
| this.div.append(getHtml('p', {text: `Event Type: ${this.eventInfo.eventName}`})); |
| } |
| if (this.processInfo) { |
| this.div.append(getHtml('p', |
| {text: `Process: ${getProcessName(this.processInfo.pid)}`})); |
| } |
| if (this.threadInfo) { |
| this.div.append(getHtml('p', |
| {text: `Thread: ${getThreadName(this.threadInfo.tid)}`})); |
| } |
| if (this.libInfo) { |
| this.div.append(getHtml('p', |
| {text: `Library: ${getLibName(this.libInfo.libId)}`})); |
| } |
| if (this.processInfo) { |
| let button = $('<button>', {text: 'Back'}); |
| button.appendTo(this.div); |
| button.button().click(() => this._goBack()); |
| } |
| } |
| |
| _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() { |
| this.div.empty(); |
| this._drawTitle(); |
| let data = new google.visualization.DataTable(); |
| let title; |
| let state = this._getState(); |
| if (state == this.states.SHOW_EVENT_INFO) { |
| title = 'Processes in event type ' + this.eventInfo.eventName; |
| data.addColumn('string', 'Process'); |
| data.addColumn('number', 'EventCount'); |
| let rows = []; |
| for (let process of this.eventInfo.processes) { |
| rows.push([getProcessName(process.pid), process.eventCount]); |
| } |
| data.addRows(rows); |
| } else if (state == this.states.SHOW_PROCESS_INFO) { |
| title = 'Threads in process ' + getProcessName(this.processInfo.pid); |
| data.addColumn('string', 'Thread'); |
| data.addColumn('number', 'EventCount'); |
| let rows = []; |
| for (let thread of this.processInfo.threads) { |
| rows.push([getThreadName(thread.tid), thread.eventCount]); |
| } |
| data.addRows(rows); |
| } else if (state == this.states.SHOW_THREAD_INFO) { |
| title = 'Libraries in thread ' + getThreadName(this.threadInfo.tid); |
| data.addColumn('string', 'Lib'); |
| data.addColumn('number', 'EventCount'); |
| let rows = []; |
| for (let lib of this.threadInfo.libs) { |
| rows.push([getLibName(lib.libId), lib.eventCount]); |
| } |
| data.addRows(rows); |
| } else if (state == this.states.SHOW_LIB_INFO) { |
| title = 'Functions in library ' + getLibName(this.libInfo.libId); |
| data.addColumn('string', 'Function'); |
| data.addColumn('number', 'EventCount'); |
| let rows = []; |
| for (let func of this.libInfo.functions) { |
| rows.push([getFuncName(func.g.f), func.g.e]); |
| } |
| data.addRows(rows); |
| } |
| |
| let options = { |
| title: title, |
| width: 1000, |
| height: 600, |
| }; |
| let wrapperDiv = $('<div>'); |
| wrapperDiv.appendTo(this.div); |
| let chart = new google.visualization.PieChart(wrapperDiv.get(0)); |
| chart.draw(data, options); |
| google.visualization.events.addListener(chart, 'select', () => this._selectHandler(chart)); |
| } |
| } |
| |
| |
| class ChartStatTab { |
| constructor() { |
| } |
| |
| init(div) { |
| this.div = div; |
| this.recordFileView = new RecordFileView(this.div); |
| this.chartViews = []; |
| for (let eventInfo of gSampleInfo) { |
| this.chartViews.push(new ChartView(this.div, eventInfo)); |
| } |
| } |
| |
| draw() { |
| this.recordFileView.draw(); |
| let thisObj = this; |
| google.charts.setOnLoadCallback(function() { |
| for (let charView of thisObj.chartViews) { |
| charView.draw(); |
| } |
| }); |
| } |
| } |
| |
| |
| class SampleTableTab { |
| constructor() { |
| } |
| |
| init(div) { |
| this.div = div; |
| } |
| |
| draw() { |
| for (let tId = 0; tId < gSampleInfo.length; tId++) { |
| let eventInfo = gSampleInfo[tId]; |
| let eventName = eventInfo.eventName; |
| let percentMul = 100.0 / eventInfo.eventCount; |
| let tableId = 'reportTable_' + tId; |
| let titles = [eventName + '_WithChildren', eventName, 'sampleCount', 'process', |
| 'thread', 'lib', 'function']; |
| let tableStr = openHtml('table', {id: tableId, cellspacing: '0', width: '100%'}) + |
| getHtml('thead', {text: getTableRow(titles, 'th')}) + |
| getHtml('tfoot', {text: getTableRow(titles, 'th')}) + |
| openHtml('tbody'); |
| |
| for (let i = 0; i < eventInfo.processes.length; ++i) { |
| let processInfo = eventInfo.processes[i]; |
| let processName = getProcessName(processInfo.pid); |
| for (let j = 0; j < processInfo.threads.length; ++j) { |
| let threadInfo = processInfo.threads[j]; |
| let threadName = getThreadName(threadInfo.tid); |
| for (let k = 0; k < threadInfo.libs.length; ++k) { |
| let lib = threadInfo.libs[k]; |
| for (let t = 0; t < lib.functions.length; ++t) { |
| let func = lib.functions[t]; |
| let key = [i, j, k, t].join('_'); |
| let treePercentage = toPercentageStr(func.g.s * percentMul); |
| let selfPercenetage = toPercentageStr(func.g.e * percentMul); |
| tableStr += getTableRow([treePercentage, selfPercenetage, func.c, |
| processName, threadName, getLibName(lib.libId), |
| getFuncName(func.g.f)], 'td', {key: key}); |
| } |
| } |
| } |
| } |
| tableStr += closeHtml('tbody') + closeHtml('table'); |
| this.div.append(tableStr); |
| let table = this.div.find(`table#${tableId}`).dataTable({ |
| lengthMenu: [10, 20, 50, 100, -1], |
| processing: true, |
| order: [0, 'desc'], |
| responsive: true, |
| }); |
| |
| table.find('tr').css('cursor', 'pointer'); |
| table.on('click', 'tr', function() { |
| let key = this.getAttribute('key'); |
| if (!key) { |
| return; |
| } |
| let indexes = key.split('_'); |
| let processInfo = eventInfo.processes[indexes[0]]; |
| let threadInfo = processInfo.threads[indexes[1]]; |
| let lib = threadInfo.libs[indexes[2]]; |
| let func = lib.functions[indexes[3]]; |
| FunctionFlameGraphTab.showFunction(eventInfo, processInfo, threadInfo, lib, func); |
| }); |
| } |
| } |
| } |
| |
| |
| // Show embedded flamegraph generated by inferno. |
| class FlameGraphTab { |
| constructor() { |
| } |
| |
| init(div) { |
| this.div = div; |
| } |
| |
| draw() { |
| $('div#flamegraph_id').appendTo(this.div).css('display', 'block'); |
| flamegraphInit(); |
| } |
| } |
| |
| |
| // Show the callgraph and reverse callgraph of a function as flamegraphs. |
| class FunctionFlameGraphTab { |
| static showFunction(eventInfo, processInfo, threadInfo, lib, func) { |
| let title = 'Function Flamegraph'; |
| let tab = gTabs.findTab(title); |
| if (!tab) { |
| tab = gTabs.addTab(title, new FunctionFlameGraphTab()); |
| } |
| 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.draw(); |
| gTabs.setActive(this); |
| } |
| |
| draw() { |
| if (!this.func) { |
| return; |
| } |
| this.div.empty(); |
| let eventName = this.eventInfo.eventName; |
| let processName = getProcessName(this.processInfo.pid); |
| let threadName = getThreadName(this.threadInfo.tid); |
| let libName = getLibName(this.lib.libId); |
| let funcName = getFuncName(this.func.g.f); |
| let title = getHtml('p', {text: `Event ${eventName}`}) + |
| getHtml('p', {text: `Process ${processName}`}) + |
| getHtml('p', {text: `Thread ${threadName}`}) + |
| getHtml('p', {text: `Library ${libName}`}) + |
| getHtml('p', {text: `Functions called by ${funcName}`}); |
| this.div.append(title); |
| |
| let selectDiv = $('<div>'); |
| selectDiv.appendTo(this.div); |
| selectDiv.css('position', 'absolute').css('right', '0%').css('top', '100px'); |
| let selectPercentStr = |
| getHtml('label', {for: 'select_percent', text: 'Show percentage relative to:'}) + |
| getHtml('select', {name: 'select_percent', id: 'select_percent', text: |
| getHtml('option', {value: 'all', text: 'All processes'}) + |
| getHtml('option', {value: 'process', text: `Process ${processName}`}) + |
| getHtml('option', {value: 'thread', text: `Thread ${threadName}`}) + |
| getHtml('option', {value: 'func', text: `Function ${funcName}`})}); |
| selectDiv.append(selectPercentStr); |
| let thisObj = this; |
| selectDiv.find('#select_percent'); |
| let totalEventCount = 0; |
| let selectIndex = 0; |
| if (this.selectPercent == 'all') { |
| totalEventCount = this.eventInfo.eventCount; |
| selectIndex = 0; |
| } else if (this.selectPercent == 'process') { |
| totalEventCount = this.processInfo.eventCount; |
| selectIndex = 1; |
| } else if (this.selectPercent == 'thread') { |
| totalEventCount = this.threadInfo.eventCount; |
| selectIndex = 2; |
| } else { |
| totalEventCount = this.func.g.s; |
| selectIndex = 3; |
| } |
| selectDiv.find('#select_percent').children().eq(selectIndex).attr('selected', 'selected'); |
| selectDiv.find('#select_percent').selectmenu({ |
| change: function() { |
| thisObj.selectPercent = this.value; |
| thisObj.draw(); |
| }, |
| }); |
| |
| let gView = new FlameGraphView(this.div, this.func.g, totalEventCount, false); |
| gView.draw(); |
| |
| this.div.append(getHtml('p')); |
| this.div.append(getHtml('p', {text: `Functions calling ${getFuncName(this.func.g.f)}`})); |
| let rgView = new FlameGraphView(this.div, this.func.rg, totalEventCount, true); |
| rgView.draw(); |
| } |
| } |
| |
| // Given a callgraph, show the flamegraph. |
| class FlameGraphView { |
| // If reverseOrder is false, the root of the flamegraph is at the bottom, |
| // otherwise it is at the top. |
| constructor(divContainer, callgraph, totalEventCount, reverseOrder) { |
| this.id = divContainer.children().length; |
| this.div = $('<div>', {id: 'fg_' + this.id}); |
| this.div.appendTo(divContainer); |
| this.callgraph = callgraph; |
| this.totalEventCount = totalEventCount; |
| this.reverseOrder = reverseOrder; |
| this.svgWidth = $(window).width(); |
| this.svgNodeHeight = 17; |
| this.fontSize = 12; |
| |
| function getMaxDepth(node) { |
| let depth = 0; |
| for (let child of node.c) { |
| depth = Math.max(depth, getMaxDepth(child)); |
| } |
| return depth + 1; |
| } |
| this.maxDepth = getMaxDepth(this.callgraph); |
| this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3); |
| } |
| |
| draw() { |
| this.div.empty(); |
| this.div.css('width', '100%').css('height', this.svgHeight + 'px'); |
| let svgStr = '<svg xmlns="http://www.w3.org/2000/svg" \ |
| xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" \ |
| width="100%" height="100%" style="border: 1px solid black; font-family: Monospace;"> \ |
| </svg>'; |
| this.div.append(svgStr); |
| this.svg = this.div.find('svg'); |
| this._renderBackground(); |
| this._renderSvgNodes(this.callgraph, 0, 0); |
| this._renderUnzoomNode(); |
| this._renderInfoNode(); |
| this._renderPercentNode(); |
| // Make the added nodes in the svg visible. |
| this.div.html(this.div.html()); |
| this.svg = this.div.find('svg'); |
| this._adjustTextSize(); |
| this._enableZoom(); |
| this._enableInfo(); |
| this._adjustTextSizeOnResize(); |
| } |
| |
| _renderBackground() { |
| this.svg.append(`<defs > <linearGradient id="background_gradient_${this.id}" |
| y1="0" y2="1" x1="0" x2="0" > \ |
| <stop stop-color="#eeeeee" offset="5%" /> \ |
| <stop stop-color="#efefb1" offset="90%" /> \ |
| </linearGradient> \ |
| </defs> \ |
| <rect x="0" y="0" width="100%" height="100%" \ |
| fill="url(#background_gradient_${this.id})" />`); |
| } |
| |
| _getYForDepth(depth) { |
| if (this.reverseOrder) { |
| return (depth + 3) * this.svgNodeHeight; |
| } |
| return this.svgHeight - (depth + 1) * this.svgNodeHeight; |
| } |
| |
| _getWidthPercentage(eventCount) { |
| return eventCount * 100.0 / this.callgraph.s; |
| } |
| |
| _getEventPercentage(eventCount) { |
| return eventCount * 100.0 / this.totalEventCount; |
| } |
| |
| _getHeatColor(widthPercentage) { |
| return { |
| r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)), |
| g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)), |
| b: 100, |
| }; |
| } |
| |
| _renderSvgNodes(callNode, depth, xOffset) { |
| let x = xOffset; |
| let y = this._getYForDepth(depth); |
| let width = this._getWidthPercentage(callNode.s); |
| if (width < 0.1) { |
| return xOffset; |
| } |
| let color = this._getHeatColor(width); |
| let borderColor = {}; |
| for (let key in color) { |
| borderColor[key] = Math.max(0, color[key] - 50); |
| } |
| let funcName = getFuncName(callNode.f); |
| let libName = getLibNameOfFunction(callNode.f); |
| let eventPercent = this._getEventPercentage(callNode.s); |
| let title = funcName + ' | ' + libName + ' (' + callNode.s + ' events: ' + |
| eventPercent.toFixed(2) + '%)'; |
| this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \ |
| depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \ |
| ofill="rgb(${color.r},${color.g},${color.b})" \ |
| fill="rgb(${color.r},${color.g},${color.b})" \ |
| style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \ |
| <text x="${x}%" y="${y + 12}" font-size="${this.fontSize}" \ |
| font-family="Monospace"></text></g>`); |
| |
| let childXOffset = xOffset; |
| for (let child of callNode.c) { |
| childXOffset = this._renderSvgNodes(child, depth + 1, childXOffset); |
| } |
| return xOffset + width; |
| } |
| |
| _renderUnzoomNode() { |
| this.svg.append(`<rect id="zoom_rect_${this.id}" style="display:none;stroke:rgb(0,0,0);" \ |
| rx="10" ry="10" x="10" y="10" width="80" height="30" \ |
| fill="rgb(255,255,255)"/> \ |
| <text id="zoom_text_${this.id}" x="19" y="30" style="display:none">Zoom out</text>`); |
| } |
| |
| _renderInfoNode() { |
| this.svg.append(`<clipPath id="info_clip_path_${this.id}"> \ |
| <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \ |
| width="789" height="30" fill="rgb(255,255,255)"/> \ |
| </clipPath> \ |
| <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \ |
| width="799" height="30" fill="rgb(255,255,255)"/> \ |
| <text clip-path="url(#info_clip_path_${this.id})" \ |
| id="info_text_${this.id}" x="128" y="30"></text>`); |
| } |
| |
| _renderPercentNode() { |
| this.svg.append(`<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" \ |
| x="934" y="10" width="82" height="30" \ |
| fill="rgb(255,255,255)"/> \ |
| <text id="percent_text_${this.id}" text-anchor="end" \ |
| x="999" y="30">100.00%</text>`); |
| } |
| |
| _adjustTextSizeForNode(g) { |
| let text = g.find('text'); |
| let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01; |
| if (width < 28) { |
| text.text(''); |
| return; |
| } |
| let methodName = g.find('title').text().split(' | ')[0]; |
| let numCharacters; |
| for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) { |
| if (numCharacters * 7.5 <= width) { |
| break; |
| } |
| } |
| if (numCharacters == methodName.length) { |
| text.text(methodName); |
| } else { |
| text.text(methodName.substring(0, numCharacters - 2) + '..'); |
| } |
| } |
| |
| _adjustTextSize() { |
| this.svgWidth = $(window).width(); |
| let thisObj = this; |
| this.svg.find('g').each(function(_, g) { |
| thisObj._adjustTextSizeForNode($(g)); |
| }); |
| } |
| |
| _enableZoom() { |
| this.zoomStack = [this.svg.find('g').first().get(0)]; |
| this.svg.find('g').css('cursor', 'pointer').click(zoom); |
| this.svg.find(`#zoom_rect_${this.id}`).css('cursor', 'pointer').click(unzoom); |
| this.svg.find(`#zoom_text_${this.id}`).css('cursor', 'pointer').click(unzoom); |
| |
| let thisObj = this; |
| function zoom() { |
| thisObj.zoomStack.push(this); |
| displayFromElement(this); |
| thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'block'); |
| thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'block'); |
| } |
| |
| function unzoom() { |
| if (thisObj.zoomStack.length > 1) { |
| thisObj.zoomStack.pop(); |
| displayFromElement(thisObj.zoomStack[thisObj.zoomStack.length - 1]); |
| if (thisObj.zoomStack.length == 1) { |
| thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'none'); |
| thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'none'); |
| } |
| } |
| } |
| |
| function displayFromElement(g) { |
| g = $(g); |
| let clickedRect = g.find('rect'); |
| let clickedOriginX = parseFloat(clickedRect.attr('ox')); |
| let clickedDepth = parseInt(clickedRect.attr('depth')); |
| let clickedOriginWidth = parseFloat(clickedRect.attr('owidth')); |
| let scaleFactor = 100.0 / clickedOriginWidth; |
| thisObj.svg.find('g').each(function(_, g) { |
| g = $(g); |
| let text = g.find('text'); |
| let rect = g.find('rect'); |
| let depth = parseInt(rect.attr('depth')); |
| let ox = parseFloat(rect.attr('ox')); |
| let owidth = parseFloat(rect.attr('owidth')); |
| if (depth < clickedDepth || ox < clickedOriginX - 1e-9 || |
| ox + owidth > clickedOriginX + clickedOriginWidth + 1e-9) { |
| rect.css('display', 'none'); |
| text.css('display', 'none'); |
| } else { |
| rect.css('display', 'block'); |
| text.css('display', 'block'); |
| let nx = (ox - clickedOriginX) * scaleFactor + '%'; |
| let ny = thisObj._getYForDepth(depth - clickedDepth); |
| rect.attr('x', nx); |
| rect.attr('y', ny); |
| rect.attr('width', owidth * scaleFactor + '%'); |
| text.attr('x', nx); |
| text.attr('y', ny + 12); |
| thisObj._adjustTextSizeForNode(g); |
| } |
| }); |
| } |
| } |
| |
| _enableInfo() { |
| this.selected = null; |
| let thisObj = this; |
| this.svg.find('g').on('mouseenter', function(e) { |
| 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 = /(.*) \(.* (\d*\.\d*%)\)/g; |
| let match = regexp.exec(methodAndInfo[1]); |
| if (match.length > 2) { |
| let percentage = match[2]; |
| thisObj.svg.find(`#percent_text_${thisObj.id}`).text(percentage); |
| } |
| }); |
| } |
| |
| _adjustTextSizeOnResize() { |
| function throttle(callback) { |
| let running = false; |
| return function() { |
| if (!running) { |
| running = true; |
| window.requestAnimationFrame(function () { |
| callback(); |
| running = false; |
| }); |
| } |
| }; |
| } |
| $(window).resize(throttle(() => this._adjustTextSize())); |
| } |
| } |
| |
| function initGlobalObjects() { |
| gTabs = new TabManager($('div#report_content')); |
| gRecordInfo = JSON.parse(gRecordInfo); |
| gProcesses = gRecordInfo.processNames; |
| gThreads = gRecordInfo.threadNames; |
| gLibList = gRecordInfo.libList; |
| gFunctionMap = gRecordInfo.functionMap; |
| gSampleInfo = gRecordInfo.sampleInfo; |
| } |
| |
| function createTabs() { |
| gTabs.addTab('Chart Statistics', new ChartStatTab()); |
| gTabs.addTab('Sample Table', new SampleTableTab()); |
| gTabs.addTab('Flamegraph', new FlameGraphTab()); |
| gTabs.draw(); |
| } |
| |
| function main() { |
| initGlobalObjects(); |
| createTabs(); |
| } |
| |
| let gTabs; |
| let gProcesses; |
| let gThreads; |
| let gLibList; |
| let gFunctionMap; |
| let gSampleInfo; |
| |
| $(document).ready(main); |
| |
| }()) |