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(function () {
20
21function getTimeInMs() {
22    return new Date().getTime();
23}
24
25class TimeLog {
26    constructor() {
27        this.start = getTimeInMs();
28    }
29
30    log(name) {
31        let end = getTimeInMs();
32        console.log(name, end - this.start, 'ms');
33        this.start = end;
34    }
35}
36
37class ProgressBar {
38    constructor() {
39        let str = `
40            <div class="modal" tabindex="-1" role="dialog">
41                <div class="modal-dialog" role="document">
42                    <div class="modal-content">
43                        <div class="modal-header"><h5 class="modal-title">Loading page...</h5></div>
44                        <div class="modal-body">
45                            <div class="progress">
46                                <div class="progress-bar" role="progressbar"
47                                    style="width: 0%" aria-valuenow="0" aria-valuemin="0"
48                                    aria-valuemax="100">0%</div>
49                            </div>
50                        </div>
51                    </div>
52                </div>
53            </div>
54        `;
55        this.modal = $(str).appendTo($('body'));
56        this.progress = 0;
57        this.shownCallback = null;
58        this.modal.on('shown.bs.modal', () => this._onShown());
59        // Shorten progress bar update time.
60        this.modal.find('.progress-bar').css('transition-duration', '0ms');
61        this.shown = false;
62    }
63
64    // progress is [0-100]. Return a Promise resolved when the update is shown.
65    updateAsync(text, progress) {
66        progress = parseInt(progress);  // Truncate float number to integer.
67        return this.showAsync().then(() => {
68            if (text) {
69                this.modal.find('.modal-title').text(text);
70            }
71            this.progress = progress;
72            this.modal.find('.progress-bar').css('width', progress + '%')
73                    .attr('aria-valuenow', progress).text(progress + '%');
74            // Leave 100ms for the progess bar to update.
75            return createPromise((resolve) => setTimeout(resolve, 100));
76        });
77    }
78
79    showAsync() {
80        if (this.shown) {
81            return createPromise();
82        }
83        return createPromise((resolve) => {
84            this.shownCallback = resolve;
85            this.modal.modal({
86                show: true,
87                keyboard: false,
88                backdrop: false,
89            });
90        });
91    }
92
93    _onShown() {
94        this.shown = true;
95        if (this.shownCallback) {
96            let callback = this.shownCallback;
97            this.shownCallback = null;
98            callback();
99        }
100    }
101
102    hide() {
103        this.shown = false;
104        this.modal.modal('hide');
105    }
106}
107
108function openHtml(name, attrs={}) {
109    let s = `<${name} `;
110    for (let key in attrs) {
111        s += `${key}="${attrs[key]}" `;
112    }
113    s += '>';
114    return s;
115}
116
117function closeHtml(name) {
118    return `</${name}>`;
119}
120
121function getHtml(name, attrs={}) {
122    let text;
123    if ('text' in attrs) {
124        text = attrs.text;
125        delete attrs.text;
126    }
127    let s = openHtml(name, attrs);
128    if (text) {
129        s += text;
130    }
131    s += closeHtml(name);
132    return s;
133}
134
135function getTableRow(cols, colName, attrs={}) {
136    let s = openHtml('tr', attrs);
137    for (let col of cols) {
138        s += `<${colName}>${col}</${colName}>`;
139    }
140    s += '</tr>';
141    return s;
142}
143
144function getProcessName(pid) {
145    let name = gProcesses[pid];
146    return name ? `${pid} (${name})`: pid.toString();
147}
148
149function getThreadName(tid) {
150    let name = gThreads[tid];
151    return name ? `${tid} (${name})`: tid.toString();
152}
153
154function getLibName(libId) {
155    return gLibList[libId];
156}
157
158function getFuncName(funcId) {
159    return gFunctionMap[funcId].f;
160}
161
162function getLibNameOfFunction(funcId) {
163    return getLibName(gFunctionMap[funcId].l);
164}
165
166function getFuncSourceRange(funcId) {
167    let func = gFunctionMap[funcId];
168    if (func.hasOwnProperty('s')) {
169        return {fileId: func.s[0], startLine: func.s[1], endLine: func.s[2]};
170    }
171    return null;
172}
173
174function getFuncDisassembly(funcId) {
175    let func = gFunctionMap[funcId];
176    return func.hasOwnProperty('d') ? func.d : null;
177}
178
179function getSourceFilePath(sourceFileId) {
180    return gSourceFiles[sourceFileId].path;
181}
182
183function getSourceCode(sourceFileId) {
184    return gSourceFiles[sourceFileId].code;
185}
186
187function isClockEvent(eventInfo) {
188    return eventInfo.eventName.includes('task-clock') ||
189            eventInfo.eventName.includes('cpu-clock');
190}
191
192let createId = function() {
193    let currentId = 0;
194    return () => `id${++currentId}`;
195}();
196
197class TabManager {
198    constructor(divContainer) {
199        let id = createId();
200        divContainer.append(`<ul class="nav nav-pills mb-3 mt-3 ml-3" id="${id}" role="tablist">
201            </ul><hr/><div class="tab-content" id="${id}Content"></div>`);
202        this.ul = divContainer.find(`#${id}`);
203        this.content = divContainer.find(`#${id}Content`);
204        // Map from title to [tabObj, drawn=false|true].
205        this.tabs = new Map();
206        this.tabActiveCallback = null;
207    }
208
209    addTab(title, tabObj) {
210        let id = createId();
211        this.content.append(`<div class="tab-pane" id="${id}" role="tabpanel"
212            aria-labelledby="${id}-tab"></div>`);
213        this.ul.append(`
214            <li class="nav-item">
215                <a class="nav-link" id="${id}-tab" data-toggle="pill" href="#${id}" role="tab"
216                    aria-controls="${id}" aria-selected="false">${title}</a>
217            </li>`);
218        tabObj.init(this.content.find(`#${id}`));
219        this.tabs.set(title, [tabObj, false]);
220        this.ul.find(`#${id}-tab`).on('shown.bs.tab', () => this.onTabActive(title));
221        return tabObj;
222    }
223
224    setActiveAsync(title) {
225        let tabObj = this.findTab(title);
226        return createPromise((resolve) => {
227            this.tabActiveCallback = resolve;
228            let id = tabObj.div.attr('id') + '-tab';
229            this.ul.find(`#${id}`).tab('show');
230        });
231    }
232
233    onTabActive(title) {
234        let array = this.tabs.get(title);
235        let tabObj = array[0];
236        let drawn = array[1];
237        if (!drawn) {
238            tabObj.draw();
239            array[1] = true;
240        }
241        if (this.tabActiveCallback) {
242            let callback = this.tabActiveCallback;
243            this.tabActiveCallback = null;
244            callback();
245        }
246    }
247
248    findTab(title) {
249        let array = this.tabs.get(title);
250        return array ? array[0] : null;
251    }
252}
253
254function createEventTabs(id) {
255    let ul = `<ul class="nav nav-pills mb-3 mt-3 ml-3" id="${id}" role="tablist">`;
256    let content = `<div class="tab-content" id="${id}Content">`;
257    for (let i = 0; i < gSampleInfo.length; ++i) {
258        let subId = id + '_' + i;
259        let title = gSampleInfo[i].eventName;
260        ul += `
261            <li class="nav-item">
262                <a class="nav-link" id="${subId}-tab" data-toggle="pill" href="#${subId}" role="tab"
263                aria-controls="${subId}" aria-selected="${i == 0 ? "true" : "false"}">${title}</a>
264            </li>`;
265        content += `
266            <div class="tab-pane" id="${subId}" role="tabpanel" aria-labelledby="${subId}-tab">
267            </div>`;
268    }
269    ul += '</ul>';
270    content += '</div>';
271    return ul + content;
272}
273
274function createViewsForEvents(div, createViewCallback) {
275    let views = [];
276    if (gSampleInfo.length == 1) {
277        views.push(createViewCallback(div, gSampleInfo[0]));
278    } else if (gSampleInfo.length > 1) {
279        // If more than one event, draw them in tabs.
280        let id = createId();
281        div.append(createEventTabs(id));
282        for (let i = 0; i < gSampleInfo.length; ++i) {
283            let subId = id + '_' + i;
284            views.push(createViewCallback(div.find(`#${subId}`), gSampleInfo[i]));
285        }
286        div.find(`#${id}_0-tab`).tab('show');
287    }
288    return views;
289}
290
291// Return a promise to draw views.
292function drawViewsAsync(views, totalProgress, drawViewCallback) {
293    if (views.length == 0) {
294        return createPromise();
295    }
296    let drawPos = 0;
297    let eachProgress = totalProgress / views.length;
298    function drawAsync() {
299        if (drawPos == views.length) {
300            return createPromise();
301        }
302        return drawViewCallback(views[drawPos++], eachProgress).then(drawAsync);
303    }
304    return drawAsync();
305}
306
307// Show global information retrieved from the record file, including:
308//   record time
309//   machine type
310//   Android version
311//   record cmdline
312//   total samples
313class RecordFileView {
314    constructor(divContainer) {
315        this.div = $('<div>');
316        this.div.appendTo(divContainer);
317    }
318
319    draw() {
320        google.charts.setOnLoadCallback(() => this.realDraw());
321    }
322
323    realDraw() {
324        this.div.empty();
325        // Draw a table of 'Name', 'Value'.
326        let rows = [];
327        if (gRecordInfo.recordTime) {
328            rows.push(['Record Time', gRecordInfo.recordTime]);
329        }
330        if (gRecordInfo.machineType) {
331            rows.push(['Machine Type', gRecordInfo.machineType]);
332        }
333        if (gRecordInfo.androidVersion) {
334            rows.push(['Android Version', gRecordInfo.androidVersion]);
335        }
336        if (gRecordInfo.recordCmdline) {
337            rows.push(['Record cmdline', gRecordInfo.recordCmdline]);
338        }
339        rows.push(['Total Samples', '' + gRecordInfo.totalSamples]);
340
341        let data = new google.visualization.DataTable();
342        data.addColumn('string', '');
343        data.addColumn('string', '');
344        data.addRows(rows);
345        for (let i = 0; i < rows.length; ++i) {
346            data.setProperty(i, 0, 'className', 'boldTableCell');
347        }
348        let table = new google.visualization.Table(this.div.get(0));
349        table.draw(data, {
350            width: '100%',
351            sort: 'disable',
352            allowHtml: true,
353            cssClassNames: {
354                'tableCell': 'tableCell',
355            },
356        });
357    }
358}
359
360// Show pieChart of event count percentage of each process, thread, library and function.
361class ChartView {
362    constructor(divContainer, eventInfo) {
363        this.div = $('<div>').appendTo(divContainer);
364        this.eventInfo = eventInfo;
365        this.processInfo = null;
366        this.threadInfo = null;
367        this.libInfo = null;
368        this.states = {
369            SHOW_EVENT_INFO: 1,
370            SHOW_PROCESS_INFO: 2,
371            SHOW_THREAD_INFO: 3,
372            SHOW_LIB_INFO: 4,
373        };
374        if (isClockEvent(this.eventInfo)) {
375            this.getSampleWeight = function (eventCount) {
376                return (eventCount / 1000000.0).toFixed(3) + ' ms';
377            };
378        } else {
379            this.getSampleWeight = (eventCount) => '' + eventCount;
380        }
381    }
382
383    _getState() {
384        if (this.libInfo) {
385            return this.states.SHOW_LIB_INFO;
386        }
387        if (this.threadInfo) {
388            return this.states.SHOW_THREAD_INFO;
389        }
390        if (this.processInfo) {
391            return this.states.SHOW_PROCESS_INFO;
392        }
393        return this.states.SHOW_EVENT_INFO;
394    }
395
396    _goBack() {
397        let state = this._getState();
398        if (state == this.states.SHOW_PROCESS_INFO) {
399            this.processInfo = null;
400        } else if (state == this.states.SHOW_THREAD_INFO) {
401            this.threadInfo = null;
402        } else if (state == this.states.SHOW_LIB_INFO) {
403            this.libInfo = null;
404        }
405        this.draw();
406    }
407
408    _selectHandler(chart) {
409        let selectedItem = chart.getSelection()[0];
410        if (selectedItem) {
411            let state = this._getState();
412            if (state == this.states.SHOW_EVENT_INFO) {
413                this.processInfo = this.eventInfo.processes[selectedItem.row];
414            } else if (state == this.states.SHOW_PROCESS_INFO) {
415                this.threadInfo = this.processInfo.threads[selectedItem.row];
416            } else if (state == this.states.SHOW_THREAD_INFO) {
417                this.libInfo = this.threadInfo.libs[selectedItem.row];
418            }
419            this.draw();
420        }
421    }
422
423    draw() {
424        google.charts.setOnLoadCallback(() => this.realDraw());
425    }
426
427    realDraw() {
428        this.div.empty();
429        this._drawTitle();
430        this._drawPieChart();
431    }
432
433    _drawTitle() {
434        // Draw a table of 'Name', 'Event Count'.
435        let rows = [];
436        rows.push(['Event Type: ' + this.eventInfo.eventName,
437                   this.getSampleWeight(this.eventInfo.eventCount)]);
438        if (this.processInfo) {
439            rows.push(['Process: ' + getProcessName(this.processInfo.pid),
440                       this.getSampleWeight(this.processInfo.eventCount)]);
441        }
442        if (this.threadInfo) {
443            rows.push(['Thread: ' + getThreadName(this.threadInfo.tid),
444                       this.getSampleWeight(this.threadInfo.eventCount)]);
445        }
446        if (this.libInfo) {
447            rows.push(['Library: ' + getLibName(this.libInfo.libId),
448                       this.getSampleWeight(this.libInfo.eventCount)]);
449        }
450        let data = new google.visualization.DataTable();
451        data.addColumn('string', '');
452        data.addColumn('string', '');
453        data.addRows(rows);
454        for (let i = 0; i < rows.length; ++i) {
455            data.setProperty(i, 0, 'className', 'boldTableCell');
456        }
457        let wrapperDiv = $('<div>');
458        wrapperDiv.appendTo(this.div);
459        let table = new google.visualization.Table(wrapperDiv.get(0));
460        table.draw(data, {
461            width: '100%',
462            sort: 'disable',
463            allowHtml: true,
464            cssClassNames: {
465                'tableCell': 'tableCell',
466            },
467        });
468        if (this._getState() != this.states.SHOW_EVENT_INFO) {
469            $('<button type="button" class="btn btn-primary">Back</button>').appendTo(this.div)
470                .click(() => this._goBack());
471        }
472    }
473
474    _drawPieChart() {
475        let state = this._getState();
476        let title = null;
477        let firstColumn = null;
478        let rows = [];
479        let thisObj = this;
480        function getItem(name, eventCount, totalEventCount) {
481            let sampleWeight = thisObj.getSampleWeight(eventCount);
482            let percent = (eventCount * 100.0 / totalEventCount).toFixed(2) + '%';
483            return [name, eventCount, getHtml('pre', {text: name}) +
484                        getHtml('b', {text: `${sampleWeight} (${percent})`})];
485        }
486
487        if (state == this.states.SHOW_EVENT_INFO) {
488            title = 'Processes in event type ' + this.eventInfo.eventName;
489            firstColumn = 'Process';
490            for (let process of this.eventInfo.processes) {
491                rows.push(getItem('Process: ' + getProcessName(process.pid), process.eventCount,
492                                  this.eventInfo.eventCount));
493            }
494        } else if (state == this.states.SHOW_PROCESS_INFO) {
495            title = 'Threads in process ' + getProcessName(this.processInfo.pid);
496            firstColumn = 'Thread';
497            for (let thread of this.processInfo.threads) {
498                rows.push(getItem('Thread: ' + getThreadName(thread.tid), thread.eventCount,
499                                  this.processInfo.eventCount));
500            }
501        } else if (state == this.states.SHOW_THREAD_INFO) {
502            title = 'Libraries in thread ' + getThreadName(this.threadInfo.tid);
503            firstColumn = 'Library';
504            for (let lib of this.threadInfo.libs) {
505                rows.push(getItem('Library: ' + getLibName(lib.libId), lib.eventCount,
506                                  this.threadInfo.eventCount));
507            }
508        } else if (state == this.states.SHOW_LIB_INFO) {
509            title = 'Functions in library ' + getLibName(this.libInfo.libId);
510            firstColumn = 'Function';
511            for (let func of this.libInfo.functions) {
512                rows.push(getItem('Function: ' + getFuncName(func.f), func.c[1],
513                                  this.libInfo.eventCount));
514            }
515        }
516        let data = new google.visualization.DataTable();
517        data.addColumn('string', firstColumn);
518        data.addColumn('number', 'EventCount');
519        data.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
520        data.addRows(rows);
521
522        let wrapperDiv = $('<div>');
523        wrapperDiv.appendTo(this.div);
524        let chart = new google.visualization.PieChart(wrapperDiv.get(0));
525        chart.draw(data, {
526            title: title,
527            width: 1000,
528            height: 600,
529            tooltip: {isHtml: true},
530        });
531        google.visualization.events.addListener(chart, 'select', () => this._selectHandler(chart));
532    }
533}
534
535
536class ChartStatTab {
537    init(div) {
538        this.div = div;
539    }
540
541    draw() {
542        new RecordFileView(this.div).draw();
543        let views = createViewsForEvents(this.div, (div, eventInfo) => {
544            return new ChartView(div, eventInfo);
545        });
546        for (let view of views) {
547            view.draw();
548        }
549    }
550}
551
552
553class SampleTableTab {
554    init(div) {
555        this.div = div;
556    }
557
558    draw() {
559        let views = [];
560        createPromise()
561            .then(updateProgress('Draw SampleTable...', 0))
562            .then(wait(() => {
563                this.div.empty();
564                views = createViewsForEvents(this.div, (div, eventInfo) => {
565                    return new SampleTableView(div, eventInfo);
566                });
567            }))
568            .then(() => drawViewsAsync(views, 100, (view, progress) => view.drawAsync(progress)))
569            .then(hideProgress());
570    }
571}
572
573// Select the way to show sample weight in SampleTableTab.
574// 1. Show percentage of event count.
575// 2. Show event count (For cpu-clock and task-clock events, it is time in ms).
576class SampleTableWeightSelectorView {
577    constructor(divContainer, eventInfo, onSelectChange) {
578        let options = new Map();
579        options.set('percent', 'Show percentage of event count');
580        options.set('event_count', 'Show event count');
581        if (isClockEvent(eventInfo)) {
582            options.set('event_count_in_ms', 'Show event count in milliseconds');
583        }
584        let buttons = [];
585        options.forEach((value, key) => {
586            buttons.push(`<button type="button" class="dropdown-item" key="${key}">${value}
587                          </button>`);
588        });
589        this.curOption = 'percent';
590        this.eventCount = eventInfo.eventCount;
591        let id = createId();
592        let str = `
593            <div class="dropdown">
594                <button type="button" class="btn btn-primary dropdown-toggle" id="${id}"
595                    data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
596                    >${options.get(this.curOption)}</button>
597                <div class="dropdown-menu" aria-labelledby="${id}">${buttons.join('')}</div>
598            </div>
599        `;
600        divContainer.append(str);
601        divContainer.children().last().on('hidden.bs.dropdown', (e) => {
602            if (e.clickEvent) {
603                let button = $(e.clickEvent.target);
604                let newOption = button.attr('key');
605                if (newOption && this.curOption != newOption) {
606                    this.curOption = newOption;
607                    divContainer.find(`#${id}`).text(options.get(this.curOption));
608                    onSelectChange();
609                }
610            }
611        });
612    }
613
614    getSampleWeightFunction() {
615        if (this.curOption == 'percent') {
616            return (eventCount) => (eventCount * 100.0 / this.eventCount).toFixed(2) + '%';
617        }
618        if (this.curOption == 'event_count') {
619            return (eventCount) => '' + eventCount;
620        }
621        if (this.curOption == 'event_count_in_ms') {
622            return (eventCount) => (eventCount / 1000000.0).toFixed(3);
623        }
624    }
625
626    getSampleWeightSuffix() {
627        if (this.curOption == 'event_count_in_ms') {
628            return ' ms';
629        }
630        return '';
631    }
632}
633
634
635class SampleTableView {
636    constructor(divContainer, eventInfo) {
637        this.id = createId();
638        this.div = $('<div>', {id: this.id}).appendTo(divContainer);
639        this.eventInfo = eventInfo;
640        this.selectorView = null;
641        this.tableDiv = null;
642    }
643
644    drawAsync(totalProgress) {
645        return createPromise()
646            .then(wait(() => {
647                this.div.empty();
648                this.selectorView = new SampleTableWeightSelectorView(
649                    this.div, this.eventInfo, () => this.onSampleWeightChange());
650                this.tableDiv = $('<div>').appendTo(this.div);
651            }))
652            .then(() => this._drawSampleTable(totalProgress));
653    }
654
655    // Return a promise to draw SampleTable.
656    _drawSampleTable(totalProgress) {
657        let eventInfo = this.eventInfo;
658        let data = [];
659        return createPromise()
660            .then(wait(() => {
661                this.tableDiv.empty();
662                let getSampleWeight = this.selectorView.getSampleWeightFunction();
663                let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix();
664                // Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library',
665                // 'Function'.
666                let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : '';
667                let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples', 'Process',
668                              'Thread', 'Library', 'Function', 'HideKey'];
669                this.tableDiv.append(`
670                    <table cellspacing="0" class="table table-striped table-bordered"
671                        style="width:100%">
672                        <thead>${getTableRow(titles, 'th')}</thead>
673                        <tbody></tbody>
674                        <tfoot>${getTableRow(titles, 'th')}</tfoot>
675                    </table>`);
676                for (let [i, process] of eventInfo.processes.entries()) {
677                    let processName = getProcessName(process.pid);
678                    for (let [j, thread] of process.threads.entries()) {
679                        let threadName = getThreadName(thread.tid);
680                        for (let [k, lib] of thread.libs.entries()) {
681                            let libName = getLibName(lib.libId);
682                            for (let [t, func] of lib.functions.entries()) {
683                                let totalValue = getSampleWeight(func.c[2]);
684                                let selfValue = getSampleWeight(func.c[1]);
685                                let key = [i, j, k, t].join('_');
686                                data.push([totalValue, selfValue, func.c[0], processName,
687                                           threadName, libName, getFuncName(func.f), key])
688                           }
689                        }
690                    }
691                }
692            }))
693            .then(addProgress(totalProgress / 2))
694            .then(wait(() => {
695                let table = this.tableDiv.find('table');
696                let dataTable = table.DataTable({
697                    lengthMenu: [10, 20, 50, 100, -1],
698                    order: [0, 'desc'],
699                    data: data,
700                    responsive: true,
701                });
702                dataTable.column(7).visible(false);
703
704                table.find('tr').css('cursor', 'pointer');
705                table.on('click', 'tr', function() {
706                    let data = dataTable.row(this).data();
707                    if (!data) {
708                        // A row in header or footer.
709                        return;
710                    }
711                    let key = data[7];
712                    if (!key) {
713                        return;
714                    }
715                    let indexes = key.split('_');
716                    let processInfo = eventInfo.processes[indexes[0]];
717                    let threadInfo = processInfo.threads[indexes[1]];
718                    let lib = threadInfo.libs[indexes[2]];
719                    let func = lib.functions[indexes[3]];
720                    FunctionTab.showFunction(eventInfo, processInfo, threadInfo, lib, func);
721                });
722            }));
723    }
724
725    onSampleWeightChange() {
726        createPromise()
727            .then(updateProgress('Draw SampleTable...', 0))
728            .then(() => this._drawSampleTable(100))
729            .then(hideProgress());
730    }
731}
732
733
734// Show embedded flamegraph generated by inferno.
735class FlameGraphTab {
736    init(div) {
737        this.div = div;
738    }
739
740    draw() {
741        let views = [];
742        createPromise()
743            .then(updateProgress('Draw Flamegraph...', 0))
744            .then(wait(() => {
745                this.div.empty();
746                views = createViewsForEvents(this.div, (div, eventInfo) => {
747                    return new FlameGraphViewList(div, eventInfo);
748                });
749            }))
750            .then(() => drawViewsAsync(views, 100, (view, progress) => view.drawAsync(progress)))
751            .then(hideProgress());
752    }
753}
754
755// Show FlameGraphs for samples in an event type, used in FlameGraphTab.
756// 1. Draw 10 FlameGraphs at one time, and use a "More" button to show more FlameGraphs.
757// 2. First draw background of Flamegraphs, then draw details in idle time.
758class FlameGraphViewList {
759    constructor(div, eventInfo) {
760        this.div = div;
761        this.eventInfo = eventInfo;
762        this.selectorView = null;
763        this.flamegraphDiv = null;
764        this.flamegraphs = [];
765        this.moreButton = null;
766    }
767
768    drawAsync(totalProgress) {
769        this.div.empty();
770        this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo,
771                                                         () => this.onSampleWeightChange());
772        this.flamegraphDiv = $('<div>').appendTo(this.div);
773        return this._drawMoreFlameGraphs(10, totalProgress);
774    }
775
776    // Return a promise to draw flamegraphs.
777    _drawMoreFlameGraphs(moreCount, progress) {
778        let initProgress = progress / (1 + moreCount);
779        let newFlamegraphs = [];
780        return createPromise()
781        .then(wait(() => {
782            if (this.moreButton) {
783                this.moreButton.hide();
784            }
785            let pId = 0;
786            let tId = 0;
787            let newCount = this.flamegraphs.length + moreCount;
788            for (let i = 0; i < newCount; ++i) {
789                if (pId == this.eventInfo.processes.length) {
790                    break;
791                }
792                let process = this.eventInfo.processes[pId];
793                let thread = process.threads[tId];
794                if (i >= this.flamegraphs.length) {
795                    let title = `Process ${getProcessName(process.pid)} ` +
796                                `Thread ${getThreadName(thread.tid)} ` +
797                                `(Samples: ${thread.sampleCount})`;
798                    let totalCount = {countForProcess: process.eventCount,
799                                      countForThread: thread.eventCount};
800                    let flamegraph = new FlameGraphView(this.flamegraphDiv, title, totalCount,
801                                                        thread.g.c, false);
802                    flamegraph.draw();
803                    newFlamegraphs.push(flamegraph);
804                }
805                tId++;
806                if (tId == process.threads.length) {
807                    pId++;
808                    tId = 0;
809                }
810            }
811            if (pId < this.eventInfo.processes.length) {
812                // Show "More" Button.
813                if (!this.moreButton) {
814                    this.div.append(`
815                        <div style="text-align:center">
816                            <button type="button" class="btn btn-primary">More</button>
817                        </div>`);
818                    this.moreButton = this.div.children().last().find('button');
819                    this.moreButton.click(() => {
820                        createPromise().then(updateProgress('Draw FlameGraph...', 0))
821                            .then(() => this._drawMoreFlameGraphs(10, 100))
822                            .then(hideProgress());
823                    });
824                    this.moreButton.hide();
825                }
826            } else if (this.moreButton) {
827                this.moreButton.remove();
828                this.moreButton = null;
829            }
830            for (let flamegraph of newFlamegraphs) {
831                this.flamegraphs.push(flamegraph);
832            }
833        }))
834        .then(addProgress(initProgress))
835        .then(() => this.drawDetails(newFlamegraphs, progress - initProgress));
836    }
837
838    drawDetails(flamegraphs, totalProgress) {
839        return createPromise()
840            .then(() => drawViewsAsync(flamegraphs, totalProgress, (view, progress) => {
841                return createPromise()
842                    .then(wait(() => view.drawDetails(this.selectorView.getSampleWeightFunction())))
843                    .then(addProgress(progress));
844            }))
845            .then(wait(() => {
846               if (this.moreButton) {
847                   this.moreButton.show();
848               }
849            }));
850    }
851
852    onSampleWeightChange() {
853        createPromise().then(updateProgress('Draw FlameGraph...', 0))
854            .then(() => this.drawDetails(this.flamegraphs, 100))
855            .then(hideProgress());
856    }
857}
858
859// FunctionTab: show information of a function.
860// 1. Show the callgrpah and reverse callgraph of a function as flamegraphs.
861// 2. Show the annotated source code of the function.
862class FunctionTab {
863    static showFunction(eventInfo, processInfo, threadInfo, lib, func) {
864        let title = 'Function';
865        let tab = gTabs.findTab(title);
866        if (!tab) {
867            tab = gTabs.addTab(title, new FunctionTab());
868        }
869        gTabs.setActiveAsync(title)
870            .then(() => tab.setFunction(eventInfo, processInfo, threadInfo, lib, func));
871    }
872
873    constructor() {
874        this.func = null;
875        this.selectPercent = 'thread';
876    }
877
878    init(div) {
879        this.div = div;
880    }
881
882    setFunction(eventInfo, processInfo, threadInfo, lib, func) {
883        this.eventInfo = eventInfo;
884        this.processInfo = processInfo;
885        this.threadInfo = threadInfo;
886        this.lib = lib;
887        this.func = func;
888        this.selectorView = null;
889        this.views = [];
890        this.redraw();
891    }
892
893    redraw() {
894        if (!this.func) {
895            return;
896        }
897        createPromise()
898            .then(updateProgress("Draw Function...", 0))
899            .then(wait(() => {
900                this.div.empty();
901                this._drawTitle();
902
903                this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo,
904                                                                 () => this.onSampleWeightChange());
905                let funcId = this.func.f;
906                let funcName = getFuncName(funcId);
907                function getNodesMatchingFuncId(root) {
908                    let nodes = [];
909                    function recursiveFn(node) {
910                        if (node.f == funcId) {
911                            nodes.push(node);
912                        } else {
913                            for (let child of node.c) {
914                                recursiveFn(child);
915                            }
916                        }
917                    }
918                    recursiveFn(root);
919                    return nodes;
920                }
921                let totalCount = {countForProcess: this.processInfo.eventCount,
922                                  countForThread: this.threadInfo.eventCount};
923                let callgraphView = new FlameGraphView(
924                    this.div, `Functions called by ${funcName}`, totalCount,
925                    getNodesMatchingFuncId(this.threadInfo.g), false);
926                callgraphView.draw();
927                this.views.push(callgraphView);
928                let reverseCallgraphView = new FlameGraphView(
929                    this.div, `Functions calling ${funcName}`, totalCount,
930                    getNodesMatchingFuncId(this.threadInfo.rg), true);
931                reverseCallgraphView.draw();
932                this.views.push(reverseCallgraphView);
933                let sourceFiles = collectSourceFilesForFunction(this.func);
934                if (sourceFiles) {
935                    this.div.append(getHtml('hr'));
936                    this.div.append(getHtml('b', {text: 'SourceCode:'}) + '<br/>');
937                    this.views.push(new SourceCodeView(this.div, sourceFiles, totalCount));
938                }
939
940                let disassembly = collectDisassemblyForFunction(this.func);
941                if (disassembly) {
942                    this.div.append(getHtml('hr'));
943                    this.div.append(getHtml('b', {text: 'Disassembly:'}) + '<br/>');
944                    this.views.push(new DisassemblyView(this.div, disassembly, totalCount));
945                }
946            }))
947            .then(addProgress(25))
948            .then(() => this.drawDetails(75))
949            .then(hideProgress());
950    }
951
952    draw() {}
953
954    _drawTitle() {
955        let eventName = this.eventInfo.eventName;
956        let processName = getProcessName(this.processInfo.pid);
957        let threadName = getThreadName(this.threadInfo.tid);
958        let libName = getLibName(this.lib.libId);
959        let funcName = getFuncName(this.func.f);
960        // Draw a table of 'Name', 'Value'.
961        let rows = [];
962        rows.push(['Event Type', eventName]);
963        rows.push(['Process', processName]);
964        rows.push(['Thread', threadName]);
965        rows.push(['Library', libName]);
966        rows.push(['Function', getHtml('pre', {text: funcName})]);
967        let data = new google.visualization.DataTable();
968        data.addColumn('string', '');
969        data.addColumn('string', '');
970        data.addRows(rows);
971        for (let i = 0; i < rows.length; ++i) {
972            data.setProperty(i, 0, 'className', 'boldTableCell');
973        }
974        let wrapperDiv = $('<div>');
975        wrapperDiv.appendTo(this.div);
976        let table = new google.visualization.Table(wrapperDiv.get(0));
977        table.draw(data, {
978            width: '100%',
979            sort: 'disable',
980            allowHtml: true,
981            cssClassNames: {
982                'tableCell': 'tableCell',
983            },
984        });
985    }
986
987    onSampleWeightChange() {
988        createPromise()
989            .then(updateProgress("Draw Function...", 0))
990            .then(() => this.drawDetails(100))
991            .then(hideProgress());
992    }
993
994    drawDetails(totalProgress) {
995        let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
996        return drawViewsAsync(this.views, totalProgress, (view, progress) => {
997            return createPromise()
998                .then(wait(() => view.drawDetails(sampleWeightFunction)))
999                .then(addProgress(progress));
1000        });
1001    }
1002}
1003
1004
1005// Select the way to show sample weight in FlamegraphTab and FunctionTab.
1006// 1. Show percentage of event count relative to all processes.
1007// 2. Show percentage of event count relative to the current process.
1008// 3. Show percentage of event count relative to the current thread.
1009// 4. Show absolute event count.
1010// 5. Show event count in milliseconds, only possible for cpu-clock or task-clock events.
1011class SampleWeightSelectorView {
1012    constructor(divContainer, eventInfo, onSelectChange) {
1013        let options = new Map();
1014        options.set('percent_to_all', 'Show percentage of event count relative to all processes');
1015        options.set('percent_to_process',
1016                    'Show percentage of event count relative to the current process');
1017        options.set('percent_to_thread',
1018                    'Show percentage of event count relative to the current thread');
1019        options.set('event_count', 'Show event count');
1020        if (isClockEvent(eventInfo)) {
1021            options.set('event_count_in_ms', 'Show event count in milliseconds');
1022        }
1023        let buttons = [];
1024        options.forEach((value, key) => {
1025            buttons.push(`<button type="button" class="dropdown-item" key="${key}">${value}
1026                          </button>`);
1027        });
1028        this.curOption = 'percent_to_all';
1029        let id = createId();
1030        let str = `
1031            <div class="dropdown">
1032                <button type="button" class="btn btn-primary dropdown-toggle" id="${id}"
1033                    data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
1034                    >${options.get(this.curOption)}</button>
1035                <div class="dropdown-menu" aria-labelledby="${id}">${buttons.join('')}</div>
1036            </div>
1037        `;
1038        divContainer.append(str);
1039        divContainer.children().last().on('hidden.bs.dropdown', (e) => {
1040            if (e.clickEvent) {
1041                let button = $(e.clickEvent.target);
1042                let newOption = button.attr('key');
1043                if (newOption && this.curOption != newOption) {
1044                    this.curOption = newOption;
1045                    divContainer.find(`#${id}`).text(options.get(this.curOption));
1046                    onSelectChange();
1047                }
1048            }
1049        });
1050        this.countForAllProcesses = eventInfo.eventCount;
1051    }
1052
1053    getSampleWeightFunction() {
1054        if (this.curOption == 'percent_to_all') {
1055            let countForAllProcesses = this.countForAllProcesses;
1056            return function(eventCount, _) {
1057                let percent = eventCount * 100.0 / countForAllProcesses;
1058                return percent.toFixed(2) + '%';
1059            };
1060        }
1061        if (this.curOption == 'percent_to_process') {
1062            return function(eventCount, totalCount) {
1063                let percent = eventCount * 100.0 / totalCount.countForProcess;
1064                return percent.toFixed(2) + '%';
1065            };
1066        }
1067        if (this.curOption == 'percent_to_thread') {
1068            return function(eventCount, totalCount) {
1069                let percent = eventCount * 100.0 / totalCount.countForThread;
1070                return percent.toFixed(2) + '%';
1071            };
1072        }
1073        if (this.curOption == 'event_count') {
1074            return function(eventCount, _) {
1075                return '' + eventCount;
1076            };
1077        }
1078        if (this.curOption == 'event_count_in_ms') {
1079            return function(eventCount, _) {
1080                let timeInMs = eventCount / 1000000.0;
1081                return timeInMs.toFixed(3) + ' ms';
1082            };
1083        }
1084    }
1085}
1086
1087// Given a callgraph, show the flamegraph.
1088class FlameGraphView {
1089    constructor(divContainer, title, totalCount, initNodes, reverseOrder) {
1090        this.id = createId();
1091        this.div = $('<div>', {id: this.id,
1092                               style: 'font-family: Monospace; font-size: 12px'});
1093        this.div.appendTo(divContainer);
1094        this.title = title;
1095        this.totalCount = totalCount;
1096        this.reverseOrder = reverseOrder;
1097        this.sampleWeightFunction = null;
1098        this.svgNodeHeight = 17;
1099        this.initNodes = initNodes;
1100        this.sumCount = 0;
1101        for (let node of initNodes) {
1102            this.sumCount += node.s;
1103        }
1104        this.maxDepth = this._getMaxDepth(this.initNodes);
1105        this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3);
1106        this.svgStr = null;
1107        this.svgDiv = null;
1108        this.svg = null;
1109    }
1110
1111    _getMaxDepth(nodes) {
1112        let isArray = Array.isArray(nodes);
1113        let sumCount;
1114        if (isArray) {
1115            sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0);
1116        } else {
1117            sumCount = nodes.s;
1118        }
1119        let width = this._getWidthPercentage(sumCount);
1120        if (width < 0.1) {
1121            return 0;
1122        }
1123        let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c;
1124        let childDepth = 0;
1125        for (let child of children) {
1126            childDepth = Math.max(childDepth, this._getMaxDepth(child));
1127        }
1128        return childDepth + 1;
1129    }
1130
1131    draw() {
1132        // Only draw skeleton.
1133        this.div.empty();
1134        this.div.append(`<p><b>${this.title}</b></p>`);
1135        this.svgStr = [];
1136        this._renderBackground();
1137        this.svgStr.push('</svg></div>');
1138        this.div.append(this.svgStr.join(''));
1139        this.svgDiv = this.div.children().last();
1140        this.div.append('<br/><br/>');
1141    }
1142
1143    drawDetails(sampleWeightFunction) {
1144        this.sampleWeightFunction = sampleWeightFunction;
1145        this.svgStr = [];
1146        this._renderBackground();
1147        this._renderSvgNodes();
1148        this._renderUnzoomNode();
1149        this._renderInfoNode();
1150        this._renderPercentNode();
1151        this._renderSearchNode();
1152        // It is much faster to add html content to svgStr than adding it directly to svgDiv.
1153        this.svgDiv.html(this.svgStr.join(''));
1154        this.svgStr = [];
1155        this.svg = this.svgDiv.find('svg');
1156        this._adjustTextSize();
1157        this._enableZoom();
1158        this._enableInfo();
1159        this._enableSearch();
1160        this._adjustTextSizeOnResize();
1161    }
1162
1163    _renderBackground() {
1164        this.svgStr.push(`
1165            <div style="width: 100%; height: ${this.svgHeight}px;">
1166                <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
1167                    version="1.1" width="100%" height="100%" style="border: 1px solid black; ">
1168                        <defs > <linearGradient id="background_gradient_${this.id}"
1169                                  y1="0" y2="1" x1="0" x2="0" >
1170                                  <stop stop-color="#eeeeee" offset="5%" />
1171                                  <stop stop-color="#efefb1" offset="90%" />
1172                                  </linearGradient>
1173                         </defs>
1174                         <rect x="0" y="0" width="100%" height="100%"
1175                           fill="url(#background_gradient_${this.id})" />`);
1176    }
1177
1178    _getYForDepth(depth) {
1179        if (this.reverseOrder) {
1180            return (depth + 3) * this.svgNodeHeight;
1181        }
1182        return this.svgHeight - (depth + 1) * this.svgNodeHeight;
1183    }
1184
1185    _getWidthPercentage(eventCount) {
1186        return eventCount * 100.0 / this.sumCount;
1187    }
1188
1189    _getHeatColor(widthPercentage) {
1190        return {
1191            r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)),
1192            g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)),
1193            b: 100,
1194        };
1195    }
1196
1197    _renderSvgNodes() {
1198        let fakeNodes = [{c: this.initNodes}];
1199        let children = this._splitChildrenForNodes(fakeNodes);
1200        let xOffset = 0;
1201        for (let child of children) {
1202            xOffset = this._renderSvgNodesWithSameRoot(child, 0, xOffset);
1203        }
1204    }
1205
1206    // Return an array of children nodes, with children having the same functionId merged in a
1207    // subarray.
1208    _splitChildrenForNodes(nodes) {
1209        let map = new Map();
1210        for (let node of nodes) {
1211            for (let child of node.c) {
1212                let subNodes = map.get(child.f);
1213                if (subNodes) {
1214                    subNodes.push(child);
1215                } else {
1216                    map.set(child.f, [child]);
1217                }
1218            }
1219        }
1220        let res = [];
1221        for (let subNodes of map.values()) {
1222            res.push(subNodes.length == 1 ? subNodes[0] : subNodes);
1223        }
1224        return res;
1225    }
1226
1227    // nodes can be a CallNode, or an array of CallNodes with the same functionId.
1228    _renderSvgNodesWithSameRoot(nodes, depth, xOffset) {
1229        let x = xOffset;
1230        let y = this._getYForDepth(depth);
1231        let isArray = Array.isArray(nodes);
1232        let funcId;
1233        let sumCount;
1234        if (isArray) {
1235            funcId = nodes[0].f;
1236            sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0);
1237        } else {
1238            funcId = nodes.f;
1239            sumCount = nodes.s;
1240        }
1241        let width = this._getWidthPercentage(sumCount);
1242        if (width < 0.1) {
1243            return xOffset;
1244        }
1245        let color = this._getHeatColor(width);
1246        let borderColor = {};
1247        for (let key in color) {
1248            borderColor[key] = Math.max(0, color[key] - 50);
1249        }
1250        let funcName = getFuncName(funcId);
1251        let libName = getLibNameOfFunction(funcId);
1252        let sampleWeight = this.sampleWeightFunction(sumCount, this.totalCount);
1253        let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' +
1254                    sampleWeight + ')';
1255        this.svgStr.push(`<g><title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}"
1256                        depth="${depth}" width="${width}%" owidth="${width}" height="15.0"
1257                        ofill="rgb(${color.r},${color.g},${color.b})"
1258                        fill="rgb(${color.r},${color.g},${color.b})"
1259                        style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/>
1260                        <text x="${x}%" y="${y + 12}"></text></g>`);
1261
1262        let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c;
1263        let childXOffset = xOffset;
1264        for (let child of children) {
1265            childXOffset = this._renderSvgNodesWithSameRoot(child, depth + 1, childXOffset);
1266        }
1267        return xOffset + width;
1268    }
1269
1270    _renderUnzoomNode() {
1271        this.svgStr.push(`<rect id="zoom_rect_${this.id}" style="display:none;stroke:rgb(0,0,0);"
1272        rx="10" ry="10" x="10" y="10" width="80" height="30"
1273        fill="rgb(255,255,255)"/>
1274         <text id="zoom_text_${this.id}" x="19" y="30" style="display:none">Zoom out</text>`);
1275    }
1276
1277    _renderInfoNode() {
1278        this.svgStr.push(`<clipPath id="info_clip_path_${this.id}">
1279                         <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10"
1280                         width="789" height="30" fill="rgb(255,255,255)"/>
1281                         </clipPath>
1282                         <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10"
1283                         width="799" height="30" fill="rgb(255,255,255)"/>
1284                         <text clip-path="url(#info_clip_path_${this.id})"
1285                         id="info_text_${this.id}" x="128" y="30"></text>`);
1286    }
1287
1288    _renderPercentNode() {
1289        this.svgStr.push(`<rect style="stroke:rgb(0,0,0);" rx="10" ry="10"
1290                         x="934" y="10" width="150" height="30"
1291                         fill="rgb(255,255,255)"/>
1292                         <text id="percent_text_${this.id}" text-anchor="end"
1293                         x="1074" y="30"></text>`);
1294    }
1295
1296    _renderSearchNode() {
1297        this.svgStr.push(`<rect style="stroke:rgb(0,0,0); rx="10" ry="10"
1298                         x="1150" y="10" width="80" height="30"
1299                         fill="rgb(255,255,255)" class="search"/>
1300                         <text x="1160" y="30" class="search">Search</text>`);
1301    }
1302
1303    _adjustTextSizeForNode(g) {
1304        let text = g.find('text');
1305        let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01;
1306        if (width < 28) {
1307            text.text('');
1308            return;
1309        }
1310        let methodName = g.find('title').text().split(' | ')[0];
1311        let numCharacters;
1312        for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) {
1313            if (numCharacters * 7.5 <= width) {
1314                break;
1315            }
1316        }
1317        if (numCharacters == methodName.length) {
1318            text.text(methodName);
1319        } else {
1320            text.text(methodName.substring(0, numCharacters - 2) + '..');
1321        }
1322    }
1323
1324    _adjustTextSize() {
1325        this.svgWidth = $(window).width();
1326        let thisObj = this;
1327        this.svg.find('g').each(function(_, g) {
1328            thisObj._adjustTextSizeForNode($(g));
1329        });
1330    }
1331
1332    _enableZoom() {
1333        this.zoomStack = [null];
1334        this.svg.find('g').css('cursor', 'pointer').click(zoom);
1335        this.svg.find(`#zoom_rect_${this.id}`).css('cursor', 'pointer').click(unzoom);
1336        this.svg.find(`#zoom_text_${this.id}`).css('cursor', 'pointer').click(unzoom);
1337
1338        let thisObj = this;
1339        function zoom() {
1340            thisObj.zoomStack.push(this);
1341            displayFromElement(this);
1342            thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'block');
1343            thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'block');
1344        }
1345
1346        function unzoom() {
1347            if (thisObj.zoomStack.length > 1) {
1348                thisObj.zoomStack.pop();
1349                displayFromElement(thisObj.zoomStack[thisObj.zoomStack.length - 1]);
1350                if (thisObj.zoomStack.length == 1) {
1351                    thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'none');
1352                    thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'none');
1353                }
1354            }
1355        }
1356
1357        function displayFromElement(g) {
1358            let clickedOriginX = 0;
1359            let clickedDepth = 0;
1360            let clickedOriginWidth = 100;
1361            let scaleFactor = 1;
1362            if (g) {
1363                g = $(g);
1364                let clickedRect = g.find('rect');
1365                clickedOriginX = parseFloat(clickedRect.attr('ox'));
1366                clickedDepth = parseInt(clickedRect.attr('depth'));
1367                clickedOriginWidth = parseFloat(clickedRect.attr('owidth'));
1368                scaleFactor = 100.0 / clickedOriginWidth;
1369            }
1370            thisObj.svg.find('g').each(function(_, g) {
1371                g = $(g);
1372                let text = g.find('text');
1373                let rect = g.find('rect');
1374                let depth = parseInt(rect.attr('depth'));
1375                let ox = parseFloat(rect.attr('ox'));
1376                let owidth = parseFloat(rect.attr('owidth'));
1377                if (depth < clickedDepth || ox < clickedOriginX - 1e-9 ||
1378                    ox + owidth > clickedOriginX + clickedOriginWidth + 1e-9) {
1379                    rect.css('display', 'none');
1380                    text.css('display', 'none');
1381                } else {
1382                    rect.css('display', 'block');
1383                    text.css('display', 'block');
1384                    let nx = (ox - clickedOriginX) * scaleFactor + '%';
1385                    let ny = thisObj._getYForDepth(depth - clickedDepth);
1386                    rect.attr('x', nx);
1387                    rect.attr('y', ny);
1388                    rect.attr('width', owidth * scaleFactor + '%');
1389                    text.attr('x', nx);
1390                    text.attr('y', ny + 12);
1391                    thisObj._adjustTextSizeForNode(g);
1392                }
1393            });
1394        }
1395    }
1396
1397    _enableInfo() {
1398        this.selected = null;
1399        let thisObj = this;
1400        this.svg.find('g').on('mouseenter', function() {
1401            if (thisObj.selected) {
1402                thisObj.selected.css('stroke-width', '0');
1403            }
1404            // Mark current node.
1405            let g = $(this);
1406            thisObj.selected = g;
1407            g.css('stroke', 'black').css('stroke-width', '0.5');
1408
1409            // Parse title.
1410            let title = g.find('title').text();
1411            let methodAndInfo = title.split(' | ');
1412            thisObj.svg.find(`#info_text_${thisObj.id}`).text(methodAndInfo[0]);
1413
1414            // Parse percentage.
1415            // '/system/lib64/libhwbinder.so (4 events: 0.28%)'
1416            let regexp = /.* \(.*:\s+(.*)\)/g;
1417            let match = regexp.exec(methodAndInfo[1]);
1418            let percentage = '';
1419            if (match && match.length > 1) {
1420                percentage = match[1];
1421            }
1422            thisObj.svg.find(`#percent_text_${thisObj.id}`).text(percentage);
1423        });
1424    }
1425
1426    _enableSearch() {
1427        this.svg.find('.search').css('cursor', 'pointer').click(() => {
1428            let term = prompt('Search for:', '');
1429            if (!term) {
1430                this.svg.find('g > rect').each(function() {
1431                    this.attributes['fill'].value = this.attributes['ofill'].value;
1432                });
1433            } else {
1434                this.svg.find('g').each(function() {
1435                    let title = this.getElementsByTagName('title')[0];
1436                    let rect = this.getElementsByTagName('rect')[0];
1437                    if (title.textContent.indexOf(term) != -1) {
1438                        rect.attributes['fill'].value = 'rgb(230,100,230)';
1439                    } else {
1440                        rect.attributes['fill'].value = rect.attributes['ofill'].value;
1441                    }
1442                });
1443            }
1444        });
1445    }
1446
1447    _adjustTextSizeOnResize() {
1448        function throttle(callback) {
1449            let running = false;
1450            return function() {
1451                if (!running) {
1452                    running = true;
1453                    window.requestAnimationFrame(function () {
1454                        callback();
1455                        running = false;
1456                    });
1457                }
1458            };
1459        }
1460        $(window).resize(throttle(() => this._adjustTextSize()));
1461    }
1462}
1463
1464
1465class SourceFile {
1466
1467    constructor(fileId) {
1468        this.path = getSourceFilePath(fileId);
1469        this.code = getSourceCode(fileId);
1470        this.showLines = {};  // map from line number to {eventCount, subtreeEventCount}.
1471        this.hasCount = false;
1472    }
1473
1474    addLineRange(startLine, endLine) {
1475        for (let i = startLine; i <= endLine; ++i) {
1476            if (i in this.showLines || !(i in this.code)) {
1477                continue;
1478            }
1479            this.showLines[i] = {eventCount: 0, subtreeEventCount: 0};
1480        }
1481    }
1482
1483    addLineCount(lineNumber, eventCount, subtreeEventCount) {
1484        let line = this.showLines[lineNumber];
1485        if (line) {
1486            line.eventCount += eventCount;
1487            line.subtreeEventCount += subtreeEventCount;
1488            this.hasCount = true;
1489        }
1490    }
1491}
1492
1493// Return a list of SourceFile related to a function.
1494function collectSourceFilesForFunction(func) {
1495    if (!func.hasOwnProperty('s')) {
1496        return null;
1497    }
1498    let hitLines = func.s;
1499    let sourceFiles = {};  // map from sourceFileId to SourceFile.
1500
1501    function getFile(fileId) {
1502        let file = sourceFiles[fileId];
1503        if (!file) {
1504            file = sourceFiles[fileId] = new SourceFile(fileId);
1505        }
1506        return file;
1507    }
1508
1509    // Show lines for the function.
1510    let funcRange = getFuncSourceRange(func.f);
1511    if (funcRange) {
1512        let file = getFile(funcRange.fileId);
1513        file.addLineRange(funcRange.startLine);
1514    }
1515
1516    // Show lines for hitLines.
1517    for (let hitLine of hitLines) {
1518        let file = getFile(hitLine.f);
1519        file.addLineRange(hitLine.l - 5, hitLine.l + 5);
1520        file.addLineCount(hitLine.l, hitLine.e, hitLine.s);
1521    }
1522
1523    let result = [];
1524    // Show the source file containing the function before other source files.
1525    if (funcRange) {
1526        let file = getFile(funcRange.fileId);
1527        if (file.hasCount) {
1528            result.push(file);
1529        }
1530        delete sourceFiles[funcRange.fileId];
1531    }
1532    for (let fileId in sourceFiles) {
1533        let file = sourceFiles[fileId];
1534        if (file.hasCount) {
1535            result.push(file);
1536        }
1537    }
1538    return result.length > 0 ? result : null;
1539}
1540
1541// Show annotated source code of a function.
1542class SourceCodeView {
1543
1544    constructor(divContainer, sourceFiles, totalCount) {
1545        this.div = $('<div>');
1546        this.div.appendTo(divContainer);
1547        this.sourceFiles = sourceFiles;
1548        this.totalCount = totalCount;
1549    }
1550
1551    drawDetails(sampleWeightFunction) {
1552        google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction));
1553    }
1554
1555    realDraw(sampleWeightFunction) {
1556        this.div.empty();
1557        // For each file, draw a table of 'Line', 'Total', 'Self', 'Code'.
1558        for (let sourceFile of this.sourceFiles) {
1559            let rows = [];
1560            let lineNumbers = Object.keys(sourceFile.showLines);
1561            lineNumbers.sort((a, b) => a - b);
1562            for (let lineNumber of lineNumbers) {
1563                let code = getHtml('pre', {text: sourceFile.code[lineNumber]});
1564                let countInfo = sourceFile.showLines[lineNumber];
1565                let totalValue = '';
1566                let selfValue = '';
1567                if (countInfo.subtreeEventCount != 0) {
1568                    totalValue = sampleWeightFunction(countInfo.subtreeEventCount, this.totalCount);
1569                    selfValue = sampleWeightFunction(countInfo.eventCount, this.totalCount);
1570                }
1571                rows.push([lineNumber, totalValue, selfValue, code]);
1572            }
1573
1574            let data = new google.visualization.DataTable();
1575            data.addColumn('string', 'Line');
1576            data.addColumn('string', 'Total');
1577            data.addColumn('string', 'Self');
1578            data.addColumn('string', 'Code');
1579            data.addRows(rows);
1580            for (let i = 0; i < rows.length; ++i) {
1581                data.setProperty(i, 0, 'className', 'colForLine');
1582                for (let j = 1; j <= 2; ++j) {
1583                    data.setProperty(i, j, 'className', 'colForCount');
1584                }
1585            }
1586            this.div.append(getHtml('pre', {text: sourceFile.path}));
1587            let wrapperDiv = $('<div>');
1588            wrapperDiv.appendTo(this.div);
1589            let table = new google.visualization.Table(wrapperDiv.get(0));
1590            table.draw(data, {
1591                width: '100%',
1592                sort: 'disable',
1593                frozenColumns: 3,
1594                allowHtml: true,
1595            });
1596        }
1597    }
1598}
1599
1600// Return a list of disassembly related to a function.
1601function collectDisassemblyForFunction(func) {
1602    if (!func.hasOwnProperty('a')) {
1603        return null;
1604    }
1605    let hitAddrs = func.a;
1606    let rawCode = getFuncDisassembly(func.f);
1607    if (!rawCode) {
1608        return null;
1609    }
1610
1611    // Annotate disassembly with event count information.
1612    let annotatedCode = [];
1613    let codeForLastAddr = null;
1614    let hitAddrPos = 0;
1615    let hasCount = false;
1616
1617    function addEventCount(addr) {
1618        while (hitAddrPos < hitAddrs.length && BigInt(hitAddrs[hitAddrPos].a) < addr) {
1619            if (codeForLastAddr) {
1620                codeForLastAddr.eventCount += hitAddrs[hitAddrPos].e;
1621                codeForLastAddr.subtreeEventCount += hitAddrs[hitAddrPos].s;
1622                hasCount = true;
1623            }
1624            hitAddrPos++;
1625        }
1626    }
1627
1628    for (let line of rawCode) {
1629        let code = line[0];
1630        let addr = BigInt(line[1]);
1631
1632        addEventCount(addr);
1633        let item = {code: code, eventCount: 0, subtreeEventCount: 0};
1634        annotatedCode.push(item);
1635        // Objdump sets addr to 0 when a disassembly line is not associated with an addr.
1636        if (addr != 0) {
1637            codeForLastAddr = item;
1638        }
1639    }
1640    addEventCount(Number.MAX_VALUE);
1641    return hasCount ? annotatedCode : null;
1642}
1643
1644// Show annotated disassembly of a function.
1645class DisassemblyView {
1646
1647    constructor(divContainer, disassembly, totalCount) {
1648        this.div = $('<div>');
1649        this.div.appendTo(divContainer);
1650        this.disassembly = disassembly;
1651        this.totalCount = totalCount;
1652    }
1653
1654    drawDetails(sampleWeightFunction) {
1655        google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction));
1656    }
1657
1658    realDraw(sampleWeightFunction) {
1659        this.div.empty();
1660        // Draw a table of 'Total', 'Self', 'Code'.
1661        let rows = [];
1662        for (let line of this.disassembly) {
1663            let code = getHtml('pre', {text: line.code});
1664            let totalValue = '';
1665            let selfValue = '';
1666            if (line.subtreeEventCount != 0) {
1667                totalValue = sampleWeightFunction(line.subtreeEventCount, this.totalCount);
1668                selfValue = sampleWeightFunction(line.eventCount, this.totalCount);
1669            }
1670            rows.push([totalValue, selfValue, code]);
1671        }
1672        let data = new google.visualization.DataTable();
1673        data.addColumn('string', 'Total');
1674        data.addColumn('string', 'Self');
1675        data.addColumn('string', 'Code');
1676        data.addRows(rows);
1677        for (let i = 0; i < rows.length; ++i) {
1678            for (let j = 0; j < 2; ++j) {
1679                data.setProperty(i, j, 'className', 'colForCount');
1680            }
1681        }
1682        let wrapperDiv = $('<div>');
1683        wrapperDiv.appendTo(this.div);
1684        let table = new google.visualization.Table(wrapperDiv.get(0));
1685        table.draw(data, {
1686            width: '100%',
1687            sort: 'disable',
1688            frozenColumns: 2,
1689            allowHtml: true,
1690        });
1691    }
1692}
1693
1694
1695function initGlobalObjects() {
1696    let recordData = $('#record_data').text();
1697    gRecordInfo = JSON.parse(recordData);
1698    gProcesses = gRecordInfo.processNames;
1699    gThreads = gRecordInfo.threadNames;
1700    gLibList = gRecordInfo.libList;
1701    gFunctionMap = gRecordInfo.functionMap;
1702    gSampleInfo = gRecordInfo.sampleInfo;
1703    gSourceFiles = gRecordInfo.sourceFiles;
1704}
1705
1706function createTabs() {
1707    gTabs = new TabManager($('div#report_content'));
1708    gTabs.addTab('Chart Statistics', new ChartStatTab());
1709    gTabs.addTab('Sample Table', new SampleTableTab());
1710    gTabs.addTab('Flamegraph', new FlameGraphTab());
1711}
1712
1713// Global draw objects
1714let gTabs;
1715let gProgressBar = new ProgressBar();
1716
1717// Gobal Json Data
1718let gRecordInfo;
1719let gProcesses;
1720let gThreads;
1721let gLibList;
1722let gFunctionMap;
1723let gSampleInfo;
1724let gSourceFiles;
1725
1726function updateProgress(text, progress) {
1727    return () => gProgressBar.updateAsync(text, progress);
1728}
1729
1730function addProgress(progress) {
1731    return () => gProgressBar.updateAsync(null, gProgressBar.progress + progress);
1732}
1733
1734function hideProgress() {
1735    return () => gProgressBar.hide();
1736}
1737
1738function createPromise(callback) {
1739    if (callback) {
1740        return new Promise((resolve, _) => callback(resolve));
1741    }
1742    return new Promise((resolve,_) => resolve());
1743}
1744
1745function waitDocumentReady() {
1746    return createPromise((resolve) => $(document).ready(resolve));
1747}
1748
1749function wait(functionCall) {
1750    return () => {
1751        functionCall();
1752        return createPromise();
1753    };
1754}
1755
1756createPromise()
1757    .then(updateProgress('Load page...', 0))
1758    .then(waitDocumentReady)
1759    .then(updateProgress('Parse Json data...', 20))
1760    .then(wait(initGlobalObjects))
1761    .then(updateProgress('Create tabs...', 30))
1762    .then(wait(createTabs))
1763    .then(updateProgress('Draw ChartStat...', 40))
1764    .then(() => gTabs.setActiveAsync('Chart Statistics'))
1765    .then(updateProgress(null, 100))
1766    .then(hideProgress());
1767})();
1768