Merge "simpleperf: draw thread flamegraphs using javascript in report_html.py."
diff --git a/simpleperf/scripts/report_html.js b/simpleperf/scripts/report_html.js
index e35bba6..1e000b1 100644
--- a/simpleperf/scripts/report_html.js
+++ b/simpleperf/scripts/report_html.js
@@ -102,17 +102,21 @@
             eventInfo.eventName.includes('cpu-clock');
 }
 
+let createId = function() {
+    let currentId = 0;
+    return () => `id${++currentId}`;
+}();
+
 class TabManager {
     constructor(divContainer) {
-        this.div = $('<div>', {id: 'tabs'});
-        this.div.appendTo(divContainer);
+        this.div = $('<div>').appendTo(divContainer);
         this.div.append(getHtml('ul'));
         this.tabs = [];
         this.isDrawCalled = false;
     }
 
     addTab(title, tabObj) {
-        let id = 'tab_' + this.div.children().length;
+        let id = createId();
         let tabDiv = $('<div>', {id: id});
         tabDiv.appendTo(this.div);
         this.div.children().first().append(
@@ -211,9 +215,7 @@
 // Show pieChart of event count percentage of each process, thread, library and function.
 class ChartView {
     constructor(divContainer, eventInfo) {
-        this.id = divContainer.children().length;
-        this.div = $('<div>', {id: 'chartstat_' + this.id});
-        this.div.appendTo(divContainer);
+        this.div = $('<div>').appendTo(divContainer);
         this.eventInfo = eventInfo;
         this.processInfo = null;
         this.threadInfo = null;
@@ -227,7 +229,7 @@
         if (isClockEvent(this.eventInfo)) {
             this.getSampleWeight = function (eventCount) {
                 return (eventCount / 1000000.0).toFixed(3) + ' ms';
-            }
+            };
         } else {
             this.getSampleWeight = (eventCount) => '' + eventCount;
         }
@@ -388,9 +390,6 @@
 
 
 class ChartStatTab {
-    constructor() {
-    }
-
     init(div) {
         this.div = div;
         this.recordFileView = new RecordFileView(this.div);
@@ -410,9 +409,6 @@
 
 
 class SampleTableTab {
-    constructor() {
-    }
-
     init(div) {
         this.div = div;
         this.selectorView = null;
@@ -481,7 +477,7 @@
         if (this.curOption == this.options.SHOW_PERCENT) {
             return function(eventCount) {
                 return (eventCount * 100.0 / eventInfo.eventCount).toFixed(2) + '%';
-            }
+            };
         }
         if (isClockEvent(eventInfo)) {
             return (eventCount) => (eventCount / 1000000.0).toFixed(3);
@@ -500,9 +496,8 @@
 
 class SampleTableView {
     constructor(divContainer, eventInfo) {
-        this.id = divContainer.children().length;
-        this.div = $('<div>');
-        this.div.appendTo(divContainer);
+        this.id = createId();
+        this.div = $('<div>', {id: this.id}).appendTo(divContainer);
         this.eventInfo = eventInfo;
     }
 
@@ -570,19 +565,73 @@
 
 // Show embedded flamegraph generated by inferno.
 class FlameGraphTab {
-    constructor() {
-    }
-
     init(div) {
         this.div = div;
     }
 
     draw() {
-        $('div#flamegraph_id').appendTo(this.div).css('display', 'block');
-        flamegraphInit();
+        this.div.empty();
+        if (gSampleInfo.length == 0) {
+            return;
+        }
+        let id = this.div.attr('id');
+        if (gSampleInfo.length == 1) {
+            new FlameGraphViewList(this.div, `${id}_0`, gSampleInfo[0]).draw();
+        } else {
+            // If more than one event, draw them in tabs.
+            this.div.append(getHtml('ul'));
+            let ul = this.div.children().first();
+            for (let i = 0; i < gSampleInfo.length; ++i) {
+                let subId = id + '_' + i;
+                let title = gSampleInfo[i].eventName;
+                ul.append(getHtml('li', {text: getHtml('a', {href: '#' + subId, text: title})}));
+                new FlameGraphViewList(this.div, subId, gSampleInfo[i]).draw();
+            }
+            this.div.tabs({active: 0});
+        }
     }
 }
 
+// Show FlameGraphs for samples in an event type, used in FlameGraphTab.
+class FlameGraphViewList {
+    constructor(divContainer, divId, eventInfo) {
+        this.div = $('<div>', {id: divId});
+        this.div.appendTo(divContainer);
+        this.eventInfo = eventInfo;
+    }
+
+    draw() {
+        this.div.empty();
+        this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo,
+                                                         () => this.onSampleWeightChange());
+        this.selectorView.draw();
+        this.flamegraphs = [];
+        for (let process of this.eventInfo.processes) {
+            for (let thread of process.threads) {
+                let title = `Process ${getProcessName(process.pid)} ` +
+                    `Thread ${getThreadName(thread.tid)} ` +
+                    `(Samples: ${thread.sampleCount})`;
+                let flamegraphView = new FlameGraphView(this.div, title, thread.g.c, false);
+                this.flamegraphs.push(flamegraphView);
+            }
+        }
+        this.onSampleWeightChange();
+    }
+
+    onSampleWeightChange() {
+        let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
+        let totalCount = {};
+        let graphId = 0;
+        for (let process of this.eventInfo.processes) {
+            totalCount.countForProcess = process.eventCount;
+            for (let thread of process.threads) {
+                totalCount.countForThread = thread.eventCount;
+                this.flamegraphs[graphId++].draw(
+                    (eventCount) => sampleWeightFunction(eventCount, totalCount));
+            }
+        }
+    }
+}
 
 // FunctionTab: show information of a function.
 // 1. Show the callgrpah and reverse callgraph of a function as flamegraphs.
@@ -628,20 +677,31 @@
         this.div.empty();
         this._drawTitle();
 
-        this.selectorView = new FunctionSampleWeightSelectorView(this.div, this.eventInfo,
-            this.processInfo, this.threadInfo, () => this.onSampleWeightChange());
+        this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo,
+                                                         () => this.onSampleWeightChange());
         this.selectorView.draw();
 
-        this.div.append(getHtml('hr'));
-        let funcName = getFuncName(this.func.f);
-        this.div.append(getHtml('b', {text: `Functions called by ${funcName}`}) + '<br/>');
-        this.callgraphView = new FlameGraphView(this.div, this.threadInfo.g, this.func, false);
-
-        this.div.append(getHtml('hr'));
-        this.div.append(getHtml('b', {text: `Functions calling ${funcName}`}) + '<br/>');
-        this.reverseCallgraphView = new FlameGraphView(this.div, this.threadInfo.rg, this.func,
+        let funcId = this.func.f;
+        let funcName = getFuncName(funcId);
+        function getNodesMatchingFuncId(root) {
+            let nodes = [];
+            function recursiveFn(node) {
+                if (node.f == funcId) {
+                    nodes.push(node);
+                } else {
+                    for (let child of node.c) {
+                        recursiveFn(child);
+                    }
+                }
+            }
+            recursiveFn(root);
+            return nodes;
+        }
+        this.callgraphView = new FlameGraphView(this.div, `Functions called by ${funcName}`,
+                                                getNodesMatchingFuncId(this.threadInfo.g), false);
+        this.reverseCallgraphView = new FlameGraphView(this.div, `Functions calling ${funcName}`,
+                                                       getNodesMatchingFuncId(this.threadInfo.rg),
                                                        true);
-
         let sourceFiles = collectSourceFilesForFunction(this.func);
         if (sourceFiles) {
             this.div.append(getHtml('hr'));
@@ -693,7 +753,12 @@
     }
 
     onSampleWeightChange() {
-        let sampleWeightFunction = this.selectorView.getSampleWeightFunction();
+        let rawSampleWeightFunction = this.selectorView.getSampleWeightFunction();
+        let totalCount = {
+            countForProcess: this.processInfo.eventCount,
+            countForThread: this.threadInfo.eventCount,
+        };
+        let sampleWeightFunction = (eventCount) => rawSampleWeightFunction(eventCount, totalCount);
         if (this.callgraphView) {
             this.callgraphView.draw(sampleWeightFunction);
         }
@@ -710,20 +775,18 @@
 }
 
 
-// Select the way to show sample weight in FunctionTab.
+// Select the way to show sample weight in FlamegraphTab and FunctionTab.
 // 1. Show percentage of event count relative to all processes.
 // 2. Show percentage of event count relative to the current process.
 // 3. Show percentage of event count relative to the current thread.
 // 4. Show absolute event count.
 // 5. Show event count in milliseconds, only possible for cpu-clock or task-clock events.
-class FunctionSampleWeightSelectorView {
-    constructor(divContainer, eventInfo, processInfo, threadInfo, onSelectChange) {
+class SampleWeightSelectorView {
+    constructor(divContainer, eventInfo, onSelectChange) {
         this.div = $('<div>');
         this.div.appendTo(divContainer);
+        this.countForAllProcesses = eventInfo.eventCount;
         this.onSelectChange = onSelectChange;
-        this.eventCountForAllProcesses = eventInfo.eventCount;
-        this.eventCountForProcess = processInfo.eventCount;
-        this.eventCountForThread = threadInfo.eventCount;
         this.options = {
             PERCENT_TO_ALL_PROCESSES: 0,
             PERCENT_TO_CUR_PROCESS: 1,
@@ -766,32 +829,32 @@
     }
 
     getSampleWeightFunction() {
-        let thisObj = this;
         if (this.curOption == this.options.PERCENT_TO_ALL_PROCESSES) {
-            return function(eventCount) {
-                let percent = eventCount * 100.0 / thisObj.eventCountForAllProcesses;
+            let countForAllProcesses = this.countForAllProcesses;
+            return function(eventCount, _) {
+                let percent = eventCount * 100.0 / countForAllProcesses;
                 return percent.toFixed(2) + '%';
             };
         }
         if (this.curOption == this.options.PERCENT_TO_CUR_PROCESS) {
-            return function(eventCount) {
-                let percent = eventCount * 100.0 / thisObj.eventCountForProcess;
+            return function(eventCount, totalCount) {
+                let percent = eventCount * 100.0 / totalCount.countForProcess;
                 return percent.toFixed(2) + '%';
             };
         }
         if (this.curOption == this.options.PERCENT_TO_CUR_THREAD) {
-            return function(eventCount) {
-                let percent = eventCount * 100.0 / thisObj.eventCountForThread;
+            return function(eventCount, totalCount) {
+                let percent = eventCount * 100.0 / totalCount.countForThread;
                 return percent.toFixed(2) + '%';
             };
         }
         if (this.curOption == this.options.RAW_EVENT_COUNT) {
-            return function(eventCount) {
+            return function(eventCount, _) {
                 return '' + eventCount;
             };
         }
         if (this.curOption == this.options.EVENT_COUNT_IN_TIME) {
-            return function(eventCount) {
+            return function(eventCount, _) {
                 let timeInMs = eventCount / 1000000.0;
                 return timeInMs.toFixed(3) + ' ms';
             };
@@ -799,39 +862,22 @@
     }
 }
 
-
 // Given a callgraph, show the flamegraph.
 class FlameGraphView {
-    // If reverseOrder is false, the root of the flamegraph is at the bottom,
-    // otherwise it is at the top.
-    constructor(divContainer, callgraph, funcInfo, reverseOrder) {
-        this.id = divContainer.children().length;
-        this.div = $('<div>', {id: 'fg_' + this.id});
+    constructor(divContainer, title, initNodes, reverseOrder) {
+        this.id = createId();
+        this.div = $('<div>', {id: this.id,
+                               style: 'font-family: Monospace; font-size: 12px'});
         this.div.appendTo(divContainer);
+        this.title = title;
         this.reverseOrder = reverseOrder;
         this.sampleWeightFunction = null;
-        this.svgWidth = $(window).width();
         this.svgNodeHeight = 17;
-        this.fontSize = 12;
-
-        let funcId = funcInfo.f;
-        let initNodes = [];
-        function findNodesMatchingFuncId(node) {
-            if (node.f == funcId) {
-                initNodes.push(node);
-            } else {
-                for (let child of node.c) {
-                    findNodesMatchingFuncId(child);
-                }
-            }
-        }
-        findNodesMatchingFuncId(callgraph);
         this.initNodes = initNodes;
         this.sumCount = 0;
         for (let node of initNodes) {
             this.sumCount += node.s;
         }
-
         function getMaxDepth(node) {
             let depth = 0;
             for (let child of node.c) {
@@ -849,24 +895,29 @@
     draw(sampleWeightFunction) {
         this.sampleWeightFunction = sampleWeightFunction;
         this.div.empty();
-        this.div.css('width', '100%').css('height', this.svgHeight + 'px');
+        this.div.append(`<p><b>${this.title}</b></p>`);
+        let svgDiv = $('<div>').appendTo(this.div);
+        this.div.append('<br/><br/>');
+        svgDiv.css('width', '100%').css('height', this.svgHeight + 'px');
         let svgStr = '<svg xmlns="http://www.w3.org/2000/svg" \
         xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" \
-        width="100%" height="100%" style="border: 1px solid black; font-family: Monospace;"> \
+        width="100%" height="100%" style="border: 1px solid black; "> \
         </svg>';
-        this.div.append(svgStr);
-        this.svg = this.div.find('svg');
+        svgDiv.append(svgStr);
+        this.svg = svgDiv.find('svg');
         this._renderBackground();
-        this._renderSvgNodes(this.initNodes, 0, 0);
+        this._renderSvgNodes(this.initNodes);
         this._renderUnzoomNode();
         this._renderInfoNode();
         this._renderPercentNode();
+        this._renderSearchNode();
         // Make the added nodes in the svg visible.
-        this.div.html(this.div.html());
-        this.svg = this.div.find('svg');
+        svgDiv.html(svgDiv.html());
+        this.svg = svgDiv.find('svg');
         this._adjustTextSize();
         this._enableZoom();
         this._enableInfo();
+        this._enableSearch();
         this._adjustTextSizeOnResize();
     }
 
@@ -900,36 +951,18 @@
         };
     }
 
-    _renderSvgNodes(nodes, depth, xOffset) {
-        let x = xOffset;
-        let y = this._getYForDepth(depth);
-        let sumCount = 0;
-        for (let node of nodes) {
-            sumCount += node.s;
+    _renderSvgNodes() {
+        let fakeNodes = [{c: this.initNodes}];
+        let children = this._splitChildrenForNodes(fakeNodes);
+        let xOffset = 0;
+        for (let child of children) {
+            xOffset = this._renderSvgNodesWithSameRoot(child, 0, xOffset);
         }
-        let width = this._getWidthPercentage(sumCount);
-        if (width < 0.1) {
-            return xOffset;
-        }
-        let color = this._getHeatColor(width);
-        let borderColor = {};
-        for (let key in color) {
-            borderColor[key] = Math.max(0, color[key] - 50);
-        }
-        let funcName = getFuncName(nodes[0].f);
-        let libName = getLibNameOfFunction(nodes[0].f);
-        let sampleWeight = this.sampleWeightFunction(sumCount);
-        let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' +
-                    sampleWeight + ')';
-        this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \
-                        depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \
-                        ofill="rgb(${color.r},${color.g},${color.b})" \
-                        fill="rgb(${color.r},${color.g},${color.b})" \
-                        style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \
-                        <text x="${x}%" y="${y + 12}" font-size="${this.fontSize}" \
-                        font-family="Monospace"></text></g>`);
+    }
 
-        let childXOffset = xOffset;
+    // Return an array of children nodes, with children having the same functionId merged in a
+    // subarray.
+    _splitChildrenForNodes(nodes) {
         let map = new Map();
         for (let node of nodes) {
             for (let child of node.c) {
@@ -941,8 +974,52 @@
                 }
             }
         }
+        let res = [];
         for (let subNodes of map.values()) {
-            childXOffset = this._renderSvgNodes(subNodes, depth + 1, childXOffset);
+            res.push(subNodes.length == 1 ? subNodes[0] : subNodes);
+        }
+        return res;
+    }
+
+    // nodes can be a CallNode, or an array of CallNodes with the same functionId.
+    _renderSvgNodesWithSameRoot(nodes, depth, xOffset) {
+        let x = xOffset;
+        let y = this._getYForDepth(depth);
+        let isArray = Array.isArray(nodes);
+        let funcId;
+        let sumCount;
+        if (isArray) {
+            funcId = nodes[0].f;
+            sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0);
+        } else {
+            funcId = nodes.f;
+            sumCount = nodes.s;
+        }
+        let width = this._getWidthPercentage(sumCount);
+        if (width < 0.1) {
+            return xOffset;
+        }
+        let color = this._getHeatColor(width);
+        let borderColor = {};
+        for (let key in color) {
+            borderColor[key] = Math.max(0, color[key] - 50);
+        }
+        let funcName = getFuncName(funcId);
+        let libName = getLibNameOfFunction(funcId);
+        let sampleWeight = this.sampleWeightFunction(sumCount);
+        let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' +
+                    sampleWeight + ')';
+        this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \
+                        depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \
+                        ofill="rgb(${color.r},${color.g},${color.b})" \
+                        fill="rgb(${color.r},${color.g},${color.b})" \
+                        style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \
+                        <text x="${x}%" y="${y + 12}"></text></g>`);
+
+        let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c;
+        let childXOffset = xOffset;
+        for (let child of children) {
+            childXOffset = this._renderSvgNodesWithSameRoot(child, depth + 1, childXOffset);
         }
         return xOffset + width;
     }
@@ -973,6 +1050,13 @@
                          x="1074" y="30"></text>`);
     }
 
+    _renderSearchNode() {
+        this.svg.append('<rect style="stroke:rgb(0,0,0); rx="10" ry="10" \
+                         x="1150" y="10" width="80" height="30" \
+                         fill="rgb(255,255,255)" class="search"/> \
+                         <text x="1160" y="30" class="search">Search</text>');
+    }
+
     _adjustTextSizeForNode(g) {
         let text = g.find('text');
         let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01;
@@ -1090,6 +1174,27 @@
         });
     }
 
+    _enableSearch() {
+        this.svg.find('.search').css('cursor', 'pointer').click(() => {
+            let term = prompt('Search for:', '');
+            if (!term) {
+                this.svg.find('g > rect').each(function() {
+                    this.attributes['fill'].value = this.attributes['ofill'].value;
+                });
+            } else {
+                this.svg.find('g').each(function() {
+                    let title = this.getElementsByTagName('title')[0];
+                    let rect = this.getElementsByTagName('rect')[0];
+                    if (title.textContent.indexOf(term) != -1) {
+                        rect.attributes['fill'].value = 'rgb(230,100,230)';
+                    } else {
+                        rect.attributes['fill'].value = rect.attributes['ofill'].value;
+                    }
+                });
+            }
+        });
+    }
+
     _adjustTextSizeOnResize() {
         function throttle(callback) {
             let running = false;
diff --git a/simpleperf/scripts/report_html.py b/simpleperf/scripts/report_html.py
index 2eaa6e9..9c8843a 100644
--- a/simpleperf/scripts/report_html.py
+++ b/simpleperf/scripts/report_html.py
@@ -16,16 +16,14 @@
 #
 
 import argparse
+import collections
 import datetime
 import json
 import os
-import subprocess
-import sys
-import tempfile
 
 from simpleperf_report_lib import ReportLib
-from utils import log_info, log_warning, log_exit
-from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser, remove
+from utils import log_info, log_exit
+from utils import Addr2Nearestline, get_script_dir, Objdump, open_report_in_browser
 
 
 class HtmlWriter(object):
@@ -81,8 +79,9 @@
         result = {}
         result['eventName'] = self.name
         result['eventCount'] = self.event_count
+        processes = sorted(self.processes.values(), key=lambda a: a.event_count, reverse=True)
         result['processes'] = [process.get_sample_info(gen_addr_hit_map)
-                               for process in self.processes.values()]
+                               for process in processes]
         return result
 
     @property
@@ -120,8 +119,9 @@
         result = {}
         result['pid'] = self.pid
         result['eventCount'] = self.event_count
+        threads = sorted(self.threads.values(), key=lambda a: a.event_count, reverse=True)
         result['threads'] = [thread.get_sample_info(gen_addr_hit_map)
-                             for thread in self.threads.values()]
+                             for thread in threads]
         return result
 
 
@@ -131,6 +131,7 @@
         self.tid = tid
         self.name = ''
         self.event_count = 0
+        self.sample_count = 0
         self.libs = {}  # map from lib_id to LibScope
         self.call_graph = CallNode(-1)
         self.reverse_call_graph = CallNode(-1)
@@ -189,6 +190,7 @@
         result = {}
         result['tid'] = self.tid
         result['eventCount'] = self.event_count
+        result['sampleCount'] = self.sample_count
         result['libs'] = [lib.gen_sample_info(gen_addr_hit_map)
                           for lib in self.libs.values()]
         result['g'] = self.call_graph.gen_sample_info()
@@ -276,7 +278,7 @@
         self.event_count = 0
         self.subtree_event_count = 0
         self.func_id = func_id
-        self.children = {}  # map from func_id to CallNode
+        self.children = collections.OrderedDict()  # map from func_id to CallNode
 
     def get_child(self, func_id):
         child = self.children.get(func_id)
@@ -508,6 +510,7 @@
                 threadInfo = {
                     tid
                     eventCount
+                    sampleCount
                     libs: [libInfo],
                     g: callGraph,
                     rg: reverseCallgraph
@@ -596,6 +599,7 @@
             process.event_count += raw_sample.period
             thread = process.get_thread(raw_sample.tid, raw_sample.thread_comm)
             thread.event_count += raw_sample.period
+            thread.sample_count += 1
 
             lib_id = self.libs.get_lib_id(symbol.dso_name)
             func_id = self.functions.get_func_id(lib_id, symbol)
@@ -834,9 +838,6 @@
         self.hw.add(json.dumps(record_data))
         self.hw.close_tag()
 
-    def write_flamegraph(self, flamegraph):
-        self.hw.add(flamegraph)
-
     def write_script(self):
         self.hw.open_tag('script').add_file('report_html.js').close_tag()
 
@@ -846,21 +847,6 @@
         self.hw.close()
 
 
-def gen_flamegraph(record_file, show_art_frames):
-    fd, flamegraph_path = tempfile.mkstemp()
-    os.close(fd)
-    inferno_script_path = os.path.join(get_script_dir(), 'inferno', 'inferno.py')
-    args = [sys.executable, inferno_script_path, '-sc', '-o', flamegraph_path,
-            '--record_file', record_file, '--embedded_flamegraph', '--no_browser']
-    if show_art_frames:
-        args.append('--show_art_frames')
-    subprocess.check_call(args)
-    with open(flamegraph_path, 'r') as fh:
-        data = fh.read()
-    remove(flamegraph_path)
-    return data
-
-
 def main():
     parser = argparse.ArgumentParser(description='report profiling data')
     parser.add_argument('-i', '--record_file', nargs='+', default=['perf.data'], help="""
@@ -915,11 +901,6 @@
     report_generator.write_content_div()
     report_generator.write_record_data(record_data.gen_record_info())
     report_generator.write_script()
-    # TODO: support multiple perf.data in flamegraph.
-    if len(args.record_file) > 1:
-        log_warning('flamegraph will only be shown for %s' % args.record_file[0])
-    flamegraph = gen_flamegraph(args.record_file[0], args.show_art_frames)
-    report_generator.write_flamegraph(flamegraph)
     report_generator.finish()
 
     if not args.no_browser: