Home | History | Annotate | Download | only in scripts
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 'use strict';
     17 
     18 // Use IIFE to avoid leaking names to other scripts.
     19 $(document).ready(function() {
     20 
     21 function openHtml(name, attrs={}) {
     22     let s = `<${name} `;
     23     for (let key in attrs) {
     24         s += `${key}="${attrs[key]}" `;
     25     }
     26     s += '>';
     27     return s;
     28 }
     29 
     30 function closeHtml(name) {
     31     return `</${name}>`;
     32 }
     33 
     34 function getHtml(name, attrs={}) {
     35     let text;
     36     if ('text' in attrs) {
     37         text = attrs.text;
     38         delete attrs.text;
     39     }
     40     let s = openHtml(name, attrs);
     41     if (text) {
     42         s += text;
     43     }
     44     s += closeHtml(name);
     45     return s;
     46 }
     47 
     48 function getTableRow(cols, colName, attrs={}) {
     49     let s = openHtml('tr', attrs);
     50     for (let col of cols) {
     51         s += `<${colName}>${col}</${colName}>`;
     52     }
     53     s += '</tr>';
     54     return s;
     55 }
     56 
     57 function toPercentageStr(percentage) {
     58     return percentage.toFixed(2) + '%';
     59 }
     60 
     61 function getProcessName(pid) {
     62     let name = gProcesses[pid];
     63     return name ? `${pid} (${name})`: pid.toString();
     64 }
     65 
     66 function getThreadName(tid) {
     67     let name = gThreads[tid];
     68     return name ? `${tid} (${name})`: tid.toString();
     69 }
     70 
     71 function getLibName(libId) {
     72     return gLibList[libId];
     73 }
     74 
     75 function getFuncName(funcId) {
     76     return gFunctionMap[funcId].f;
     77 }
     78 
     79 function getLibNameOfFunction(funcId) {
     80     return getLibName(gFunctionMap[funcId].l);
     81 }
     82 
     83 function getFuncSourceRange(funcId) {
     84     let func = gFunctionMap[funcId];
     85     if (func.hasOwnProperty('s')) {
     86         return {fileId: func.s[0], startLine: func.s[1], endLine: func.s[2]};
     87     }
     88     return null;
     89 }
     90 
     91 function getFuncDisassembly(funcId) {
     92     let func = gFunctionMap[funcId];
     93     return func.hasOwnProperty('d') ? func.d : null;
     94 }
     95 
     96 function getSourceFilePath(sourceFileId) {
     97     return gSourceFiles[sourceFileId].path;
     98 }
     99 
    100 function getSourceCode(sourceFileId) {
    101     return gSourceFiles[sourceFileId].code;
    102 }
    103 
    104 function isClockEvent(eventInfo) {
    105     return eventInfo.eventName.includes('task-clock') ||
    106             eventInfo.eventName.includes('cpu-clock');
    107 }
    108 
    109 class TabManager {
    110     constructor(divContainer) {
    111         this.div = $('<div>', {id: 'tabs'});
    112         this.div.appendTo(divContainer);
    113         this.div.append(getHtml('ul'));
    114         this.tabs = [];
    115         this.isDrawCalled = false;
    116     }
    117 
    118     addTab(title, tabObj) {
    119         let id = 'tab_' + this.div.children().length;
    120         let tabDiv = $('<div>', {id: id});
    121         tabDiv.appendTo(this.div);
    122         this.div.children().first().append(
    123             getHtml('li', {text: getHtml('a', {href: '#' + id, text: title})}));
    124         tabObj.init(tabDiv);
    125         this.tabs.push(tabObj);
    126         if (this.isDrawCalled) {
    127             this.div.tabs('refresh');
    128         }
    129         return tabObj;
    130     }
    131 
    132     findTab(title) {
    133         let links = this.div.find('li a');
    134         for (let i = 0; i < links.length; ++i) {
    135             if (links.eq(i).text() == title) {
    136                 return this.tabs[i];
    137             }
    138         }
    139         return null;
    140     }
    141 
    142     draw() {
    143         this.div.tabs({
    144             active: 0,
    145         });
    146         this.tabs.forEach(function(tab) {
    147             tab.draw();
    148         });
    149         this.isDrawCalled = true;
    150     }
    151 
    152     setActive(tabObj) {
    153         for (let i = 0; i < this.tabs.length; ++i) {
    154             if (this.tabs[i] == tabObj) {
    155                 this.div.tabs('option', 'active', i);
    156                 break;
    157             }
    158         }
    159     }
    160 }
    161 
    162 // Show global information retrieved from the record file, including:
    163 //   record time
    164 //   machine type
    165 //   Android version
    166 //   record cmdline
    167 //   total samples
    168 class RecordFileView {
    169     constructor(divContainer) {
    170         this.div = $('<div>');
    171         this.div.appendTo(divContainer);
    172     }
    173 
    174     draw() {
    175         google.charts.setOnLoadCallback(() => this.realDraw());
    176     }
    177 
    178     realDraw() {
    179         this.div.empty();
    180         // Draw a table of 'Name', 'Value'.
    181         let rows = [];
    182         if (gRecordInfo.recordTime) {
    183             rows.push(['Record Time', gRecordInfo.recordTime]);
    184         }
    185         if (gRecordInfo.machineType) {
    186             rows.push(['Machine Type', gRecordInfo.machineType]);
    187         }
    188         if (gRecordInfo.androidVersion) {
    189             rows.push(['Android Version', gRecordInfo.androidVersion]);
    190         }
    191         if (gRecordInfo.recordCmdline) {
    192             rows.push(['Record cmdline', gRecordInfo.recordCmdline]);
    193         }
    194         rows.push(['Total Samples', '' + gRecordInfo.totalSamples]);
    195 
    196         let data = new google.visualization.DataTable();
    197         data.addColumn('string', '');
    198         data.addColumn('string', '');
    199         data.addRows(rows);
    200         for (let i = 0; i < rows.length; ++i) {
    201             data.setProperty(i, 0, 'className', 'boldTableCell');
    202         }
    203         let table = new google.visualization.Table(this.div.get(0));
    204         table.draw(data, {
    205             width: '100%',
    206             sort: 'disable',
    207             allowHtml: true,
    208             cssClassNames: {
    209                 'tableCell': 'tableCell',
    210             },
    211         });
    212     }
    213 }
    214 
    215 // Show pieChart of event count percentage of each process, thread, library and function.
    216 class ChartView {
    217     constructor(divContainer, eventInfo) {
    218         this.id = divContainer.children().length;
    219         this.div = $('<div>', {id: 'chartstat_' + this.id});
    220         this.div.appendTo(divContainer);
    221         this.eventInfo = eventInfo;
    222         this.processInfo = null;
    223         this.threadInfo = null;
    224         this.libInfo = null;
    225         this.states = {
    226             SHOW_EVENT_INFO: 1,
    227             SHOW_PROCESS_INFO: 2,
    228             SHOW_THREAD_INFO: 3,
    229             SHOW_LIB_INFO: 4,
    230         };
    231         if (isClockEvent(this.eventInfo)) {
    232             this.getSampleWeight = function (eventCount) {
    233                 return (eventCount / 1000000.0).toFixed(3) + ' ms';
    234             }
    235         } else {
    236             this.getSampleWeight = (eventCount) => '' + eventCount;
    237         }
    238     }
    239 
    240     _getState() {
    241         if (this.libInfo) {
    242             return this.states.SHOW_LIB_INFO;
    243         }
    244         if (this.threadInfo) {
    245             return this.states.SHOW_THREAD_INFO;
    246         }
    247         if (this.processInfo) {
    248             return this.states.SHOW_PROCESS_INFO;
    249         }
    250         return this.states.SHOW_EVENT_INFO;
    251     }
    252 
    253     _goBack() {
    254         let state = this._getState();
    255         if (state == this.states.SHOW_PROCESS_INFO) {
    256             this.processInfo = null;
    257         } else if (state == this.states.SHOW_THREAD_INFO) {
    258             this.threadInfo = null;
    259         } else if (state == this.states.SHOW_LIB_INFO) {
    260             this.libInfo = null;
    261         }
    262         this.draw();
    263     }
    264 
    265     _selectHandler(chart) {
    266         let selectedItem = chart.getSelection()[0];
    267         if (selectedItem) {
    268             let state = this._getState();
    269             if (state == this.states.SHOW_EVENT_INFO) {
    270                 this.processInfo = this.eventInfo.processes[selectedItem.row];
    271             } else if (state == this.states.SHOW_PROCESS_INFO) {
    272                 this.threadInfo = this.processInfo.threads[selectedItem.row];
    273             } else if (state == this.states.SHOW_THREAD_INFO) {
    274                 this.libInfo = this.threadInfo.libs[selectedItem.row];
    275             }
    276             this.draw();
    277         }
    278     }
    279 
    280     draw() {
    281         google.charts.setOnLoadCallback(() => this.realDraw());
    282     }
    283 
    284     realDraw() {
    285         this.div.empty();
    286         this._drawTitle();
    287         this._drawPieChart();
    288     }
    289 
    290     _drawTitle() {
    291         // Draw a table of 'Name', 'Event Count'.
    292         let rows = [];
    293         rows.push(['Event Type: ' + this.eventInfo.eventName,
    294                    this.getSampleWeight(this.eventInfo.eventCount)]);
    295         if (this.processInfo) {
    296             rows.push(['Process: ' + getProcessName(this.processInfo.pid),
    297                        this.getSampleWeight(this.processInfo.eventCount)]);
    298         }
    299         if (this.threadInfo) {
    300             rows.push(['Thread: ' + getThreadName(this.threadInfo.tid),
    301                        this.getSampleWeight(this.threadInfo.eventCount)]);
    302         }
    303         if (this.libInfo) {
    304             rows.push(['Library: ' + getLibName(this.libInfo.libId),
    305                        this.getSampleWeight(this.libInfo.eventCount)]);
    306         }
    307         let data = new google.visualization.DataTable();
    308         data.addColumn('string', '');
    309         data.addColumn('string', '');
    310         data.addRows(rows);
    311         for (let i = 0; i < rows.length; ++i) {
    312             data.setProperty(i, 0, 'className', 'boldTableCell');
    313         }
    314         let wrapperDiv = $('<div>');
    315         wrapperDiv.appendTo(this.div);
    316         let table = new google.visualization.Table(wrapperDiv.get(0));
    317         table.draw(data, {
    318             width: '100%',
    319             sort: 'disable',
    320             allowHtml: true,
    321             cssClassNames: {
    322                 'tableCell': 'tableCell',
    323             },
    324         });
    325         if (this._getState() != this.states.SHOW_EVENT_INFO) {
    326             let button = $('<button>', {text: 'Back'});
    327             button.appendTo(this.div);
    328             button.button().click(() => this._goBack());
    329         }
    330     }
    331 
    332     _drawPieChart() {
    333         let state = this._getState();
    334         let title = null;
    335         let firstColumn = null;
    336         let rows = [];
    337         let thisObj = this;
    338         function getItem(name, eventCount, totalEventCount) {
    339             let sampleWeight = thisObj.getSampleWeight(eventCount);
    340             let percent = (eventCount * 100.0 / totalEventCount).toFixed(2) + '%';
    341             return [name, eventCount, getHtml('pre', {text: name}) +
    342                         getHtml('b', {text: `${sampleWeight} (${percent})`})];
    343         }
    344 
    345         if (state == this.states.SHOW_EVENT_INFO) {
    346             title = 'Processes in event type ' + this.eventInfo.eventName;
    347             firstColumn = 'Process';
    348             for (let process of this.eventInfo.processes) {
    349                 rows.push(getItem('Process: ' + getProcessName(process.pid), process.eventCount,
    350                                   this.eventInfo.eventCount));
    351             }
    352         } else if (state == this.states.SHOW_PROCESS_INFO) {
    353             title = 'Threads in process ' + getProcessName(this.processInfo.pid);
    354             firstColumn = 'Thread';
    355             for (let thread of this.processInfo.threads) {
    356                 rows.push(getItem('Thread: ' + getThreadName(thread.tid), thread.eventCount,
    357                                   this.processInfo.eventCount));
    358             }
    359         } else if (state == this.states.SHOW_THREAD_INFO) {
    360             title = 'Libraries in thread ' + getThreadName(this.threadInfo.tid);
    361             firstColumn = 'Library';
    362             for (let lib of this.threadInfo.libs) {
    363                 rows.push(getItem('Library: ' + getLibName(lib.libId), lib.eventCount,
    364                                   this.threadInfo.eventCount));
    365             }
    366         } else if (state == this.states.SHOW_LIB_INFO) {
    367             title = 'Functions in library ' + getLibName(this.libInfo.libId);
    368             firstColumn = 'Function';
    369             for (let func of this.libInfo.functions) {
    370                 rows.push(getItem('Function: ' + getFuncName(func.g.f), func.g.e,
    371                                   this.libInfo.eventCount));
    372             }
    373         }
    374         let data = new google.visualization.DataTable();
    375         data.addColumn('string', firstColumn);
    376         data.addColumn('number', 'EventCount');
    377         data.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
    378         data.addRows(rows);
    379 
    380         let wrapperDiv = $('<div>');
    381         wrapperDiv.appendTo(this.div);
    382         let chart = new google.visualization.PieChart(wrapperDiv.get(0));
    383         chart.draw(data, {
    384             title: title,
    385             width: 1000,
    386             height: 600,
    387             tooltip: {isHtml: true},
    388         });
    389         google.visualization.events.addListener(chart, 'select', () => this._selectHandler(chart));
    390     }
    391 }
    392 
    393 
    394 class ChartStatTab {
    395     constructor() {
    396     }
    397 
    398     init(div) {
    399         this.div = div;
    400         this.recordFileView = new RecordFileView(this.div);
    401         this.chartViews = [];
    402         for (let eventInfo of gSampleInfo) {
    403             this.chartViews.push(new ChartView(this.div, eventInfo));
    404         }
    405     }
    406 
    407     draw() {
    408         this.recordFileView.draw();
    409         for (let charView of this.chartViews) {
    410             charView.draw();
    411         }
    412     }
    413 }
    414 
    415 
    416 class SampleTableTab {
    417     constructor() {
    418     }
    419 
    420     init(div) {
    421         this.div = div;
    422         this.selectorView = null;
    423         this.sampleTableViews = [];
    424     }
    425 
    426     draw() {
    427         this.selectorView = new SampleTableWeightSelectorView(this.div, gSampleInfo[0],
    428                                                               () => this.onSampleWeightChange());
    429         this.selectorView.draw();
    430         for (let eventInfo of gSampleInfo) {
    431             this.div.append(getHtml('hr'));
    432             this.sampleTableViews.push(new SampleTableView(this.div, eventInfo));
    433         }
    434         this.onSampleWeightChange();
    435     }
    436 
    437     onSampleWeightChange() {
    438         for (let i = 0; i < gSampleInfo.length; ++i) {
    439             let sampleWeightFunction = this.selectorView.getSampleWeightFunction(gSampleInfo[i]);
    440             let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix(gSampleInfo[i]);
    441             this.sampleTableViews[i].draw(sampleWeightFunction, sampleWeightSuffix);
    442         }
    443     }
    444 }
    445 
    446 // Select the way to show sample weight in SampleTableTab.
    447 // 1. Show percentage of event count.
    448 // 2. Show event count (For cpu-clock and task-clock events, it is time in ms).
    449 class SampleTableWeightSelectorView {
    450     constructor(divContainer, firstEventInfo, onSelectChange) {
    451         this.div = $('<div>');
    452         this.div.appendTo(divContainer);
    453         this.onSelectChange = onSelectChange;
    454         this.options = {
    455             SHOW_PERCENT: 0,
    456             SHOW_EVENT_COUNT: 1,
    457         };
    458         if (isClockEvent(firstEventInfo)) {
    459             this.curOption = this.options.SHOW_EVENT_COUNT;
    460         } else {
    461             this.curOption = this.options.SHOW_PERCENT;
    462         }
    463     }
    464 
    465     draw() {
    466         let options = ['Show percentage of event count', 'Show event count'];
    467         let optionStr = '';
    468         for (let i = 0; i < options.length; ++i) {
    469             optionStr += getHtml('option', {value: i, text: options[i]});
    470         }
    471         this.div.append(getHtml('select', {text: optionStr}));
    472         let selectMenu = this.div.children().last();
    473         selectMenu.children().eq(this.curOption).attr('selected', 'selected');
    474         let thisObj = this;
    475         selectMenu.selectmenu({
    476             change: function() {
    477                 thisObj.curOption = this.value;
    478                 thisObj.onSelectChange();
    479             },
    480             width: '100%',
    481         });
    482     }
    483 
    484     getSampleWeightFunction(eventInfo) {
    485         if (this.curOption == this.options.SHOW_PERCENT) {
    486             return function(eventCount) {
    487                 return (eventCount * 100.0 / eventInfo.eventCount).toFixed(2) + '%';
    488             }
    489         }
    490         if (isClockEvent(eventInfo)) {
    491             return (eventCount) => (eventCount / 1000000.0).toFixed(3);
    492         }
    493         return (eventCount) => '' + eventCount;
    494     }
    495 
    496     getSampleWeightSuffix(eventInfo) {
    497         if (this.curOption == this.options.SHOW_EVENT_COUNT && isClockEvent(eventInfo)) {
    498             return ' ms';
    499         }
    500         return '';
    501     }
    502 }
    503 
    504 
    505 class SampleTableView {
    506     constructor(divContainer, eventInfo) {
    507         this.id = divContainer.children().length;
    508         this.div = $('<div>');
    509         this.div.appendTo(divContainer);
    510         this.eventInfo = eventInfo;
    511     }
    512 
    513     draw(getSampleWeight, sampleWeightSuffix) {
    514         // Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library', 'Function'.
    515         this.div.empty();
    516         let eventInfo = this.eventInfo;
    517         let sampleWeight = getSampleWeight(eventInfo.eventCount);
    518         this.div.append(getHtml('p', {text: `Sample table for event ${eventInfo.eventName}, ` +
    519                 `total count ${sampleWeight}${sampleWeightSuffix}`}));
    520         let tableId = 'sampleTable_' + this.id;
    521         let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : '';
    522         let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples',
    523                       'Process', 'Thread', 'Library', 'Function'];
    524         let tableStr = openHtml('table', {id: tableId, cellspacing: '0', width: '100%'}) +
    525                         getHtml('thead', {text: getTableRow(titles, 'th')}) +
    526                         getHtml('tfoot', {text: getTableRow(titles, 'th')}) +
    527                         openHtml('tbody');
    528         for (let i = 0; i < eventInfo.processes.length; ++i) {
    529             let processInfo = eventInfo.processes[i];
    530             let processName = getProcessName(processInfo.pid);
    531             for (let j = 0; j < processInfo.threads.length; ++j) {
    532                 let threadInfo = processInfo.threads[j];
    533                 let threadName = getThreadName(threadInfo.tid);
    534                 for (let k = 0; k < threadInfo.libs.length; ++k) {
    535                     let lib = threadInfo.libs[k];
    536                     let libName = getLibName(lib.libId);
    537                     for (let t = 0; t < lib.functions.length; ++t) {
    538                         let func = lib.functions[t];
    539                         let key = [i, j, k, t].join('_');
    540                         let totalValue = getSampleWeight(func.g.s);
    541                         let selfValue = getSampleWeight(func.g.e);
    542                         tableStr += getTableRow([totalValue, selfValue, func.c,
    543                                                  processName, threadName, libName,
    544                                                  getFuncName(func.g.f)], 'td', {key: key});
    545                     }
    546                 }
    547             }
    548         }
    549         tableStr += closeHtml('tbody') + closeHtml('table');
    550         this.div.append(tableStr);
    551         let table = this.div.find(`table#${tableId}`).dataTable({
    552             lengthMenu: [10, 20, 50, 100, -1],
    553             processing: true,
    554             order: [0, 'desc'],
    555             responsive: true,
    556         });
    557 
    558         table.find('tr').css('cursor', 'pointer');
    559         table.on('click', 'tr', function() {
    560             let key = this.getAttribute('key');
    561             if (!key) {
    562                 return;
    563             }
    564             let indexes = key.split('_');
    565             let processInfo = eventInfo.processes[indexes[0]];
    566             let threadInfo = processInfo.threads[indexes[1]];
    567             let lib = threadInfo.libs[indexes[2]];
    568             let func = lib.functions[indexes[3]];
    569             FunctionTab.showFunction(eventInfo, processInfo, threadInfo, lib, func);
    570         });
    571     }
    572 }
    573 
    574 
    575 // Show embedded flamegraph generated by inferno.
    576 class FlameGraphTab {
    577     constructor() {
    578     }
    579 
    580     init(div) {
    581         this.div = div;
    582     }
    583 
    584     draw() {
    585         $('div#flamegraph_id').appendTo(this.div).css('display', 'block');
    586         flamegraphInit();
    587     }
    588 }
    589 
    590 
    591 // FunctionTab: show information of a function.
    592 // 1. Show the callgrpah and reverse callgraph of a function as flamegraphs.
    593 // 2. Show the annotated source code of the function.
    594 class FunctionTab {
    595     static showFunction(eventInfo, processInfo, threadInfo, lib, func) {
    596         let title = 'Function';
    597         let tab = gTabs.findTab(title);
    598         if (!tab) {
    599             tab = gTabs.addTab(title, new FunctionTab());
    600         }
    601         tab.setFunction(eventInfo, processInfo, threadInfo, lib, func);
    602     }
    603 
    604     constructor() {
    605         this.func = null;
    606         this.selectPercent = 'thread';
    607     }
    608 
    609     init(div) {
    610         this.div = div;
    611     }
    612 
    613     setFunction(eventInfo, processInfo, threadInfo, lib, func) {
    614         this.eventInfo = eventInfo;
    615         this.processInfo = processInfo;
    616         this.threadInfo = threadInfo;
    617         this.lib = lib;
    618         this.func = func;
    619         this.selectorView = null;
    620         this.callgraphView = null;
    621         this.reverseCallgraphView = null;
    622         this.sourceCodeView = null;
    623         this.disassemblyView = null;
    624         this.draw();
    625         gTabs.setActive(this);
    626     }
    627 
    628     draw() {
    629         if (!this.func) {
    630             return;
    631         }
    632         this.div.empty();
    633         this._drawTitle();
    634 
    635         this.selectorView = new FunctionSampleWeightSelectorView(this.div, this.eventInfo,
    636             this.processInfo, this.threadInfo, () => this.onSampleWeightChange());
    637         this.selectorView.draw();
    638 
    639         this.div.append(getHtml('hr'));
    640         let funcName = getFuncName(this.func.g.f);
    641         this.div.append(getHtml('b', {text: `Functions called by ${funcName}`}) + '<br/>');
    642         this.callgraphView = new FlameGraphView(this.div, this.func.g, false);
    643 
    644         this.div.append(getHtml('hr'));
    645         this.div.append(getHtml('b', {text: `Functions calling ${funcName}`}) + '<br/>');
    646         this.reverseCallgraphView = new FlameGraphView(this.div, this.func.rg, true);
    647 
    648         let sourceFiles = collectSourceFilesForFunction(this.func);
    649         if (sourceFiles) {
    650             this.div.append(getHtml('hr'));
    651             this.div.append(getHtml('b', {text: 'SourceCode:'}) + '<br/>');
    652             this.sourceCodeView = new SourceCodeView(this.div, sourceFiles);
    653         }
    654 
    655         let disassembly = collectDisassemblyForFunction(this.func);
    656         if (disassembly) {
    657             this.div.append(getHtml('hr'));
    658             this.div.append(getHtml('b', {text: 'Disassembly:'}) + '<br/>');
    659             this.disassemblyView = new DisassemblyView(this.div, disassembly);
    660         }
    661 
    662         this.onSampleWeightChange();  // Manually set sample weight function for the first time.
    663     }
    664 
    665     _drawTitle() {
    666         let eventName = this.eventInfo.eventName;
    667         let processName = getProcessName(this.processInfo.pid);
    668         let threadName = getThreadName(this.threadInfo.tid);
    669         let libName = getLibName(this.lib.libId);
    670         let funcName = getFuncName(this.func.g.f);
    671         // Draw a table of 'Name', 'Value'.
    672         let rows = [];
    673         rows.push(['Event Type', eventName]);
    674         rows.push(['Process', processName]);
    675         rows.push(['Thread', threadName]);
    676         rows.push(['Library', libName]);
    677         rows.push(['Function', getHtml('pre', {text: funcName})]);
    678         let data = new google.visualization.DataTable();
    679         data.addColumn('string', '');
    680         data.addColumn('string', '');
    681         data.addRows(rows);
    682         for (let i = 0; i < rows.length; ++i) {
    683             data.setProperty(i, 0, 'className', 'boldTableCell');
    684         }
    685         let wrapperDiv = $('<div>');
    686         wrapperDiv.appendTo(this.div);
    687         let table = new google.visualization.Table(wrapperDiv.get(0));
    688         table.draw(data, {
    689             width: '100%',
    690             sort: 'disable',
    691             allowHtml: true,
    692             cssClassNames: {
    693                 'tableCell': 'tableCell',
    694             },
    695         });
    696     }
    697 
    698     onSampleWeightChange() {
    699         let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
    700         if (this.callgraphView) {
    701             this.callgraphView.draw(sampleWeightFunction);
    702         }
    703         if (this.reverseCallgraphView) {
    704             this.reverseCallgraphView.draw(sampleWeightFunction);
    705         }
    706         if (this.sourceCodeView) {
    707             this.sourceCodeView.draw(sampleWeightFunction);
    708         }
    709         if (this.disassemblyView) {
    710             this.disassemblyView.draw(sampleWeightFunction);
    711         }
    712     }
    713 }
    714 
    715 
    716 // Select the way to show sample weight in FunctionTab.
    717 // 1. Show percentage of event count relative to all processes.
    718 // 2. Show percentage of event count relative to the current process.
    719 // 3. Show percentage of event count relative to the current thread.
    720 // 4. Show absolute event count.
    721 // 5. Show event count in milliseconds, only possible for cpu-clock or task-clock events.
    722 class FunctionSampleWeightSelectorView {
    723     constructor(divContainer, eventInfo, processInfo, threadInfo, onSelectChange) {
    724         this.div = $('<div>');
    725         this.div.appendTo(divContainer);
    726         this.onSelectChange = onSelectChange;
    727         this.eventCountForAllProcesses = eventInfo.eventCount;
    728         this.eventCountForProcess = processInfo.eventCount;
    729         this.eventCountForThread = threadInfo.eventCount;
    730         this.options = {
    731             PERCENT_TO_ALL_PROCESSES: 0,
    732             PERCENT_TO_CUR_PROCESS: 1,
    733             PERCENT_TO_CUR_THREAD: 2,
    734             RAW_EVENT_COUNT: 3,
    735             EVENT_COUNT_IN_TIME: 4,
    736         };
    737         let name = eventInfo.eventName;
    738         this.supportEventCountInTime = isClockEvent(eventInfo);
    739         if (this.supportEventCountInTime) {
    740             this.curOption = this.options.EVENT_COUNT_IN_TIME;
    741         } else {
    742             this.curOption = this.options.PERCENT_TO_CUR_THREAD;
    743         }
    744     }
    745 
    746     draw() {
    747         let options = [];
    748         options.push('Show percentage of event count relative to all processes.');
    749         options.push('Show percentage of event count relative to the current process.');
    750         options.push('Show percentage of event count relative to the current thread.');
    751         options.push('Show event count.');
    752         if (this.supportEventCountInTime) {
    753             options.push('Show event count in milliseconds.');
    754         }
    755         let optionStr = '';
    756         for (let i = 0; i < options.length; ++i) {
    757             optionStr += getHtml('option', {value: i, text: options[i]});
    758         }
    759         this.div.append(getHtml('select', {text: optionStr}));
    760         let selectMenu = this.div.children().last();
    761         selectMenu.children().eq(this.curOption).attr('selected', 'selected');
    762         let thisObj = this;
    763         selectMenu.selectmenu({
    764             change: function() {
    765                 thisObj.curOption = this.value;
    766                 thisObj.onSelectChange();
    767             },
    768             width: '100%',
    769         });
    770     }
    771 
    772     getSampleWeightFunction() {
    773         let thisObj = this;
    774         if (this.curOption == this.options.PERCENT_TO_ALL_PROCESSES) {
    775             return function(eventCount) {
    776                 let percent = eventCount * 100.0 / thisObj.eventCountForAllProcesses;
    777                 return percent.toFixed(2) + '%';
    778             };
    779         }
    780         if (this.curOption == this.options.PERCENT_TO_CUR_PROCESS) {
    781             return function(eventCount) {
    782                 let percent = eventCount * 100.0 / thisObj.eventCountForProcess;
    783                 return percent.toFixed(2) + '%';
    784             };
    785         }
    786         if (this.curOption == this.options.PERCENT_TO_CUR_THREAD) {
    787             return function(eventCount) {
    788                 let percent = eventCount * 100.0 / thisObj.eventCountForThread;
    789                 return percent.toFixed(2) + '%';
    790             };
    791         }
    792         if (this.curOption == this.options.RAW_EVENT_COUNT) {
    793             return function(eventCount) {
    794                 return '' + eventCount;
    795             };
    796         }
    797         if (this.curOption == this.options.EVENT_COUNT_IN_TIME) {
    798             return function(eventCount) {
    799                 let timeInMs = eventCount / 1000000.0;
    800                 return timeInMs.toFixed(3) + ' ms';
    801             };
    802         }
    803     }
    804 }
    805 
    806 
    807 // Given a callgraph, show the flamegraph.
    808 class FlameGraphView {
    809     // If reverseOrder is false, the root of the flamegraph is at the bottom,
    810     // otherwise it is at the top.
    811     constructor(divContainer, callgraph, reverseOrder) {
    812         this.id = divContainer.children().length;
    813         this.div = $('<div>', {id: 'fg_' + this.id});
    814         this.div.appendTo(divContainer);
    815         this.callgraph = callgraph;
    816         this.reverseOrder = reverseOrder;
    817         this.sampleWeightFunction = null;
    818         this.svgWidth = $(window).width();
    819         this.svgNodeHeight = 17;
    820         this.fontSize = 12;
    821 
    822         function getMaxDepth(node) {
    823             let depth = 0;
    824             for (let child of node.c) {
    825                 depth = Math.max(depth, getMaxDepth(child));
    826             }
    827             return depth + 1;
    828         }
    829         this.maxDepth = getMaxDepth(this.callgraph);
    830         this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3);
    831     }
    832 
    833     draw(sampleWeightFunction) {
    834         this.sampleWeightFunction = sampleWeightFunction;
    835         this.div.empty();
    836         this.div.css('width', '100%').css('height', this.svgHeight + 'px');
    837         let svgStr = '<svg xmlns="http://www.w3.org/2000/svg" \
    838         xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" \
    839         width="100%" height="100%" style="border: 1px solid black; font-family: Monospace;"> \
    840         </svg>';
    841         this.div.append(svgStr);
    842         this.svg = this.div.find('svg');
    843         this._renderBackground();
    844         this._renderSvgNodes(this.callgraph, 0, 0);
    845         this._renderUnzoomNode();
    846         this._renderInfoNode();
    847         this._renderPercentNode();
    848         // Make the added nodes in the svg visible.
    849         this.div.html(this.div.html());
    850         this.svg = this.div.find('svg');
    851         this._adjustTextSize();
    852         this._enableZoom();
    853         this._enableInfo();
    854         this._adjustTextSizeOnResize();
    855     }
    856 
    857     _renderBackground() {
    858         this.svg.append(`<defs > <linearGradient id="background_gradient_${this.id}"
    859                                   y1="0" y2="1" x1="0" x2="0" > \
    860                                   <stop stop-color="#eeeeee" offset="5%" /> \
    861                                   <stop stop-color="#efefb1" offset="90%" /> \
    862                                   </linearGradient> \
    863                          </defs> \
    864                          <rect x="0" y="0" width="100%" height="100%" \
    865                            fill="url(#background_gradient_${this.id})" />`);
    866     }
    867 
    868     _getYForDepth(depth) {
    869         if (this.reverseOrder) {
    870             return (depth + 3) * this.svgNodeHeight;
    871         }
    872         return this.svgHeight - (depth + 1) * this.svgNodeHeight;
    873     }
    874 
    875     _getWidthPercentage(eventCount) {
    876         return eventCount * 100.0 / this.callgraph.s;
    877     }
    878 
    879     _getHeatColor(widthPercentage) {
    880         return {
    881             r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)),
    882             g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)),
    883             b: 100,
    884         };
    885     }
    886 
    887     _renderSvgNodes(callNode, depth, xOffset) {
    888         let x = xOffset;
    889         let y = this._getYForDepth(depth);
    890         let width = this._getWidthPercentage(callNode.s);
    891         if (width < 0.1) {
    892             return xOffset;
    893         }
    894         let color = this._getHeatColor(width);
    895         let borderColor = {};
    896         for (let key in color) {
    897             borderColor[key] = Math.max(0, color[key] - 50);
    898         }
    899         let funcName = getFuncName(callNode.f);
    900         let libName = getLibNameOfFunction(callNode.f);
    901         let sampleWeight = this.sampleWeightFunction(callNode.s);
    902         let title = funcName + ' | ' + libName + ' (' + callNode.s + ' events: ' +
    903                     sampleWeight + ')';
    904         this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \
    905                         depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \
    906                         ofill="rgb(${color.r},${color.g},${color.b})" \
    907                         fill="rgb(${color.r},${color.g},${color.b})" \
    908                         style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \
    909                         <text x="${x}%" y="${y + 12}" font-size="${this.fontSize}" \
    910                         font-family="Monospace"></text></g>`);
    911 
    912         let childXOffset = xOffset;
    913         for (let child of callNode.c) {
    914             childXOffset = this._renderSvgNodes(child, depth + 1, childXOffset);
    915         }
    916         return xOffset + width;
    917     }
    918 
    919     _renderUnzoomNode() {
    920         this.svg.append(`<rect id="zoom_rect_${this.id}" style="display:none;stroke:rgb(0,0,0);" \
    921         rx="10" ry="10" x="10" y="10" width="80" height="30" \
    922         fill="rgb(255,255,255)"/> \
    923          <text id="zoom_text_${this.id}" x="19" y="30" style="display:none">Zoom out</text>`);
    924     }
    925 
    926     _renderInfoNode() {
    927         this.svg.append(`<clipPath id="info_clip_path_${this.id}"> \
    928                          <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \
    929                          width="789" height="30" fill="rgb(255,255,255)"/> \
    930                          </clipPath> \
    931                          <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \
    932                          width="799" height="30" fill="rgb(255,255,255)"/> \
    933                          <text clip-path="url(#info_clip_path_${this.id})" \
    934                          id="info_text_${this.id}" x="128" y="30"></text>`);
    935     }
    936 
    937     _renderPercentNode() {
    938         this.svg.append(`<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" \
    939                          x="934" y="10" width="150" height="30" \
    940                          fill="rgb(255,255,255)"/> \
    941                          <text id="percent_text_${this.id}" text-anchor="end" \
    942                          x="1074" y="30"></text>`);
    943     }
    944 
    945     _adjustTextSizeForNode(g) {
    946         let text = g.find('text');
    947         let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01;
    948         if (width < 28) {
    949             text.text('');
    950             return;
    951         }
    952         let methodName = g.find('title').text().split(' | ')[0];
    953         let numCharacters;
    954         for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) {
    955             if (numCharacters * 7.5 <= width) {
    956                 break;
    957             }
    958         }
    959         if (numCharacters == methodName.length) {
    960             text.text(methodName);
    961         } else {
    962             text.text(methodName.substring(0, numCharacters - 2) + '..');
    963         }
    964     }
    965 
    966     _adjustTextSize() {
    967         this.svgWidth = $(window).width();
    968         let thisObj = this;
    969         this.svg.find('g').each(function(_, g) {
    970             thisObj._adjustTextSizeForNode($(g));
    971         });
    972     }
    973 
    974     _enableZoom() {
    975         this.zoomStack = [this.svg.find('g').first().get(0)];
    976         this.svg.find('g').css('cursor', 'pointer').click(zoom);
    977         this.svg.find(`#zoom_rect_${this.id}`).css('cursor', 'pointer').click(unzoom);
    978         this.svg.find(`#zoom_text_${this.id}`).css('cursor', 'pointer').click(unzoom);
    979 
    980         let thisObj = this;
    981         function zoom() {
    982             thisObj.zoomStack.push(this);
    983             displayFromElement(this);
    984             thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'block');
    985             thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'block');
    986         }
    987 
    988         function unzoom() {
    989             if (thisObj.zoomStack.length > 1) {
    990                 thisObj.zoomStack.pop();
    991                 displayFromElement(thisObj.zoomStack[thisObj.zoomStack.length - 1]);
    992                 if (thisObj.zoomStack.length == 1) {
    993                     thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'none');
    994                     thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'none');
    995                 }
    996             }
    997         }
    998 
    999         function displayFromElement(g) {
   1000             g = $(g);
   1001             let clickedRect = g.find('rect');
   1002             let clickedOriginX = parseFloat(clickedRect.attr('ox'));
   1003             let clickedDepth = parseInt(clickedRect.attr('depth'));
   1004             let clickedOriginWidth = parseFloat(clickedRect.attr('owidth'));
   1005             let scaleFactor = 100.0 / clickedOriginWidth;
   1006             thisObj.svg.find('g').each(function(_, g) {
   1007                 g = $(g);
   1008                 let text = g.find('text');
   1009                 let rect = g.find('rect');
   1010                 let depth = parseInt(rect.attr('depth'));
   1011                 let ox = parseFloat(rect.attr('ox'));
   1012                 let owidth = parseFloat(rect.attr('owidth'));
   1013                 if (depth < clickedDepth || ox < clickedOriginX - 1e-9 ||
   1014                     ox + owidth > clickedOriginX + clickedOriginWidth + 1e-9) {
   1015                     rect.css('display', 'none');
   1016                     text.css('display', 'none');
   1017                 } else {
   1018                     rect.css('display', 'block');
   1019                     text.css('display', 'block');
   1020                     let nx = (ox - clickedOriginX) * scaleFactor + '%';
   1021                     let ny = thisObj._getYForDepth(depth - clickedDepth);
   1022                     rect.attr('x', nx);
   1023                     rect.attr('y', ny);
   1024                     rect.attr('width', owidth * scaleFactor + '%');
   1025                     text.attr('x', nx);
   1026                     text.attr('y', ny + 12);
   1027                     thisObj._adjustTextSizeForNode(g);
   1028                 }
   1029             });
   1030         }
   1031     }
   1032 
   1033     _enableInfo() {
   1034         this.selected = null;
   1035         let thisObj = this;
   1036         this.svg.find('g').on('mouseenter', function() {
   1037             if (thisObj.selected) {
   1038                 thisObj.selected.css('stroke-width', '0');
   1039             }
   1040             // Mark current node.
   1041             let g = $(this);
   1042             thisObj.selected = g;
   1043             g.css('stroke', 'black').css('stroke-width', '0.5');
   1044 
   1045             // Parse title.
   1046             let title = g.find('title').text();
   1047             let methodAndInfo = title.split(' | ');
   1048             thisObj.svg.find(`#info_text_${thisObj.id}`).text(methodAndInfo[0]);
   1049 
   1050             // Parse percentage.
   1051             // '/system/lib64/libhwbinder.so (4 events: 0.28%)'
   1052             let regexp = /.* \(.*:\s+(.*)\)/g;
   1053             let match = regexp.exec(methodAndInfo[1]);
   1054             let percentage = '';
   1055             if (match && match.length > 1) {
   1056                 percentage = match[1];
   1057             }
   1058             thisObj.svg.find(`#percent_text_${thisObj.id}`).text(percentage);
   1059         });
   1060     }
   1061 
   1062     _adjustTextSizeOnResize() {
   1063         function throttle(callback) {
   1064             let running = false;
   1065             return function() {
   1066                 if (!running) {
   1067                     running = true;
   1068                     window.requestAnimationFrame(function () {
   1069                         callback();
   1070                         running = false;
   1071                     });
   1072                 }
   1073             };
   1074         }
   1075         $(window).resize(throttle(() => this._adjustTextSize()));
   1076     }
   1077 }
   1078 
   1079 
   1080 class SourceFile {
   1081 
   1082     constructor(fileId) {
   1083         this.path = getSourceFilePath(fileId);
   1084         this.code = getSourceCode(fileId);
   1085         this.showLines = {};  // map from line number to {eventCount, subtreeEventCount}.
   1086         this.hasCount = false;
   1087     }
   1088 
   1089     addLineRange(startLine, endLine) {
   1090         for (let i = startLine; i <= endLine; ++i) {
   1091             if (i in this.showLines || !(i in this.code)) {
   1092                 continue;
   1093             }
   1094             this.showLines[i] = {eventCount: 0, subtreeEventCount: 0};
   1095         }
   1096     }
   1097 
   1098     addLineCount(lineNumber, eventCount, subtreeEventCount) {
   1099         let line = this.showLines[lineNumber];
   1100         if (line) {
   1101             line.eventCount += eventCount;
   1102             line.subtreeEventCount += subtreeEventCount;
   1103             this.hasCount = true;
   1104         }
   1105     }
   1106 }
   1107 
   1108 // Return a list of SourceFile related to a function.
   1109 function collectSourceFilesForFunction(func) {
   1110     if (!func.hasOwnProperty('s')) {
   1111         return null;
   1112     }
   1113     let hitLines = func.s;
   1114     let sourceFiles = {};  // map from sourceFileId to SourceFile.
   1115 
   1116     function getFile(fileId) {
   1117         let file = sourceFiles[fileId];
   1118         if (!file) {
   1119             file = sourceFiles[fileId] = new SourceFile(fileId);
   1120         }
   1121         return file;
   1122     }
   1123 
   1124     // Show lines for the function.
   1125     let funcRange = getFuncSourceRange(func.g.f);
   1126     if (funcRange) {
   1127         let file = getFile(funcRange.fileId);
   1128         file.addLineRange(funcRange.startLine);
   1129     }
   1130 
   1131     // Show lines for hitLines.
   1132     for (let hitLine of hitLines) {
   1133         let file = getFile(hitLine.f);
   1134         file.addLineRange(hitLine.l - 5, hitLine.l + 5);
   1135         file.addLineCount(hitLine.l, hitLine.e, hitLine.s);
   1136     }
   1137 
   1138     let result = [];
   1139     // Show the source file containing the function before other source files.
   1140     if (funcRange) {
   1141         let file = getFile(funcRange.fileId);
   1142         if (file.hasCount) {
   1143             result.push(file);
   1144         }
   1145         delete sourceFiles[funcRange.fileId];
   1146     }
   1147     for (let fileId in sourceFiles) {
   1148         let file = sourceFiles[fileId];
   1149         if (file.hasCount) {
   1150             result.push(file);
   1151         }
   1152     }
   1153     return result.length > 0 ? result : null;
   1154 }
   1155 
   1156 // Show annotated source code of a function.
   1157 class SourceCodeView {
   1158 
   1159     constructor(divContainer, sourceFiles) {
   1160         this.div = $('<div>');
   1161         this.div.appendTo(divContainer);
   1162         this.sourceFiles = sourceFiles;
   1163     }
   1164 
   1165     draw(sampleWeightFunction) {
   1166         google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction));
   1167     }
   1168 
   1169     realDraw(sampleWeightFunction) {
   1170         this.div.empty();
   1171         // For each file, draw a table of 'Line', 'Total', 'Self', 'Code'.
   1172         for (let sourceFile of this.sourceFiles) {
   1173             let rows = [];
   1174             let lineNumbers = Object.keys(sourceFile.showLines);
   1175             lineNumbers.sort((a, b) => a - b);
   1176             for (let lineNumber of lineNumbers) {
   1177                 let code = getHtml('pre', {text: sourceFile.code[lineNumber]});
   1178                 let countInfo = sourceFile.showLines[lineNumber];
   1179                 let totalValue = '';
   1180                 let selfValue = '';
   1181                 if (countInfo.subtreeEventCount != 0) {
   1182                     totalValue = sampleWeightFunction(countInfo.subtreeEventCount);
   1183                     selfValue = sampleWeightFunction(countInfo.eventCount);
   1184                 }
   1185                 rows.push([lineNumber, totalValue, selfValue, code]);
   1186             }
   1187 
   1188             let data = new google.visualization.DataTable();
   1189             data.addColumn('string', 'Line');
   1190             data.addColumn('string', 'Total');
   1191             data.addColumn('string', 'Self');
   1192             data.addColumn('string', 'Code');
   1193             data.addRows(rows);
   1194             for (let i = 0; i < rows.length; ++i) {
   1195                 data.setProperty(i, 0, 'className', 'colForLine');
   1196                 for (let j = 1; j <= 2; ++j) {
   1197                     data.setProperty(i, j, 'className', 'colForCount');
   1198                 }
   1199             }
   1200             this.div.append(getHtml('pre', {text: sourceFile.path}));
   1201             let wrapperDiv = $('<div>');
   1202             wrapperDiv.appendTo(this.div);
   1203             let table = new google.visualization.Table(wrapperDiv.get(0));
   1204             table.draw(data, {
   1205                 width: '100%',
   1206                 sort: 'disable',
   1207                 frozenColumns: 3,
   1208                 allowHtml: true,
   1209             });
   1210         }
   1211     }
   1212 }
   1213 
   1214 // Return a list of disassembly related to a function.
   1215 function collectDisassemblyForFunction(func) {
   1216     if (!func.hasOwnProperty('a')) {
   1217         return null;
   1218     }
   1219     let hitAddrs = func.a;
   1220     let rawCode = getFuncDisassembly(func.g.f);
   1221     if (!rawCode) {
   1222         return null;
   1223     }
   1224 
   1225     // Annotate disassembly with event count information.
   1226     let annotatedCode = [];
   1227     let codeForLastAddr = null;
   1228     let hitAddrPos = 0;
   1229     let hasCount = false;
   1230 
   1231     function addEventCount(addr) {
   1232         while (hitAddrPos < hitAddrs.length && hitAddrs[hitAddrPos].a < addr) {
   1233             if (codeForLastAddr) {
   1234                 codeForLastAddr.eventCount += hitAddrs[hitAddrPos].e;
   1235                 codeForLastAddr.subtreeEventCount += hitAddrs[hitAddrPos].s;
   1236                 hasCount = true;
   1237             }
   1238             hitAddrPos++;
   1239         }
   1240     }
   1241 
   1242     for (let line of rawCode) {
   1243         let code = line[0];
   1244         let addr = line[1];
   1245 
   1246         addEventCount(addr);
   1247         let item = {code: code, eventCount: 0, subtreeEventCount: 0};
   1248         annotatedCode.push(item);
   1249         // Objdump sets addr to 0 when a disassembly line is not associated with an addr.
   1250         if (addr != 0) {
   1251             codeForLastAddr = item;
   1252         }
   1253     }
   1254     addEventCount(Number.MAX_VALUE);
   1255     return hasCount ? annotatedCode : null;
   1256 }
   1257 
   1258 // Show annotated disassembly of a function.
   1259 class DisassemblyView {
   1260 
   1261     constructor(divContainer, disassembly) {
   1262         this.div = $('<div>');
   1263         this.div.appendTo(divContainer);
   1264         this.disassembly = disassembly;
   1265     }
   1266 
   1267     draw(sampleWeightFunction) {
   1268         google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction));
   1269     }
   1270 
   1271     realDraw(sampleWeightFunction) {
   1272         this.div.empty();
   1273         // Draw a table of 'Total', 'Self', 'Code'.
   1274         let rows = [];
   1275         for (let line of this.disassembly) {
   1276             let code = getHtml('pre', {text: line.code});
   1277             let totalValue = '';
   1278             let selfValue = '';
   1279             if (line.subtreeEventCount != 0) {
   1280                 totalValue = sampleWeightFunction(line.subtreeEventCount);
   1281                 selfValue = sampleWeightFunction(line.eventCount);
   1282             }
   1283             rows.push([totalValue, selfValue, code]);
   1284         }
   1285         let data = new google.visualization.DataTable();
   1286         data.addColumn('string', 'Total');
   1287         data.addColumn('string', 'Self');
   1288         data.addColumn('string', 'Code');
   1289         data.addRows(rows);
   1290         for (let i = 0; i < rows.length; ++i) {
   1291             for (let j = 0; j < 2; ++j) {
   1292                 data.setProperty(i, j, 'className', 'colForCount');
   1293             }
   1294         }
   1295         let wrapperDiv = $('<div>');
   1296         wrapperDiv.appendTo(this.div);
   1297         let table = new google.visualization.Table(wrapperDiv.get(0));
   1298         table.draw(data, {
   1299             width: '100%',
   1300             sort: 'disable',
   1301             frozenColumns: 2,
   1302             allowHtml: true,
   1303         });
   1304     }
   1305 }
   1306 
   1307 
   1308 function initGlobalObjects() {
   1309     gTabs = new TabManager($('div#report_content'));
   1310     let recordData = $('#record_data').text();
   1311     gRecordInfo = JSON.parse(recordData);
   1312     gProcesses = gRecordInfo.processNames;
   1313     gThreads = gRecordInfo.threadNames;
   1314     gLibList = gRecordInfo.libList;
   1315     gFunctionMap = gRecordInfo.functionMap;
   1316     gSampleInfo = gRecordInfo.sampleInfo;
   1317     gSourceFiles = gRecordInfo.sourceFiles;
   1318 }
   1319 
   1320 function createTabs() {
   1321     gTabs.addTab('Chart Statistics', new ChartStatTab());
   1322     gTabs.addTab('Sample Table', new SampleTableTab());
   1323     gTabs.addTab('Flamegraph', new FlameGraphTab());
   1324     gTabs.draw();
   1325 }
   1326 
   1327 let gTabs;
   1328 let gRecordInfo;
   1329 let gProcesses;
   1330 let gThreads;
   1331 let gLibList;
   1332 let gFunctionMap;
   1333 let gSampleInfo;
   1334 let gSourceFiles;
   1335 
   1336 initGlobalObjects();
   1337 createTabs();
   1338 
   1339 });