blob: afa2e5b6f0e210bbe55c695eeff8dd36c186504e [file] [log] [blame]
<!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/iteration_helpers.html">
<link rel="import" href="/base/statistics.html">
<link rel="import" href="/model/event_set.html">
<link rel="import" href="/ui/base/dom_helpers.html">
<link rel="import" href="/ui/base/pie_chart.html">
<link rel="import" href="/ui/base/sortable_table.html">
<link rel="import" href="/ui/base/sunburst_chart.html">
<link rel="import" href="/ui/base/utils.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.ui.e.analysis', function() {
var THIS_DOC = document.currentScript.ownerDocument;
var RequestSelectionChangeEvent = tr.model.RequestSelectionChangeEvent;
var getColorOfKey = tr.ui.b.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.ui.b.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.model.EventSet();
stackFrameIdToSamples[id].push(sample);
}
return stackFrameIdToSamples;
}
SamplingSummaryPanel.prototype = {
__proto__: HTMLUnknownElement.prototype,
decorate: function() {
this.classList.add('x-sample-summary-panel');
this.appendChild(tr.ui.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.ui.b.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.model.EventSet();
if (node.leaf_node_id !== undefined) {
selection.addEventSet(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.addEventSet(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.ui.b.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.ui.b.createSelector(
this,
'sampleType',
'samplingSummaryPanel.sampleType',
this.sampleType_,
sampleTypeOptions);
toolbarEl.textContent = 'Sample Type: ';
toolbarEl.appendChild(this.sampleTypeSelector_);
}
};
return {
SamplingSummaryPanel: SamplingSummaryPanel,
createSunburstData: createSunburstData
};
});
</script>