| <!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> |