| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/base/units/util.html"> |
| <link rel="import" href="/core/selection.html"> |
| <link rel="import" href="/base/iteration_helpers.html"> |
| <link rel="import" href="/base/statistics.html"> |
| <link rel="import" href="/base/ui/dom_helpers.html"> |
| <link rel="import" href="/base/ui/pie_chart.html"> |
| <link rel="import" href="/base/ui/sortable_table.html"> |
| <link rel="import" href="/base/ui/sunburst_chart.html"> |
| |
| <template id="x-sample-summary-panel-template"> |
| <style> |
| .x-sample-summary-panel { |
| display: block; |
| overflow: auto; |
| padding: 5px; |
| } |
| .x-sample-summary-panel > x-toolbar { |
| display: block; |
| border-bottom: 1px solid black; |
| } |
| |
| .x-sample-summary-panel > x-left-panel { |
| position: relative; |
| width: 610px; |
| left: 0; |
| top: 0; |
| height: 610px; |
| padding: 5px; |
| } |
| |
| .x-sample-summary-panel > x-left-panel > result-area { |
| display: block; |
| width: 610px; |
| position: absolute; |
| left: 0; |
| top: 0; |
| height: 610px; |
| } |
| |
| .x-sample-summary-panel > x-left-panel > x-explanation { |
| display: block; |
| position: absolute; |
| top: 300px; |
| left: 250px; |
| width: 100px; |
| height: 100px; |
| text-align: center; |
| vertical-align:middle; |
| color: #666; |
| font-size: 12px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel { |
| display: block; |
| min-height: 610px; |
| margin-left: 610px; |
| padding: 5px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel td { |
| color: #fff; |
| padding: 1px 5px; |
| font-size: 1.0em; |
| white-space: nowrap; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-sequence { |
| display: block; |
| overflow: auto; |
| width: 600px; |
| height: 400px; |
| font-size: 14px; |
| margin: 10px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-sequence table { |
| min-width: 600px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-callees { |
| display: block; |
| position: relative; |
| width: 600px; |
| height: auto; |
| overflow: auto; |
| font-size: 14px; |
| margin: 10px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-callees .x-col-numeric { |
| width: 30px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-callees .x-td-numeric { |
| text-align: right; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-callees table { |
| min-width: 600px; |
| max-width: 600px; |
| } |
| |
| .x-sample-summary-panel > x-right-panel > x-callees { |
| display: block; |
| overflow: auto; |
| height: 300px; |
| } |
| |
| </style> |
| |
| <x-toolbar></x-toolbar> |
| <x-left-panel> |
| <result-area></result-area> |
| <x-explanation></x-explanation> |
| </x-left-panel> |
| <x-right-panel> |
| <x-sequence></x-sequence> |
| <x-callees></x-callees> |
| </x-right-panel> |
| </template> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.e.analysis', function() { |
| var THIS_DOC = document.currentScript.ownerDocument; |
| |
| var RequestSelectionChangeEvent = tr.c.RequestSelectionChangeEvent; |
| var getColorOfKey = tr.b.ui.getColorOfKey; |
| |
| /** |
| * @constructor |
| */ |
| var CallTreeNode = function(name, category) { |
| this.parent = undefined; |
| this.name = name; |
| this.category = category; |
| this.selfTime = 0.0; |
| this.children = []; |
| |
| // Defined only for leaf nodes, leaf_node_id corresponds to |
| // the id of the leaf stack frame. |
| this.leaf_node_id = undefined; |
| }; |
| |
| CallTreeNode.prototype = { |
| addChild: function(node) { |
| this.children.push(node); |
| node.parent = this; |
| } |
| }; |
| |
| /** |
| * @constructor |
| */ |
| var Thread = function(thread) { |
| this.thread = thread; |
| this.rootNode = new CallTreeNode('root', 'root'); |
| this.rootCategories = {}; |
| this.sfToNode = {}; |
| this.sfToNode['__root'] = self.rootNode; |
| }; |
| |
| Thread.prototype = { |
| getCallTreeNode: function(stackFrame) { |
| if (stackFrame.id in this.sfToNode) |
| return this.sfToNode[stackFrame.id]; |
| |
| // Create & save the node. |
| var newNode = new CallTreeNode(stackFrame.title, stackFrame.category); |
| this.sfToNode[stackFrame.id] = newNode; |
| |
| // Add node to parent tree node. |
| if (stackFrame.parentFrame) { |
| var parentNode = this.getCallTreeNode(stackFrame.parentFrame); |
| |
| parentNode.addChild(newNode); |
| } else { |
| // Creating a root category node for each category helps group samples |
| // that may be missing call stacks. |
| var rootCategory = this.rootCategories[stackFrame.category]; |
| if (!rootCategory) { |
| rootCategory = |
| new CallTreeNode(stackFrame.category, stackFrame.category); |
| this.rootNode.addChild(rootCategory); |
| this.rootCategories[stackFrame.category] = rootCategory; |
| } |
| rootCategory.addChild(newNode); |
| } |
| return newNode; |
| }, |
| |
| addSample: function(sample) { |
| var leaf_node = this.getCallTreeNode(sample.leafStackFrame); |
| leaf_node.leaf_node_id = sample.leafStackFrame.id; |
| leaf_node.selfTime += sample.weight; |
| } |
| }; |
| |
| function genCallTree(node, isRoot) { |
| var ret = { |
| category: node.category, |
| name: node.name, |
| leaf_node_id: node.leaf_node_id |
| }; |
| |
| if (isRoot || node.children.length > 0) { |
| ret.children = []; |
| for (var c = 0; c < node.children.length; c++) |
| ret.children.push(genCallTree(node.children[c], false)); |
| if (node.selfTime > 0.0) { |
| // say, caller (r) calls callee (e) which calls callee2 (2) |
| // and following are the samples |
| // r r r r r r r |
| // e e e |
| // 2 2 2 |
| // In this case, r has a non-zero self time (4 samples to be precise) |
| // The <self> node makes the representation resemble the following |
| // where s denotes the selftime. |
| // r r r r r r r |
| // s e e e s s s |
| // 2 2 2 |
| // |
| // Among the obvious visualization benefit, this also creates the |
| // invariance that a node can not simultaneously have samples and |
| // children. |
| ret.children.push({ |
| name: '<self>', |
| category: ret.category, |
| size: node.selfTime, |
| leaf_node_id: node.leaf_node_id |
| }); |
| delete ret.leaf_node_id; // ret is not a leaf node anymore. |
| } |
| } |
| else { |
| ret.size = node.selfTime; |
| } |
| if (isRoot) |
| return ret.children; |
| return ret; |
| } |
| |
| function getSampleTypes(selection) { |
| var sampleDict = {}; |
| var samples = selection.getEventsOrganizedByBaseType().sample; |
| for (var i = 0; i < samples.length; i++) { |
| sampleDict[samples[i].title] = null; |
| } |
| return Object.keys(sampleDict); |
| } |
| |
| // Create sunburst data from the selection. |
| function createSunburstData(selection, sampleType) { |
| var threads = {}; |
| function getOrCreateThread(thread) { |
| var ret = undefined; |
| if (thread.tid in threads) { |
| ret = threads[thread.tid]; |
| } else { |
| ret = new Thread(thread); |
| threads[thread.tid] = ret; |
| } |
| return ret; |
| } |
| |
| // Process samples. |
| var samples = selection.getEventsOrganizedByBaseType().sample; |
| for (var i = 0; i < samples.length; i++) { |
| var sample = samples[i]; |
| if (sample.title == sampleType) |
| getOrCreateThread(sample.thread).addSample(sample); |
| } |
| |
| // Generate sunburst data. |
| var sunburstData = { |
| name: '<All Threads>', |
| category: 'root', |
| children: [] |
| }; |
| for (var t in threads) { |
| if (!threads.hasOwnProperty(t)) continue; |
| var thread = threads[t]; |
| var threadData = { |
| name: 'Thread ' + thread.thread.tid + ': ' + thread.thread.name, |
| category: 'Thread', |
| children: genCallTree(thread.rootNode, true) |
| }; |
| sunburstData.children.push(threadData); |
| } |
| return sunburstData; |
| } |
| |
| /** |
| * @constructor |
| */ |
| var SamplingSummaryPanel = |
| tr.b.ui.define('x-sample-summary-panel'); |
| SamplingSummaryPanel.textLabel = 'Sampling Summary'; |
| SamplingSummaryPanel.supportsModel = function(m) { |
| if (m == undefined) { |
| return { |
| supported: false, |
| reason: 'Unknown tracing model' |
| }; |
| } |
| |
| if (m.samples.length == 0) { |
| return { |
| supported: false, |
| reason: 'No sampling data in trace' |
| }; |
| } |
| |
| return { |
| supported: true |
| }; |
| }; |
| |
| // Return a dict with keys as stack-frame ids |
| // and values as selection objects which contain all the |
| // samples whose leaf stack-frame id matches the key. |
| function divideSamplesBasedOnLeafStackFrame(selection) { |
| var stackFrameIdToSamples = {}; |
| for (var i = 0; i < selection.length; ++i) { |
| var sample = selection[i]; |
| var id = sample.leafStackFrame.id; |
| if (!stackFrameIdToSamples[id]) |
| stackFrameIdToSamples[id] = new tr.c.Selection(); |
| stackFrameIdToSamples[id].push(sample); |
| } |
| return stackFrameIdToSamples; |
| } |
| |
| SamplingSummaryPanel.prototype = { |
| __proto__: HTMLUnknownElement.prototype, |
| |
| decorate: function() { |
| this.classList.add('x-sample-summary-panel'); |
| this.appendChild(tr.b.instantiateTemplate( |
| '#x-sample-summary-panel-template', THIS_DOC)); |
| |
| this.sampleType_ = undefined; |
| this.sampleTypeSelector_ = undefined; |
| this.chart_ = undefined; |
| this.selection_ = undefined; |
| }, |
| |
| get selection() { |
| return this.selection_; |
| }, |
| |
| set selection(selection) { |
| this.selection_ = selection; |
| this.stackFrameIdToSamples_ = divideSamplesBasedOnLeafStackFrame( |
| selection); |
| this.updateContents_(); |
| }, |
| |
| get sampleType() { |
| return this.sampleType_; |
| }, |
| |
| set sampleType(type) { |
| this.sampleType_ = type; |
| if (this.sampleTypeSelector_) |
| this.sampleTypeSelector_.selectedValue = type; |
| this.updateResultArea_(); |
| }, |
| |
| updateCallees_: function(d) { |
| // Update callee table. |
| var that = this; |
| var table = document.createElement('table'); |
| |
| // Add column styles. |
| var col0 = document.createElement('col'); |
| var col1 = document.createElement('col'); |
| col0.className += 'x-col-numeric'; |
| col1.className += 'x-col-numeric'; |
| table.appendChild(col0); |
| table.appendChild(col1); |
| |
| // Add headers. |
| var thead = table.createTHead(); |
| var headerRow = thead.insertRow(0); |
| headerRow.style.backgroundColor = '#888'; |
| headerRow.insertCell(0).appendChild(document.createTextNode('Samples')); |
| headerRow.insertCell(1).appendChild(document.createTextNode('Percent')); |
| headerRow.insertCell(2).appendChild(document.createTextNode('Symbol')); |
| |
| // Add body. |
| var tbody = table.createTBody(); |
| if (d.children) { |
| for (var i = 0; i < d.children.length; i++) { |
| var c = d.children[i]; |
| var row = tbody.insertRow(i); |
| var bgColor = getColorOfKey(c.category); |
| if (bgColor == undefined) |
| bgColor = '#444444'; |
| row.style.backgroundColor = bgColor; |
| var cell0 = row.insertCell(0); |
| var cell1 = row.insertCell(1); |
| var cell2 = row.insertCell(2); |
| cell0.className += 'x-td-numeric'; |
| cell1.className += 'x-td-numeric'; |
| cell0.appendChild(document.createTextNode(c.value.toString())); |
| cell1.appendChild(document.createTextNode( |
| (100 * c.value / d.value).toFixed(2) + '%')); |
| cell2.appendChild(document.createTextNode(c.name)); |
| } |
| } |
| |
| // Make it sortable. |
| tr.b.ui.SortableTable.decorate(table); |
| |
| var calleeArea = that.querySelector('x-callees'); |
| calleeArea.textContent = ''; |
| calleeArea.appendChild(table); |
| }, |
| |
| updateHighlight_: function(d) { |
| var that = this; |
| |
| // Update explanation. |
| var percent = 100.0; |
| if (that.chart_.selectedNode != null) |
| percent = 100.0 * d.value / that.chart_.selectedNode.value; |
| that.querySelector('x-explanation').innerHTML = |
| d.value + '<br>' + percent.toFixed(2) + '%'; |
| |
| // Update call stack table. |
| var table = document.createElement('table'); |
| var thead = table.createTHead(); |
| var tbody = table.createTBody(); |
| var headerRow = thead.insertRow(0); |
| headerRow.style.backgroundColor = '#888'; |
| headerRow.insertCell(0).appendChild( |
| document.createTextNode('Call Stack')); |
| |
| var callStack = []; |
| var frame = d; |
| while (frame && frame.id) { |
| callStack.push(frame); |
| frame = frame.parent; |
| } |
| |
| for (var i = 0; i < callStack.length; i++) { |
| var row = tbody.insertRow(i); |
| var bgColor = getColorOfKey(callStack[i].category); |
| if (bgColor == undefined) |
| bgColor = '#444444'; |
| row.style.backgroundColor = bgColor; |
| if (i == 0) |
| row.style.fontWeight = 'bold'; |
| row.insertCell(0).appendChild( |
| document.createTextNode(callStack[i].name)); |
| } |
| |
| var sequenceArea = that.querySelector('x-sequence'); |
| sequenceArea.textContent = ''; |
| sequenceArea.appendChild(table); |
| }, |
| |
| getSamplesFromNode_: function(node) { |
| // A node has samples associated with it, if it's a leaf node. |
| var selection = new tr.c.Selection(); |
| if (node.leaf_node_id !== undefined) { |
| selection.addSelection(this.stackFrameIdToSamples_[node.leaf_node_id]); |
| } |
| else if (node.children === undefined || |
| node.children.length === 0) { |
| throw new Error('A node should either have samples, or children'); |
| } |
| else { |
| for (var i = 0; i < node.children.length; ++i) |
| selection.addSelection(this.getSamplesFromNode_(node.children[i])); |
| } |
| return selection; |
| }, |
| |
| updateResultArea_: function() { |
| if (this.selection_ === undefined) |
| return; |
| |
| var resultArea = this.querySelector('result-area'); |
| this.chart_ = undefined; |
| resultArea.textContent = ''; |
| |
| var sunburstData = |
| createSunburstData(this.selection_, this.sampleType_); |
| this.chart_ = new tr.b.ui.SunburstChart(); |
| this.chart_.width = 600; |
| this.chart_.height = 600; |
| this.chart_.chartTitle = 'Sampling Summary'; |
| |
| this.chart_.addEventListener('node-selected', (function(e) { |
| this.updateCallees_(e.node); |
| }).bind(this)); |
| |
| this.chart_.addEventListener('node-clicked', (function(e) { |
| var event = new RequestSelectionChangeEvent(); |
| var depth = e.node.depth; |
| if (e.node.name === '<self>') |
| depth--; |
| event.selection = this.getSamplesFromNode_(e.node); |
| event.selection.sunburst_zoom_level = depth; |
| this.dispatchEvent(event); |
| }).bind(this)); |
| |
| this.chart_.addEventListener('node-highlighted', (function(e) { |
| this.updateHighlight_(e.node); |
| }).bind(this)); |
| |
| this.chart_.data = { |
| nodes: sunburstData |
| }; |
| |
| resultArea.appendChild(this.chart_); |
| this.chart_.setSize(this.chart_.getMinSize()); |
| |
| if (this.selection_.sunburst_zoom_level !== undefined) { |
| this.chart_.zoomToDepth(this.selection_.sunburst_zoom_level); |
| } |
| }, |
| |
| updateContents_: function() { |
| if (this.selection_ === undefined || this.selection_.length == 0) |
| return; |
| |
| // Get available sample types in range. |
| var sampleTypes = getSampleTypes(this.selection_); |
| if (sampleTypes.indexOf(this.sampleType_) == -1) |
| this.sampleType_ = sampleTypes[0]; |
| |
| // Create sample type dropdown. |
| var sampleTypeOptions = []; |
| for (var i = 0; i < sampleTypes.length; i++) |
| sampleTypeOptions.push({label: sampleTypes[i], value: sampleTypes[i]}); |
| |
| var toolbarEl = this.querySelector('x-toolbar'); |
| this.sampleTypeSelector_ = tr.b.ui.createSelector( |
| this, |
| 'sampleType', |
| 'samplingSummaryPanel.sampleType', |
| this.sampleType_, |
| sampleTypeOptions); |
| toolbarEl.textContent = 'Sample Type: '; |
| toolbarEl.appendChild(this.sampleTypeSelector_); |
| } |
| }; |
| |
| return { |
| SamplingSummaryPanel: SamplingSummaryPanel, |
| createSunburstData: createSunburstData |
| }; |
| }); |
| </script> |