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