Index: tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html |
diff --git a/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html b/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html |
index ac842ce8d61ff85d89a7eedac85c59a3bb250221..7b53e66ab5cfa3f0693b40460410aec31fcff5c4 100644 |
--- a/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html |
+++ b/tracing/tracing/ui/analysis/memory_dump_heap_details_pane.html |
@@ -8,12 +8,14 @@ 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_heap_details_breakdown_view.html"> |
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_path_view.html"> |
+<link rel="import" href="/tracing/ui/analysis/memory_dump_heap_details_util.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/drag_handle.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'> |
@@ -38,7 +40,7 @@ found in the LICENSE file. |
#label { |
flex: 1 1 auto; |
padding: 8px; |
- font-size: 15px; |
+ font-size: 15px; |
font-weight: bold; |
} |
@@ -62,11 +64,24 @@ found in the LICENSE file. |
text-align: center; |
} |
- #table { |
+ #split_view { |
display: none; /* Hide until memory allocator dumps are set. */ |
flex: 1 0 auto; |
align-self: stretch; |
- font-size: 12px; |
+ flex-direction: row; |
+ } |
+ |
+ #path_view { |
+ width: 50%; |
+ } |
+ |
+ #breakdown_view { |
+ flex: 1 1 auto; |
+ width: 0; |
+ } |
+ |
+ #path_view, #breakdown_view { |
+ overflow-x: auto; /* Show scrollbar if necessary. */ |
} |
</style> |
<div id="header"> |
@@ -79,8 +94,16 @@ found in the LICENSE file. |
<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 id="split_view"> |
+ <tr-ui-a-memory-dump-heap-details-path-view id="path_view"> |
+ </tr-ui-a-memory-dump-heap-details-path-view> |
+ <tr-ui-b-drag-handle id="drag_handle"></tr-ui-b-drag-handle> |
+ <tr-ui-a-memory-dump-heap-details-breakdown-view id="breakdown_view"> |
+ </tr-ui-a-memory-dump-heap-details-breakdown-view> |
+ </div> |
</div> |
</template> |
</dom-module> |
@@ -96,107 +119,160 @@ tr.exportTo('tr.ui.analysis', function() { |
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); |
+ function HeapDumpTreeNode( |
+ stackFrameNodes, dimension, title, heavyView, parentNode) { |
+ this.dimension = dimension; |
+ this.title = title; |
+ this.parentNode = parentNode; |
+ |
+ this.heavyView_ = heavyView; |
+ this.stackFrameNodes_ = stackFrameNodes; |
+ this.lazyCells_ = undefined; |
+ this.lazyChildNodes_ = undefined; |
} |
- HeapDumpNodeTitleColumn.prototype = { |
- __proto__: tr.ui.analysis.TitleColumn.prototype, |
+ HeapDumpTreeNode.prototype = { |
+ 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; |
+ } |
+ }, |
- formatTitle: function(row) { |
- var title = row.title; |
- var dimension = row.dimension; |
- switch (dimension) { |
- case RowDimension.ROOT: |
- return title; |
+ get childNodes() { |
+ if (!this.lazyChildNodes_) { |
+ this.lazyChildNodes_ = new Map(); |
+ this.addDimensionChildNodes_( |
+ tr.ui.analysis.HeapDetailsRowDimension.STACK_FRAME, 0); |
+ this.addDimensionChildNodes_( |
+ tr.ui.analysis.HeapDetailsRowDimension.OBJECT_TYPE, 1); |
+ this.releaseStackFrameNodesIfPossible_(); |
+ } |
+ return this.lazyChildNodes_; |
+ }, |
- case RowDimension.STACK_FRAME: |
- case RowDimension.OBJECT_TYPE: |
- return this.formatSubRow_(title, dimension); |
+ get cells() { |
+ if (!this.lazyCells_) { |
+ this.addCells_(); |
+ this.releaseStackFrameNodesIfPossible_(); |
+ } |
+ return this.lazyCells_; |
+ }, |
- default: |
- throw new Error('Invalid row dimension: ' + row.dimension); |
+ releaseStackFrameNodesIfPossible_: function() { |
+ if (this.lazyCells_ && this.lazyChildNodes_) { |
+ // Don't unnecessarily hold a reference to the stack frame nodes when |
+ // we don't need them anymore. |
+ this.stackFrameNodes_ = undefined; |
} |
}, |
- 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); |
+ addDimensionChildNodes_: function(dimension, dimensionIndex) { |
+ // Child title -> Timestamp (list index) -> Child |
+ // MultiDimensionalViewNode. |
+ var dimensionChildTitleToStackFrameNodes = tr.b.invertArrayOfDicts( |
+ this.stackFrameNodes_, |
+ node => this.convertStackFrameNodeDimensionToChildDict_( |
+ node, dimensionIndex)); |
+ |
+ // Child title (list index) -> Child HeapDumpTreeNode. |
+ var dimensionChildNodes = []; |
+ tr.b.iterItems(dimensionChildTitleToStackFrameNodes, |
+ function(childTitle, childStackFrameNodes) { |
+ dimensionChildNodes.push(new HeapDumpTreeNode(childStackFrameNodes, |
+ dimension, childTitle, this.heavyView_, this)); |
+ }, this); |
+ this.lazyChildNodes_.set(dimension, dimensionChildNodes); |
}, |
- formatSubRow_: function(title, dimension) { |
- var titleEl = document.createElement('span'); |
+ convertStackFrameNodeDimensionToChildDict_: function( |
+ stackFrameNode, dimensionIndex) { |
+ var childDict = {}; |
+ |
+ var displayedChildrenTotalSize = 0; |
+ var displayedChildrenTotalCount = 0; |
+ var hasDisplayedChildren = false; |
+ var allDisplayedChildrenHaveDisplayedCounts = true; |
+ for (var child of stackFrameNode.children[dimensionIndex].values()) { |
+ if (child.values[0].totalState < this.minDisplayedTotalState_) |
+ continue; |
+ if (child.values[1].totalState < this.minDisplayedTotalState_) |
+ allDisplayedChildrenHaveDisplayedCounts = false; |
+ childDict[child.title[dimensionIndex]] = child; |
+ displayedChildrenTotalSize += child.values[0].total; |
+ displayedChildrenTotalCount += child.values[1].total; |
+ hasDisplayedChildren = true; |
+ } |
- 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'; |
+ var nodeTotalSize = stackFrameNode.values[0].total; |
+ var nodeTotalCount = stackFrameNode.values[1].total; |
+ |
+ // Add '<other>' node if necessary in tree-view. |
+ var hasUnclassifiedSizeOrCount = |
+ displayedChildrenTotalSize < nodeTotalSize || |
+ displayedChildrenTotalCount < nodeTotalCount; |
+ if (!this.heavyView_ && hasUnclassifiedSizeOrCount && |
+ hasDisplayedChildren) { |
+ var otherTitle = stackFrameNode.title.slice(); |
+ otherTitle[dimensionIndex] = '<other>'; |
+ var otherNode = new tr.b.MultiDimensionalViewNode(otherTitle, 2); |
+ childDict[otherTitle[dimensionIndex]] = otherNode; |
+ |
+ // '<other>' node size. |
+ otherNode.values[0].total = nodeTotalSize - displayedChildrenTotalSize; |
+ otherNode.values[0].totalState = this.minDisplayedTotalState_; |
+ |
+ // '<other>' node allocation count. |
+ otherNode.values[1].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. |
+ otherNode.values[1].totalState = |
+ allDisplayedChildrenHaveDisplayedCounts ? |
+ this.minDisplayedTotalState_ : TotalState.NOT_PROVIDED; |
} |
- 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 childDict; |
+ }, |
- return titleEl; |
+ addCells_: function() { |
+ // Transform a chronological list of heap stack frame tree nodes into a |
+ // dictionary of cells (where each cell contains a chronological list |
+ // of the values of its numeric). |
+ this.lazyCells_ = tr.ui.analysis.createCells(this.stackFrameNodes_, |
+ function(stackFrameNode) { |
+ var size = stackFrameNode.values[0].total; |
+ var numerics = { |
+ 'Size': new ScalarNumeric(sizeInBytes_smallerIsBetter, size) |
+ }; |
+ var countValue = stackFrameNode.values[1]; |
+ if (countValue.totalState >= this.minDisplayedTotalState_) { |
+ var count = countValue.total; |
+ numerics['Count'] = new ScalarNumeric( |
+ count_smallerIsBetter, count); |
+ numerics['Average size per allocation'] = new ScalarNumeric( |
+ sizeInBytes_smallerIsBetter, count === 0 ? 0 : size / count); |
+ } |
+ return numerics; |
+ }, this); |
} |
}; |
- var COLUMN_RULES = [ |
- { |
- condition: 'Size', |
- importance: 3, |
- columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn |
- }, |
- { |
- condition: 'Count', |
- importance: 2, |
- columnConstructor: tr.ui.analysis.DetailsNumericMemoryColumn |
- }, |
- { |
- 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; |
+ this.aggregationMode_ = 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).'; |
@@ -211,15 +287,30 @@ tr.exportTo('tr.ui.analysis', function() { |
}, |
{ |
label: 'Top-down (Heavy)', |
- value: MultiDimensionalViewBuilder.ViewType |
- .TOP_DOWN_HEAVY_VIEW |
+ value: |
+ MultiDimensionalViewBuilder.ViewType.TOP_DOWN_HEAVY_VIEW |
}, |
{ |
label: 'Bottom-up (Heavy)', |
- value: MultiDimensionalViewBuilder.ViewType |
- .BOTTOM_UP_HEAVY_VIEW |
+ value: |
+ MultiDimensionalViewBuilder.ViewType.BOTTOM_UP_HEAVY_VIEW |
} |
])); |
+ |
+ this.$.drag_handle.target = this.$.path_view; |
+ this.$.drag_handle.horizontal = false; |
+ |
+ // If the user selects a node in the path view, show its children in the |
+ // breakdown view. |
+ this.$.path_view.addEventListener('selected-node-changed', (function(e) { |
+ this.$.breakdown_view.displayedNode = this.$.path_view.selectedNode; |
+ }).bind(this)); |
+ |
+ // If the user double-clicks on a node in the breakdown view, select the |
+ // node in the path view. |
+ this.$.breakdown_view.addEventListener('enter-node', (function(e) { |
+ this.$.path_view.selectedNode = e.node; |
+ }).bind(this)); |
}, |
/** |
@@ -246,7 +337,8 @@ tr.exportTo('tr.ui.analysis', function() { |
set aggregationMode(aggregationMode) { |
this.aggregationMode_ = aggregationMode; |
- this.scheduleRebuild_(); |
+ this.$.path_view.aggregationMode = aggregationMode; |
+ this.$.breakdown_view.aggregationMode = aggregationMode; |
}, |
get aggregationMode() { |
@@ -277,31 +369,39 @@ tr.exportTo('tr.ui.analysis', function() { |
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.$.split_view.style.display = 'none'; |
this.$.view_mode_container.style.display = 'none'; |
this.$.info_bar.visible = false; |
- |
- this.$.table.clear(); |
- this.$.table.rebuild(); |
+ this.$.path_view.selectedNode = undefined; |
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.$.split_view.style.display = 'flex'; |
this.$.view_mode_container.style.display = 'block'; |
// Show the info bar if in heavy view mode. |
this.$.info_bar.visible = this.heavyView; |
+ this.$.path_view.selectedNode = this.createHeapTree_(); |
+ this.$.path_view.rebuild(); |
+ this.$.breakdown_view.rebuild(); |
+ }, |
+ |
+ createHeapTree_: function() { |
+ var definedHeapDump = tr.b.findFirstInArray(this.heapDumps_); |
+ if (definedHeapDump === undefined) |
+ return undefined; |
+ |
+ // The title of the root node is the name of the allocator. |
+ var rootRowTitle = definedHeapDump.allocatorName; |
+ |
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); |
+ return new HeapDumpTreeNode(stackFrameTrees, |
+ tr.ui.analysis.HeapDetailsRowDimension.ROOT, rootRowTitle, |
+ this.heavyView); |
}, |
createStackFrameTrees_: function(heapDumps) { |
@@ -330,151 +430,9 @@ tr.exportTo('tr.ui.analysis', function() { |
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(count_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, { |
- cellKey: 'cells', |
- aggregationMode: this.aggregationMode_, |
- rules: COLUMN_RULES |
- }); |
- tr.ui.analysis.MemoryColumn.spaceEqually(numericColumns); |
- |
- var columns = [titleColumn].concat(numericColumns); |
- return columns; |
} |
- }); |
+ }); |
- return { |
- // Exported for testing. |
- RowDimension: RowDimension |
- }; |
+ return {}; |
}); |
</script> |