blob: e04383c2892f6eb0da16c435b9667fb77906c996 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2015 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="/tracing/base/color_scheme.html">
<link rel="import" href="/tracing/base/iteration_helpers.html">
<link rel="import" href="/tracing/base/multi_dimensional_view.html">
<link rel="import" href="/tracing/base/unit.html">
<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
<link rel="import" href="/tracing/ui/base/dom_helpers.html">
<link rel="import" href="/tracing/ui/base/info_bar.html">
<link rel="import" href="/tracing/ui/base/table.html">
<link rel="import" href="/tracing/value/numeric.html">
<dom-module id='tr-ui-a-memory-dump-heap-details-pane'>
<template>
<style>
:host {
display: flex;
flex-direction: column;
}
#header {
flex: 0 0 auto;
display: flex;
flex-direction: row;
align-items: center;
background-color: #eee;
border-bottom: 1px solid #8e8e8e;
border-top: 1px solid white;
}
#label {
flex: 1 1 auto;
padding: 8px;
font-size: 15px;
font-weight: bold;
}
#view_mode_container {
display: none;
flex: 0 0 auto;
padding: 5px;
font-size: 15px;
}
#contents {
flex: 1 0 auto;
align-self: stretch;
font-size: 12px;
}
#info_text {
padding: 8px;
color: #666;
font-style: italic;
text-align: center;
}
#table {
display: none; /* Hide until memory allocator dumps are set. */
flex: 1 0 auto;
align-self: stretch;
}
</style>
<div id="header">
<div id="label">Heap details</div>
<div id="view_mode_container">
<span>View mode:</span>
<!-- View mode selector (added in Polymer.ready()) -->
</div>
</div>
<div id="contents">
<tr-ui-b-info-bar id="info_bar" class="info-bar-hidden">
</tr-ui-b-info-bar>
<div id="info_text">No heap dump selected</div>
<tr-ui-b-table id="table"></tr-ui-b-table>
</div>
</template>
</dom-module>
<script>
'use strict';
tr.exportTo('tr.ui.analysis', function() {
var ScalarNumeric = tr.v.ScalarNumeric;
var sizeInBytes_smallerIsBetter =
tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
var unitlessNumber_smallerIsBetter =
tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
var MultiDimensionalViewBuilder = tr.b.MultiDimensionalViewBuilder;
var TotalState = tr.b.MultiDimensionalViewNode.TotalState;
/** @{enum} */
var RowDimension = {
ROOT: -1,
STACK_FRAME: 0,
OBJECT_TYPE: 1
};
var LATIN_SMALL_LETTER_F_WITH_HOOK = String.fromCharCode(0x0192);
var CIRCLED_LATIN_CAPITAL_LETTER_T = String.fromCharCode(0x24C9);
/** @{constructor} */
function HeapDumpNodeTitleColumn(title) {
tr.ui.analysis.TitleColumn.call(this, title);
}
HeapDumpNodeTitleColumn.prototype = {
__proto__: tr.ui.analysis.TitleColumn.prototype,
formatTitle: function(row) {
var title = row.title;
var dimension = row.dimension;
switch (dimension) {
case RowDimension.ROOT:
return title;
case RowDimension.STACK_FRAME:
case RowDimension.OBJECT_TYPE:
return this.formatSubRow_(title, dimension);
default:
throw new Error('Invalid row dimension: ' + row.dimension);
}
},
cmp: function(rowA, rowB) {
if (rowA.dimension !== rowB.dimension)
return rowA.dimension - rowB.dimension;
return tr.ui.analysis.TitleColumn.prototype.cmp.call(this, rowA, rowB);
},
formatSubRow_: function(title, dimension) {
var titleEl = document.createElement('span');
var symbolEl = document.createElement('span');
var symbolColorName;
if (dimension === RowDimension.STACK_FRAME) {
Polymer.dom(symbolEl).textContent = LATIN_SMALL_LETTER_F_WITH_HOOK;
symbolEl.title = 'Stack frame';
symbolColorName = 'heap_dump_stack_frame';
} else {
Polymer.dom(symbolEl).textContent = CIRCLED_LATIN_CAPITAL_LETTER_T;
symbolEl.title = 'Object type';
symbolColorName = 'heap_dump_object_type';
}
symbolEl.style.color =
tr.b.ColorScheme.getColorForReservedNameAsString(symbolColorName);
symbolEl.style.paddingRight = '4px';
symbolEl.style.cursor = 'help';
symbolEl.style.weight = 'bold';
Polymer.dom(titleEl).appendChild(symbolEl);
Polymer.dom(titleEl).appendChild(document.createTextNode(title));
return titleEl;
}
};
/** @constructor */
function AllocationCountColumn(name, cellPath, aggregationMode) {
tr.ui.analysis.DetailsNumericMemoryColumn.call(
this, name, cellPath, aggregationMode);
}
AllocationCountColumn.prototype = {
__proto__: tr.ui.analysis.DetailsNumericMemoryColumn.prototype,
getFormattingContext: function(unit) {
return { minimumFractionDigits: 0 };
}
};
var COLUMN_RULES = [
{
condition: 'Size',
importance: 3,
columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
},
{
condition: 'Count',
importance: 2,
columnConstructor: AllocationCountColumn
},
{
condition: 'Average size per allocation',
importance: 1,
columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
},
{
importance: 0,
columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn
}
];
Polymer({
is: 'tr-ui-a-memory-dump-heap-details-pane',
behaviors: [tr.ui.analysis.StackedPane],
created: function() {
this.heapDumps_ = undefined;
this.aggregationMode_ = undefined;
this.viewMode_ = undefined;
},
ready: function() {
this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.ROW;
this.$.info_bar.message = 'Note: Values displayed in the heavy view ' +
'are lower bounds (except for the root).';
Polymer.dom(this.$.view_mode_container).appendChild(
tr.ui.b.createSelector(
this, 'viewMode', 'memoryDumpHeapDetailsPane.viewMode',
MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW,
[
{
label: 'Top-down (Tree)',
value: MultiDimensionalViewBuilder.ViewType.TOP_DOWN_TREE_VIEW
},
{
label: 'Top-down (Heavy)',
value: MultiDimensionalViewBuilder.ViewType
.TOP_DOWN_HEAVY_VIEW
},
{
label: 'Bottom-up (Heavy)',
value: MultiDimensionalViewBuilder.ViewType
.BOTTOM_UP_HEAVY_VIEW
}
]));
},
/**
* Sets the heap dumps and schedules rebuilding the pane.
*
* The provided value should be a chronological list of heap dumps. All
* dumps are assumed to belong to the same process and belong to the same
* allocator. Example:
*
* [
* tr.model.HeapDump {}, // Heap dump at timestamp 1.
* undefined, // Heap dump not provided at timestamp 2.
* tr.model.HeapDump {}, // Heap dump at timestamp 3.
* ]
*/
set heapDumps(heapDumps) {
this.heapDumps_ = heapDumps;
this.scheduleRebuild_();
},
get heapDumps() {
return this.heapDumps_;
},
set aggregationMode(aggregationMode) {
this.aggregationMode_ = aggregationMode;
this.scheduleRebuild_();
},
get aggregationMode() {
return this.aggregationMode_;
},
set viewMode(viewMode) {
this.viewMode_ = viewMode;
this.scheduleRebuild_();
},
get viewMode() {
return this.viewMode_;
},
get heavyView() {
switch (this.viewMode) {
case MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW:
case MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW:
return true;
default:
return false;
}
},
onRebuild_: function() {
if (this.heapDumps_ === undefined ||
this.heapDumps_.length === 0) {
// Show the info text (hide the table and the view mode selector).
this.$.info_text.style.display = 'block';
this.$.table.style.display = 'none';
this.$.view_mode_container.style.display = 'none';
this.$.info_bar.visible = false;
this.$.table.clear();
this.$.table.rebuild();
return;
}
// Show the table and the view mode selector (hide the info text).
this.$.info_text.style.display = 'none';
this.$.table.style.display = 'block';
this.$.view_mode_container.style.display = 'block';
// Show the info bar if in heavy view mode.
this.$.info_bar.visible = this.heavyView;
var stackFrameTrees = this.createStackFrameTrees_(this.heapDumps_);
var rows = this.createRows_(stackFrameTrees);
var columns = this.createColumns_(rows);
this.$.table.tableRows = rows;
this.$.table.tableColumns = columns;
this.$.table.rebuild();
tr.ui.analysis.expandTableRowsRecursively(this.$.table);
},
createStackFrameTrees_: function(heapDumps) {
return heapDumps.map(function(heapDump) {
if (heapDump === undefined)
return undefined;
var builder = new MultiDimensionalViewBuilder(
2 /* dimensions (stack frames, object type) */,
2 /* valueCount (size, count) */);
// Build the heap tree.
heapDump.entries.forEach(function(entry) {
var leafStackFrame = entry.leafStackFrame;
var stackTracePath = leafStackFrame === undefined ?
[] : leafStackFrame.getUserFriendlyStackTrace().reverse();
var objectTypeName = entry.objectTypeName;
var objectTypeNamePath = objectTypeName === undefined ?
[] : [objectTypeName];
builder.addPath([stackTracePath, objectTypeNamePath],
[entry.size, entry.count],
MultiDimensionalViewBuilder.ValueKind.TOTAL);
}, this);
return builder.buildView(this.viewMode);
}, this);
},
createRows_: function(stackFrameTrees) {
var definedHeapDump = tr.b.findFirstInArray(this.heapDumps);
if (definedHeapDump === undefined)
return [];
// The title of the root row is the name of the allocator.
var rootRowTitle = definedHeapDump.allocatorName;
return [this.createHeapRowRecursively_(
stackFrameTrees, RowDimension.ROOT, rootRowTitle)];
},
createHeapRowRecursively_: function(nodes, dimension, title) {
// Transform a chronological list of stack frame tree nodes into a
// dictionary of cells (where each cell contains a chronological list
// of the values of its numeric).
var cells = tr.ui.analysis.createCells(nodes, function(node) {
var size = node.values[0].total;
var row = {
'Size': new ScalarNumeric(sizeInBytes_smallerIsBetter, size)
};
var countValue = node.values[1];
if (countValue.totalState >= this.minDisplayedTotalState_) {
var count = countValue.total;
row['Count'] = new ScalarNumeric(unitlessNumber_smallerIsBetter,
count);
row['Average size per allocation'] = new ScalarNumeric(
sizeInBytes_smallerIsBetter, count === 0 ? 0 : size / count);
}
return row;
}, this);
var row = {
dimension: dimension,
title: title,
contexts: nodes,
cells: cells
};
// Recursively create sub-rows for children (if applicable).
var stackFrameSubRows = this.createHeapDimensionSubRowsRecursively_(
nodes, RowDimension.STACK_FRAME);
var objectTypeSubRows = this.createHeapDimensionSubRowsRecursively_(
nodes, RowDimension.OBJECT_TYPE);
var subRows = stackFrameSubRows.concat(objectTypeSubRows);
if (subRows.length > 0)
row.subRows = subRows;
return row;
},
get minDisplayedTotalState_() {
if (this.heavyView) {
// Show lower-bound and exact values in heavy views.
return TotalState.LOWER_BOUND;
} else {
// Show only exact values in tree view.
return TotalState.EXACT;
}
},
createHeapDimensionSubRowsRecursively_: function(nodes, dimension) {
// Sub-row name (list index) -> Timestamp (list index) -> Child
// MultiDimensionalViewNode.
var dimensionGroupedChildNodes = tr.b.dictionaryValues(
tr.b.invertArrayOfDicts(nodes, function(node) {
var childDict = {};
var displayedChildrenTotalSize = 0;
var displayedChildrenTotalCount = 0;
var hasDisplayedChildren = false;
var allDisplayedChildrenHaveDisplayedCounts = true;
for (var child of node.children[dimension].values()) {
if (child.values[0].totalState < this.minDisplayedTotalState_)
continue;
if (child.values[1].totalState < this.minDisplayedTotalState_)
allDisplayedChildrenHaveDisplayedCounts = false;
childDict[child.title[dimension]] = child;
displayedChildrenTotalSize += child.values[0].total;
displayedChildrenTotalCount += child.values[1].total;
hasDisplayedChildren = true;
}
var nodeTotalSize = node.values[0].total;
var nodeTotalCount = node.values[1].total;
// Add '<other>' node if necessary in tree-view.
var hasUnclassifiedSizeOrCount =
displayedChildrenTotalSize < nodeTotalSize ||
displayedChildrenTotalCount < nodeTotalCount;
if (!this.heavyView && hasUnclassifiedSizeOrCount &&
hasDisplayedChildren) {
var otherTitle = node.title.slice();
otherTitle[dimension] = '<other>';
childDict['<other>'] = {
title: otherTitle,
values: [
{
self: 0,
total: nodeTotalSize - displayedChildrenTotalSize,
totalState: this.minDisplayedTotalState_
},
{
self: 0,
total: nodeTotalCount - displayedChildrenTotalCount,
// Don't show allocation count of the '<other>' node if
// there is a displayed child node that did NOT display
// allocation count.
totalState: allDisplayedChildrenHaveDisplayedCounts ?
this.minDisplayedTotalState_ : TotalState.NOT_PROVIDED
}
],
children: [new Map(), new Map()]
};
}
return childDict;
}, this));
// Sub-row name (list index) -> Sub-row.
return dimensionGroupedChildNodes.map(function(subRowNodes) {
var subRowTitle = tr.b.findFirstInArray(subRowNodes).title[dimension];
return this.createHeapRowRecursively_(
subRowNodes, dimension, subRowTitle);
}, this);
},
createColumns_: function(rows) {
var titleColumn = new HeapDumpNodeTitleColumn('Stack frame');
titleColumn.width = '500px';
var numericColumns = tr.ui.analysis.MemoryColumn.fromRows(
rows, 'cells', this.aggregationMode_, COLUMN_RULES);
tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns);
var columns = [titleColumn].concat(numericColumns);
return columns;
}
});
return {
// Exported for testing.
RowDimension: RowDimension,
AllocationCountColumn: AllocationCountColumn
};
});
</script>