blob: a7753395d4eee1d8b5ae6ef9061f6228347dfa71 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 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/unit.html">
<link rel="import" href="/tracing/base/unit_scale.html">
<link rel="import" href="/tracing/model/memory_allocator_dump.html">
<link rel="import"
href="/tracing/ui/analysis/memory_dump_allocator_details_pane.html">
<link rel="import" href="/tracing/ui/analysis/memory_dump_sub_view_util.html">
<link rel="import"
href="/tracing/ui/analysis/memory_dump_vm_regions_details_pane.html">
<link rel="import" href="/tracing/ui/analysis/stacked_pane.html">
<link rel="import" href="/tracing/ui/base/color_legend.html">
<link rel="import" href="/tracing/ui/base/dom_helpers.html">
<link rel="import" href="/tracing/ui/base/table.html">
<link rel="import" href="/tracing/ui/view_specific_brushing_state.html">
<link rel="import" href="/tracing/value/numeric.html">
<dom-module id='tr-ui-a-memory-dump-overview-pane'>
<template>
<style>
:host {
display: flex;
flex-direction: column;
}
#label {
flex: 0 0 auto;
padding: 8px;
background-color: #eee;
border-bottom: 1px solid #8e8e8e;
border-top: 1px solid white;
font-size: 15px;
font-weight: bold;
}
#contents {
flex: 1 0 auto;
align-self: stretch;
font-size: 12px;
overflow: auto;
}
#info_text {
padding: 8px;
color: #666;
font-style: italic;
text-align: center;
}
#table {
display: none; /* Hide until memory dumps are set. */
flex: 1 0 auto;
align-self: stretch;
}
</style>
<tr-ui-b-view-specific-brushing-state id="state"
view-id="analysis.memory_dump_overview_pane">
</tr-ui-b-view-specific-brushing-state>
<div id="label">Overview</div>
<div id="contents">
<div id="info_text">No memory memory dumps 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 ColorScheme = tr.b.ColorScheme;
var ScalarNumeric = tr.v.ScalarNumeric;
var sizeInBytes_smallerIsBetter =
tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
var PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX = '_bytes';
var DISPLAYED_SIZE_NUMERIC_NAME =
tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
var SOME_TIMESTAMPS_INFO_QUANTIFIER =
tr.ui.analysis.MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER;
// Unicode symbols used for memory cell info icons and messages.
var RIGHTWARDS_ARROW_WITH_HOOK = String.fromCharCode(0x21AA);
var RIGHTWARDS_ARROW_FROM_BAR = String.fromCharCode(0x21A6);
var GREATER_THAN_OR_EQUAL_TO = String.fromCharCode(0x2265);
var UNMARRIED_PARTNERSHIP_SYMBOL = String.fromCharCode(0x26AF);
var TRIGRAM_FOR_HEAVEN = String.fromCharCode(0x2630);
// TODO(petrcermak): Move this to iteration_helpers.html.
function lazyMap(list, fn, opt_this) {
opt_this = opt_this || this;
var result = undefined;
list.forEach(function(item, index) {
var value = fn.call(opt_this, item, index);
if (value === undefined)
return;
if (result === undefined)
result = new Array(list.length);
result[index] = value;
});
return result;
}
/** @constructor */
function ProcessNameColumn() {
tr.ui.analysis.TitleColumn.call(this, 'Process');
}
ProcessNameColumn.prototype = {
__proto__: tr.ui.analysis.TitleColumn.prototype,
formatTitle: function(row) {
if (row.contexts === undefined)
return row.title; // Total row.
var titleEl = document.createElement('tr-ui-b-color-legend');
titleEl.label = row.title;
return titleEl;
}
};
/** @constructor */
function UsedMemoryColumn(name, cellPath, aggregationMode) {
tr.ui.analysis.NumericMemoryColumn.call(
this, name, cellPath, aggregationMode);
}
UsedMemoryColumn.COLOR =
ColorScheme.getColorForReservedNameAsString('used_memory_column');
UsedMemoryColumn.OLDER_COLOR =
ColorScheme.getColorForReservedNameAsString('older_used_memory_column');
UsedMemoryColumn.prototype = {
__proto__: tr.ui.analysis.NumericMemoryColumn.prototype,
get title() {
return tr.ui.b.createSpan({
textContent: this.name,
color: UsedMemoryColumn.COLOR
});
},
getFormattingContext: function(unit) {
return { unitPrefix: tr.b.UnitScale.Binary.MEBI };
},
color: function(numerics, processMemoryDumps) {
return UsedMemoryColumn.COLOR;
},
getChildPaneBuilder: function(processMemoryDumps) {
if (processMemoryDumps === undefined)
return undefined;
var vmRegions = lazyMap(processMemoryDumps, function(pmd) {
if (pmd === undefined)
return undefined;
return pmd.mostRecentVmRegions;
});
if (vmRegions === undefined)
return undefined;
return function() {
var pane = document.createElement(
'tr-ui-a-memory-dump-vm-regions-details-pane');
pane.vmRegions = vmRegions;
pane.aggregationMode = this.aggregationMode;
return pane;
}.bind(this);
}
};
/** @constructor */
function PeakMemoryColumn(name, cellPath, aggregationMode) {
UsedMemoryColumn.call(this, name, cellPath, aggregationMode);
}
PeakMemoryColumn.prototype = {
__proto__: UsedMemoryColumn.prototype,
addInfos: function(numerics, processMemoryDumps, infos) {
if (processMemoryDumps === undefined)
return; // Total row.
var resettableValueCount = 0;
var nonResettableValueCount = 0;
for (var i = 0; i < numerics.length; i++) {
if (numerics[i] === undefined)
continue;
if (processMemoryDumps[i].arePeakResidentBytesResettable)
resettableValueCount++;
else
nonResettableValueCount++;
}
if (resettableValueCount > 0 && nonResettableValueCount > 0) {
infos.push(tr.ui.analysis.createWarningInfo('Both resettable and ' +
'non-resettable peak RSS values were provided by the process'));
} else if (resettableValueCount > 0) {
infos.push({
icon: RIGHTWARDS_ARROW_WITH_HOOK,
message: 'Peak RSS since previous memory dump.'
});
} else {
infos.push({
icon: RIGHTWARDS_ARROW_FROM_BAR,
message: 'Peak RSS since process startup. Finer grained ' +
'peaks require a Linux kernel version ' +
GREATER_THAN_OR_EQUAL_TO + ' 4.0.'
});
}
}
};
/** @constructor */
function ByteStatColumn(name, cellPath, aggregationMode) {
UsedMemoryColumn.call(this, name, cellPath, aggregationMode);
}
ByteStatColumn.prototype = {
__proto__: UsedMemoryColumn.prototype,
color: function(numerics, processMemoryDumps) {
if (processMemoryDumps === undefined)
return UsedMemoryColumn.COLOR; // Total row.
var allOlderValues = processMemoryDumps.every(
function(processMemoryDump) {
if (processMemoryDump === undefined)
return true;
return !processMemoryDump.hasOwnVmRegions;
});
// Show the cell in lighter blue if all values were older (i.e. none of
// the defined process memory dumps had own VM regions).
if (allOlderValues)
return UsedMemoryColumn.OLDER_COLOR;
else
return UsedMemoryColumn.COLOR;
},
addInfos: function(numerics, processMemoryDumps, infos) {
if (processMemoryDumps === undefined)
return; // Total row.
var olderValueCount = 0;
for (var i = 0; i < numerics.length; i++) {
var processMemoryDump = processMemoryDumps[i];
if (processMemoryDump !== undefined &&
!processMemoryDump.hasOwnVmRegions) {
olderValueCount++;
}
}
if (olderValueCount === 0)
return; // There are no older values.
var infoQuantifier = olderValueCount < numerics.length ?
' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER /* some values are older */ :
'' /* all values are older */;
// Emit an info if there was at least one older value (i.e. at least one
// defined process memory dump did not have own VM regions).
infos.push({
message: 'Older value' + infoQuantifier +
' (only heavy (purple) memory dumps contain memory maps).',
icon: UNMARRIED_PARTNERSHIP_SYMBOL
});
}
};
// Rules for constructing and sorting used memory columns.
UsedMemoryColumn.RULES = [
{
condition: 'Total resident',
importance: 10,
columnConstructor: UsedMemoryColumn
},
{
condition: 'Peak total resident',
importance: 9,
columnConstructor: PeakMemoryColumn
},
{
condition: 'PSS',
importance: 8,
columnConstructor: ByteStatColumn
},
{
condition: 'Private dirty',
importance: 7,
columnConstructor: ByteStatColumn
},
{
condition: 'Swapped',
importance: 6,
columnConstructor: ByteStatColumn
},
{
// All other columns.
importance: 0,
columnConstructor: UsedMemoryColumn
}
];
// Map from ProcessMemoryDump totals fields to column names.
UsedMemoryColumn.TOTALS_MAP = {
'residentBytes': 'Total resident',
'peakResidentBytes': 'Peak total resident'
};
// Map from VMRegionByteStats field names to column names.
UsedMemoryColumn.BYTE_STAT_MAP = {
'proportionalResident': 'PSS',
'privateDirtyResident': 'Private dirty',
'swapped': 'Swapped'
};
/** @constructor */
function AllocatorColumn(name, cellPath, aggregationMode) {
tr.ui.analysis.NumericMemoryColumn.call(
this, name, cellPath, aggregationMode);
}
AllocatorColumn.prototype = {
__proto__: tr.ui.analysis.NumericMemoryColumn.prototype,
get title() {
var titleEl = document.createElement('tr-ui-b-color-legend');
titleEl.label = this.name;
return titleEl;
},
getFormattingContext: function(unit) {
return { unitPrefix: tr.b.UnitScale.Binary.MEBI };
},
addInfos: function(numerics, processMemoryDumps, infos) {
if (processMemoryDumps === undefined)
return;
var heapDumpCount = 0;
var missingSizeCount = 0;
for (var i = 0; i < processMemoryDumps.length; i++) {
var processMemoryDump = processMemoryDumps[i];
if (processMemoryDump === undefined)
continue;
var heapDumps = processMemoryDump.heapDumps;
if (heapDumps !== undefined && heapDumps[this.name] !== undefined)
heapDumpCount++;
var allocatorDump =
processMemoryDump.getMemoryAllocatorDumpByFullName(this.name);
if (allocatorDump !== undefined &&
allocatorDump.numerics[DISPLAYED_SIZE_NUMERIC_NAME] === undefined) {
missingSizeCount++;
}
}
// Emit a heap dump info if at least one of the process memory dumps has
// a heap dump associated with this allocator.
if (heapDumpCount > 0) {
var infoQuantifier = heapDumpCount < numerics.length ?
' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : '';
infos.push({
message: 'Heap dump provided' + infoQuantifier + '.',
icon: TRIGRAM_FOR_HEAVEN
});
}
// Emit a warning if this allocator did not provide size in at least one
// of the process memory dumps.
if (missingSizeCount > 0) {
var infoQuantifier = missingSizeCount < numerics.length ?
' ' + SOME_TIMESTAMPS_INFO_QUANTIFIER : '';
infos.push(tr.ui.analysis.createWarningInfo(
'Size was not provided' + infoQuantifier + '.'));
}
},
getChildPaneBuilder: function(processMemoryDumps) {
if (processMemoryDumps === undefined)
return undefined;
var memoryAllocatorDumps = lazyMap(processMemoryDumps, function(pmd) {
if (pmd === undefined)
return undefined;
return pmd.getMemoryAllocatorDumpByFullName(this.name);
}, this);
if (memoryAllocatorDumps === undefined)
return undefined;
var heapDumps = lazyMap(processMemoryDumps, function(pmd) {
if (pmd === undefined || pmd.heapDumps === undefined)
return undefined;
return pmd.heapDumps[this.name];
}, this);
return function() {
var pane = document.createElement(
'tr-ui-a-memory-dump-allocator-details-pane');
pane.memoryAllocatorDumps = memoryAllocatorDumps;
pane.heapDumps = heapDumps;
pane.aggregationMode = this.aggregationMode;
return pane;
}.bind(this);
}
};
/** @constructor */
function TracingColumn(name, cellPath, aggregationMode) {
AllocatorColumn.call(this, name, cellPath, aggregationMode);
}
TracingColumn.COLOR =
ColorScheme.getColorForReservedNameAsString('tracing_memory_column');
TracingColumn.prototype = {
__proto__: AllocatorColumn.prototype,
get title() {
return tr.ui.b.createSpan({
textContent: this.name,
color: TracingColumn.COLOR
});
},
color: function(numerics, processMemoryDumps) {
return TracingColumn.COLOR;
}
};
// Rules for constructing and sorting allocator columns.
AllocatorColumn.RULES = [
{
condition: 'tracing',
importance: 0,
columnConstructor: TracingColumn
},
{
// All other columns.
importance: 1,
columnConstructor: AllocatorColumn
}
];
Polymer({
is: 'tr-ui-a-memory-dump-overview-pane',
behaviors: [tr.ui.analysis.StackedPane],
created: function() {
this.processMemoryDumps_ = undefined;
this.aggregationMode_ = undefined;
},
ready: function() {
this.$.table.selectionMode = tr.ui.b.TableFormat.SelectionMode.CELL;
this.$.table.addEventListener('selection-changed',
function(tableEvent) {
tableEvent.stopPropagation();
this.changeChildPane_();
}.bind(this));
},
/**
* Sets the process memory dumps and schedules rebuilding the pane.
*
* The provided value should be a chronological list of dictionaries
* mapping process IDs to process memory dumps. Example:
*
* [
* {
* // PMDs at timestamp 1.
* 42: tr.model.ProcessMemoryDump {}
* },
* {
* // PMDs at timestamp 2.
* 42: tr.model.ProcessMemoryDump {},
* 89: tr.model.ProcessMemoryDump {}
* }
* ]
*/
set processMemoryDumps(processMemoryDumps) {
this.processMemoryDumps_ = processMemoryDumps;
this.scheduleRebuild_();
},
get processMemoryDumps() {
return this.processMemoryDumps_;
},
set aggregationMode(aggregationMode) {
this.aggregationMode_ = aggregationMode;
this.scheduleRebuild_();
},
get aggregationMode() {
return this.aggregationMode_;
},
get selectedMemoryCell() {
if (this.processMemoryDumps_ === undefined ||
this.processMemoryDumps_.length === 0) {
return undefined;
}
var selectedTableRow = this.$.table.selectedTableRow;
if (!selectedTableRow)
return undefined;
var selectedColumnIndex = this.$.table.selectedColumnIndex;
if (selectedColumnIndex === undefined)
return undefined;
var selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
var selectedMemoryCell = selectedColumn.cell(selectedTableRow);
return selectedMemoryCell;
},
changeChildPane_: function() {
this.storeSelection_();
this.childPaneBuilder = this.determineChildPaneBuilderFromSelection_();
},
determineChildPaneBuilderFromSelection_: function() {
if (this.processMemoryDumps_ === undefined ||
this.processMemoryDumps_.length === 0) {
return undefined;
}
var selectedTableRow = this.$.table.selectedTableRow;
if (!selectedTableRow)
return undefined;
var selectedColumnIndex = this.$.table.selectedColumnIndex;
if (selectedColumnIndex === undefined)
return undefined;
var selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
return selectedColumn.getChildPaneBuilder(selectedTableRow.contexts);
},
onRebuild_: function() {
if (this.processMemoryDumps_ === undefined ||
this.processMemoryDumps_.length === 0) {
// Show the info text (hide the table).
this.$.info_text.style.display = 'block';
this.$.table.style.display = 'none';
this.$.table.clear();
this.$.table.rebuild();
return;
}
// Show the table (hide the info text).
this.$.info_text.style.display = 'none';
this.$.table.style.display = 'block';
var rows = this.createRows_();
var columns = this.createColumns_(rows);
var footerRows = this.createFooterRows_(rows, columns);
this.$.table.tableRows = rows;
this.$.table.footerRows = footerRows;
this.$.table.tableColumns = columns;
this.$.table.rebuild();
this.restoreSelection_();
},
createRows_: function() {
// Timestamp (list index) -> Process ID (dict key) -> PMD.
var timeToPidToProcessMemoryDump = this.processMemoryDumps_;
// Process ID (dict key) -> Timestamp (list index) -> PMD or undefined.
var pidToTimeToProcessMemoryDump = tr.b.invertArrayOfDicts(
timeToPidToProcessMemoryDump);
// Process (list index) -> Component (dict key) -> Cell.
return tr.b.dictionaryValues(tr.b.mapItems(
pidToTimeToProcessMemoryDump, function(pid, timeToDump) {
// Get the process associated with the dumps. We can use any defined
// process memory dump in timeToDump since they all have the same pid.
var process = tr.b.findFirstInArray(timeToDump).process;
// Used memory (total resident, PSS, ...).
var usedMemoryCells = tr.ui.analysis.createCells(timeToDump,
function(dump) {
var sizes = {};
var totals = dump.totals;
if (totals !== undefined) {
// Common totals.
tr.b.iterItems(UsedMemoryColumn.TOTALS_MAP,
function(totalName, cellName) {
var total = totals[totalName];
if (total === undefined)
return;
sizes[cellName] = new ScalarNumeric(
sizeInBytes_smallerIsBetter, total);
});
// Platform-specific totals (e.g. private resident on Mac).
var platformSpecific = totals.platformSpecific;
if (platformSpecific !== undefined) {
tr.b.iterItems(platformSpecific, function(name, size) {
// Change raw OS-specific total name to a user-friendly
// column title (e.g. 'private_bytes' -> 'Private').
if (name.endsWith(PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX)) {
name = name.substring(0, name.length -
PLATFORM_SPECIFIC_TOTAL_NAME_SUFFIX.length);
}
name = name.replace('_', ' ').trim();
name = name.charAt(0).toUpperCase() + name.slice(1);
sizes[name] = new ScalarNumeric(
sizeInBytes_smallerIsBetter, size);
});
}
}
// VM regions byte stats.
var vmRegions = dump.mostRecentVmRegions;
if (vmRegions !== undefined) {
tr.b.iterItems(UsedMemoryColumn.BYTE_STAT_MAP,
function(byteStatName, cellName) {
var byteStat = vmRegions.byteStats[byteStatName];
if (byteStat === undefined)
return;
sizes[cellName] = new ScalarNumeric(
sizeInBytes_smallerIsBetter, byteStat);
});
}
return sizes;
});
// Allocator memory (v8, oilpan, ...).
var allocatorCells = tr.ui.analysis.createCells(timeToDump,
function(dump) {
var memoryAllocatorDumps = dump.memoryAllocatorDumps;
if (memoryAllocatorDumps === undefined)
return undefined;
var sizes = {};
memoryAllocatorDumps.forEach(function(allocatorDump) {
var rootDisplayedSizeNumeric = allocatorDump.numerics[
DISPLAYED_SIZE_NUMERIC_NAME];
if (rootDisplayedSizeNumeric === undefined) {
rootDisplayedSizeNumeric =
new ScalarNumeric(sizeInBytes_smallerIsBetter, 0);
}
sizes[allocatorDump.fullName] = rootDisplayedSizeNumeric;
});
return sizes;
});
return {
title: process.userFriendlyName,
contexts: timeToDump,
usedMemoryCells: usedMemoryCells,
allocatorCells: allocatorCells
};
}));
},
createFooterRows_: function(rows, columns) {
// Add a 'Total' row if there are at least two process memory dumps.
if (rows.length <= 1)
return [];
var totalRow = {title: 'Total'};
tr.ui.analysis.aggregateTableRowCells(totalRow, rows, columns);
return [totalRow];
},
createColumns_: function(rows) {
var titleColumn = new ProcessNameColumn();
titleColumn.width = '200px';
var usedMemorySizeColumns = tr.ui.analysis.MemoryColumn.fromRows(
rows, 'usedMemoryCells', this.aggregationMode_,
UsedMemoryColumn.RULES);
var allocatorSizeColumns = tr.ui.analysis.MemoryColumn.fromRows(
rows, 'allocatorCells', this.aggregationMode_,
AllocatorColumn.RULES);
var sizeColumns = usedMemorySizeColumns.concat(allocatorSizeColumns);
tr.ui.analysis.MemoryColumn.spaceEqually(sizeColumns);
var columns = [titleColumn].concat(sizeColumns);
return columns;
},
storeSelection_: function() {
var selectedRowTitle;
var selectedRow = this.$.table.selectedTableRow;
if (selectedRow !== undefined)
selectedRowTitle = selectedRow.title;
var selectedColumnName;
var selectedColumnIndex = this.$.table.selectedColumnIndex;
if (selectedColumnIndex !== undefined) {
var selectedColumn = this.$.table.tableColumns[selectedColumnIndex];
selectedColumnName = selectedColumn.name;
}
this.$.state.set(
{rowTitle: selectedRowTitle, columnName: selectedColumnName});
},
restoreSelection_: function() {
var settings = this.$.state.get();
if (settings === undefined || settings.rowTitle === undefined ||
settings.columnName === undefined)
return;
var selectedColumnName = settings.columnName;
var selectedColumnIndex = tr.b.findFirstIndexInArray(
this.$.table.tableColumns, function(column) {
return column.name === selectedColumnName;
});
if (selectedColumnIndex < 0)
return;
var selectedRowTitle = settings.rowTitle;
var selectedRow = tr.b.findFirstInArray(this.$.table.tableRows,
function(row) {
return row.title === selectedRowTitle;
});
if (selectedRow === undefined)
return;
this.$.table.selectedTableRow = selectedRow;
this.$.table.selectedColumnIndex = selectedColumnIndex;
}
});
return {
// All exports are for testing only.
ProcessNameColumn: ProcessNameColumn,
UsedMemoryColumn: UsedMemoryColumn,
PeakMemoryColumn: PeakMemoryColumn,
ByteStatColumn: ByteStatColumn,
AllocatorColumn: AllocatorColumn,
TracingColumn: TracingColumn
};
});
</script>