blob: 8134aea6b1750c16c41a6a3e4f2665063bd0e253 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2016 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/iteration_helpers.html">
<link rel="import" href="/tracing/base/multi_dimensional_view.html">
<link rel="import" href="/tracing/base/range.html">
<link rel="import" href="/tracing/base/unit.html">
<link rel="import" href="/tracing/metrics/metric_registry.html">
<link rel="import" href="/tracing/model/container_memory_dump.html">
<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html">
<link rel="import" href="/tracing/model/memory_allocator_dump.html">
<link rel="import" href="/tracing/value/histogram.html">
<script>
'use strict';
tr.exportTo('tr.metrics.sh', function() {
var BACKGROUND = tr.model.ContainerMemoryDump.LevelOfDetail.BACKGROUND;
var LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
var DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;
var sizeInBytes_smallerIsBetter =
tr.b.Unit.byName.sizeInBytes_smallerIsBetter;
var unitlessNumber_smallerIsBetter =
tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
var DISPLAYED_SIZE_NUMERIC_NAME =
tr.model.MemoryAllocatorDump.DISPLAYED_SIZE_NUMERIC_NAME;
var LEVEL_OF_DETAIL_NAMES = new Map();
LEVEL_OF_DETAIL_NAMES.set(BACKGROUND, 'background');
LEVEL_OF_DETAIL_NAMES.set(LIGHT, 'light');
LEVEL_OF_DETAIL_NAMES.set(DETAILED, 'detailed');
var BOUNDARIES_FOR_UNIT_MAP = new WeakMap();
// For unitless numerics (process counts), we use 20 linearly scaled bins
// from 0 to 20.
BOUNDARIES_FOR_UNIT_MAP.set(unitlessNumber_smallerIsBetter,
tr.v.HistogramBinBoundaries.createLinear(0, 20, 20));
// For size numerics (subsystem and vm stats), we use 1 bin from 0 B to
// 1 KiB and 4*24 exponentially scaled bins from 1 KiB to 16 GiB (=2^24 KiB).
BOUNDARIES_FOR_UNIT_MAP.set(sizeInBytes_smallerIsBetter,
new tr.v.HistogramBinBoundaries(0)
.addBinBoundary(1024 /* 1 KiB */)
.addExponentialBins(16 * 1024 * 1024 * 1024 /* 16 GiB */, 4 * 24));
function memoryMetric(values, model, opt_options) {
var rangeOfInterest = opt_options ? opt_options.rangeOfInterest : undefined;
var browserNameToGlobalDumps =
splitGlobalDumpsByBrowserName(model, rangeOfInterest);
addGeneralMemoryDumpValues(browserNameToGlobalDumps, values);
addDetailedMemoryDumpValues(browserNameToGlobalDumps, values);
addMemoryDumpCountValues(browserNameToGlobalDumps, values);
}
/**
* Splits the global memory dumps in |model| by browser name.
*
* @param {!tr.Model} model The trace model from which the global dumps
* should be extracted.
* @param {!tr.b.Range=} opt_rangeOfInterest If proided, global memory dumps
* that do not inclusively intersect the range will be skipped.
* @return {!Map<string, !Array<!tr.model.GlobalMemoryDump>} A map from
* browser names to the associated global memory dumps.
*/
function splitGlobalDumpsByBrowserName(model, opt_rangeOfInterest) {
var chromeModelHelper =
model.getOrCreateHelper(tr.model.helpers.ChromeModelHelper);
var browserNameToGlobalDumps = new Map();
var globalDumpToBrowserHelper = new WeakMap();
// 1. For each browser process in the model, add its global memory dumps to
// |browserNameToGlobalDumps|. |chromeModelHelper| can be undefined if
// it fails to find any browser, renderer or GPU process (see
// tr.model.helpers.ChromeModelHelper.supportsModel).
if (chromeModelHelper) {
chromeModelHelper.browserHelpers.forEach(function(helper) {
// Retrieve the associated global memory dumps and check that they
// haven't been classified as belonging to another browser process.
var globalDumps = skipDumpsThatDoNotIntersectRange(
helper.process.memoryDumps.map(d => d.globalMemoryDump),
opt_rangeOfInterest);
globalDumps.forEach(function(globalDump) {
var existingHelper = globalDumpToBrowserHelper.get(globalDump);
if (existingHelper !== undefined) {
throw new Error('Memory dump ID clash across multiple browsers ' +
'with PIDs: ' + existingHelper.pid + ' and ' + helper.pid);
}
globalDumpToBrowserHelper.set(globalDump, helper);
});
makeKeyUniqueAndSet(browserNameToGlobalDumps,
canonicalizeName(helper.browserName), globalDumps);
});
}
// 2. If any global memory dump does not have any associated browser
// process for some reason, associate it with an 'unknown_browser' browser
// so that we don't lose the data.
var unclassifiedGlobalDumps = skipDumpsThatDoNotIntersectRange(
model.globalMemoryDumps.filter(g => !globalDumpToBrowserHelper.has(g)),
opt_rangeOfInterest);
if (unclassifiedGlobalDumps.length > 0) {
makeKeyUniqueAndSet(
browserNameToGlobalDumps, 'unknown_browser', unclassifiedGlobalDumps);
}
return browserNameToGlobalDumps;
}
function skipDumpsThatDoNotIntersectRange(dumps, opt_range) {
if (!opt_range)
return dumps;
return dumps.filter(d => opt_range.intersectsExplicitRangeInclusive(
d.start, d.end));
}
function canonicalizeName(name) {
return name.toLowerCase().replace(' ', '_');
}
var USER_FRIENDLY_BROWSER_NAMES = {
'chrome': 'Chrome',
'webview': 'WebView',
'unknown_browser': 'an unknown browser'
};
/**
* Convert a canonical browser name used in value names to a user-friendly
* name used in value descriptions.
*
* Examples:
*
* CANONICAL BROWSER NAME -> USER-FRIENDLY NAME
* chrome -> Chrome
* unknown_browser -> an unknown browser
* webview2 -> WebView(2)
* unexpected -> 'unexpected' browser
*/
function convertBrowserNameToUserFriendlyName(browserName) {
for (var baseName in USER_FRIENDLY_BROWSER_NAMES) {
if (!browserName.startsWith(baseName))
continue;
var userFriendlyBaseName = USER_FRIENDLY_BROWSER_NAMES[baseName];
var suffix = browserName.substring(baseName.length);
if (suffix.length === 0)
return userFriendlyBaseName;
else if (/^\d+$/.test(suffix))
return userFriendlyBaseName + '(' + suffix + ')';
}
return '\'' + browserName + '\' browser';
}
function canonicalizeProcessName(rawProcessName) {
if (!rawProcessName)
return 'unknown_processes';
var baseCanonicalName = canonicalizeName(rawProcessName);
switch (baseCanonicalName) {
case 'renderer':
return 'renderer_processes'; // Intentionally plural.
case 'browser':
return 'browser_process';
default:
return baseCanonicalName;
}
}
/**
* Convert a canonical process name used in value names to a user-friendly
* name used in value descriptions.
*/
function convertProcessNameToUserFriendlyName(processName,
opt_requirePlural) {
switch (processName) {
case 'browser_process':
return opt_requirePlural ? 'browser processes' : 'the browser process';
case 'renderer_processes':
return 'renderer processes';
case 'gpu_process':
return opt_requirePlural ? 'GPU processes' : 'the GPU process';
case 'ppapi_process':
return opt_requirePlural ? 'PPAPI processes' : 'the PPAPI process';
case 'all_processes':
return 'all processes';
case 'unknown_processes':
return 'unknown processes';
default:
return '\'' + processName + '\' processes';
}
}
/**
* Function for adding entries with duplicate keys to a map without
* overriding existing entries.
*
* This is achieved by appending numeric indices (2, 3, 4, ...) to duplicate
* keys. Example:
*
* var map = new Map();
* // map = Map {}.
*
* makeKeyUniqueAndSet(map, 'key', 'a');
* // map = Map {"key" => "a"}.
*
* makeKeyUniqueAndSet(map, 'key', 'b');
* // map = Map {"key" => "a", "key2" => "b"}.
* ^^^^
* makeKeyUniqueAndSet(map, 'key', 'c');
* // map = Map {"key" => "a", "key2" => "b", "key3" => "c"}.
* ^^^^ ^^^^
*/
function makeKeyUniqueAndSet(map, key, value) {
var uniqueKey = key;
var nextIndex = 2;
while (map.has(uniqueKey)) {
uniqueKey = key + nextIndex;
nextIndex++;
}
map.set(uniqueKey, value);
}
/**
* Add general memory dump values calculated from all global memory dumps to
* |values|. In particular, this function adds the following values:
*
* * PROCESS COUNTS
* memory:{chrome, webview}:
* {browser_process, renderer_processes, ..., all_processes}:
* process_count
* type: tr.v.Histogram (over all matching global memory dumps)
* unit: unitlessNumber_smallerIsBetter
*
* * MEMORY USAGE REPORTED BY CHROME
* memory:{chrome, webview}:
* {browser_process, renderer_processes, ..., all_processes}:
* reported_by_chrome[:{v8, malloc, ...}]:
* {effective_size, allocated_objects_size, locked_size}
* type: tr.v.Histogram (over all matching global memory dumps)
* unit: sizeInBytes_smallerIsBetter
*/
function addGeneralMemoryDumpValues(browserNameToGlobalDumps, values) {
addMemoryDumpValues(browserNameToGlobalDumps,
gmd => true /* process all global memory dumps */,
function(processDump, addProcessScalar) {
// Increment memory:<browser-name>:<process-name>:process_count value.
addProcessScalar({
source: 'process_count',
value: 1,
unit: unitlessNumber_smallerIsBetter,
descriptionPrefixBuilder: buildProcessCountDescriptionPrefix
});
if (processDump.totals !== undefined) {
tr.b.iterItems(SYSTEM_TOTAL_VALUE_PROPERTIES,
function(propertyName, propertySpec) {
addProcessScalar({
source: 'reported_by_os',
property: propertyName,
component: ['system_memory'],
value: propertySpec.getPropertyFunction(processDump),
unit: sizeInBytes_smallerIsBetter,
descriptionPrefixBuilder:
propertySpec.descriptionPrefixBuilder
});
});
}
// Add memory:<browser-name>:<process-name>:reported_by_chrome:...
// values.
if (processDump.memoryAllocatorDumps === undefined)
return;
processDump.memoryAllocatorDumps.forEach(function(rootAllocatorDump) {
tr.b.iterItems(CHROME_VALUE_PROPERTIES,
function(propertyName, descriptionPrefixBuilder) {
addProcessScalar({
source: 'reported_by_chrome',
component: [rootAllocatorDump.name],
property: propertyName,
value: rootAllocatorDump.numerics[propertyName],
descriptionPrefixBuilder: descriptionPrefixBuilder
});
});
// Some dump providers add allocated objects size as
// "allocated_objects" child dump.
if (rootAllocatorDump.numerics['allocated_objects_size'] ===
undefined) {
var allocatedObjectsDump =
rootAllocatorDump.getDescendantDumpByFullName(
'allocated_objects');
if (allocatedObjectsDump !== undefined) {
addProcessScalar({
source: 'reported_by_chrome',
component: [rootAllocatorDump.name],
property: 'allocated_objects_size',
value: allocatedObjectsDump.numerics['size'],
descriptionPrefixBuilder:
CHROME_VALUE_PROPERTIES['allocated_objects_size']
});
}
}
});
// Add memory:<browser-name>:<process-name>:reported_by_chrome:v8:
// {heap, allocated_by_malloc}:...
addV8MemoryDumpValues(processDump, addProcessScalar);
},
function(componentTree) {
// Subtract memory:<browser-name>:<process-name>:reported_by_chrome:
// tracing:<size-property> from memory:<browser-name>:<process-name>:
// reported_by_chrome:<size-property> if applicable.
var tracingNode = componentTree.children[1].get('tracing');
if (tracingNode === undefined)
return;
for (var i = 0; i < componentTree.values.length; i++)
componentTree.values[i].total -= tracingNode.values[i].total;
}, values);
}
/**
* Add memory dump values calculated from V8 components excluding
* 'heap_spaces/other_spaces'.
*
* @param {!tr.model.ProcessMemoryDump} processDump The process memory dump.
* @param {!function} addProcessScalar The callback for adding a scalar value.
*/
function addV8MemoryDumpValues(processDump, addProcessScalar) {
var v8Dump = processDump.getMemoryAllocatorDumpByFullName('v8');
if (v8Dump === undefined)
return;
v8Dump.children.forEach(function(isolateDump) {
// v8:allocated_by_malloc:...
var mallocDump = isolateDump.getDescendantDumpByFullName('malloc');
if (mallocDump !== undefined) {
addV8ComponentValues(mallocDump, ['v8', 'allocated_by_malloc'],
addProcessScalar);
}
// v8:heap:...
var heapDump = isolateDump.getDescendantDumpByFullName('heap_spaces');
if (heapDump !== undefined) {
addV8ComponentValues(heapDump, ['v8', 'heap'], addProcessScalar);
heapDump.children.forEach(function(spaceDump) {
if (spaceDump.name === 'other_spaces')
return;
addV8ComponentValues(spaceDump, ['v8', 'heap', spaceDump.name],
addProcessScalar);
});
}
});
// V8 generates bytecode when interpreting and code objects when
// compiling the javascript. Total code size includes the size
// of code and bytecode objects.
addProcessScalar({
source: 'reported_by_chrome',
component: ['v8'],
property: 'code_and_metadata_size',
value: v8Dump.numerics['code_and_metadata_size'],
descriptionPrefixBuilder:
buildCodeAndMetadataSizeValueDescriptionPrefix
});
addProcessScalar({
source: 'reported_by_chrome',
component: ['v8'],
property: 'code_and_metadata_size',
value: v8Dump.numerics['bytecode_and_metadata_size'],
descriptionPrefixBuilder:
buildCodeAndMetadataSizeValueDescriptionPrefix
});
}
/**
* Add memory dump values calculated from the specified V8 component.
*
* @param {!tr.model.MemoryAllocatorDump} v8Dump The V8 memory dump.
* @param {!Array<string>} componentPath The component path for reporting.
* @param {!function} addProcessScalar The callback for adding a scalar value.
*/
function addV8ComponentValues(componentDump, componentPath,
addProcessScalar) {
tr.b.iterItems(CHROME_VALUE_PROPERTIES,
function(propertyName, descriptionPrefixBuilder) {
addProcessScalar({
source: 'reported_by_chrome',
component: componentPath,
property: propertyName,
value: componentDump.numerics[propertyName],
descriptionPrefixBuilder: descriptionPrefixBuilder
});
});
}
/**
* Build a description prefix for a memory:<browser-name>:<process-name>:
* process_count value.
*
* @param {!Array<string>} componentPath The underlying component path (must
* be empty).
* @param {string} processName The canonical name of the process.
* @return {string} Prefix for the value's description (always
* 'total number of renderer processes').
*/
function buildProcessCountDescriptionPrefix(componentPath, processName) {
if (componentPath.length > 0) {
throw new Error('Unexpected process count non-empty component path: ' +
componentPath.join(':'));
}
return 'total number of ' + convertProcessNameToUserFriendlyName(
processName, true /* opt_requirePlural */);
}
/**
* Build a description prefix for a memory:<browser-name>:<process-name>:
* reported_by_chrome:... value.
*
* @param {{
* userFriendlyPropertyName: string,
* userFriendlyPropertyNamePrefix: (string|undefined),
* totalUserFriendlyPropertyName: (string|undefined),
* componentPreposition: (string|undefined) }}
* formatSpec Specification of how the property should be formatted.
* @param {!Array<string>} componentPath The underlying component path (e.g.
* ['malloc']).
* @param {string} processName The canonical name of the process.
* @return {string} Prefix for the value's description (e.g.
* 'effective size of malloc in the browser process').
*/
function buildChromeValueDescriptionPrefix(
formatSpec, componentPath, processName) {
var nameParts = [];
if (componentPath.length === 0) {
nameParts.push('total');
if (formatSpec.totalUserFriendlyPropertyName) {
nameParts.push(formatSpec.totalUserFriendlyPropertyName);
} else {
if (formatSpec.userFriendlyPropertyNamePrefix)
nameParts.push(formatSpec.userFriendlyPropertyNamePrefix);
nameParts.push(formatSpec.userFriendlyPropertyName);
}
nameParts.push('reported by Chrome for');
} else {
if (formatSpec.componentPreposition === undefined) {
// Use component name as an adjective
// (e.g. 'size of V8 code and metadata').
if (formatSpec.userFriendlyPropertyNamePrefix)
nameParts.push(formatSpec.userFriendlyPropertyNamePrefix);
nameParts.push(componentPath.join(':'));
nameParts.push(formatSpec.userFriendlyPropertyName);
} else {
// Use component name as a noun with a preposition
// (e.g. 'size of all objects allocated BY MALLOC').
if (formatSpec.userFriendlyPropertyNamePrefix)
nameParts.push(formatSpec.userFriendlyPropertyNamePrefix);
nameParts.push(formatSpec.userFriendlyPropertyName);
nameParts.push(formatSpec.componentPreposition);
if (componentPath[componentPath.length - 1] === 'allocated_by_malloc') {
nameParts.push('objects allocated by malloc for');
nameParts.push(
componentPath.slice(0, componentPath.length - 1).join(':'));
} else {
nameParts.push(componentPath.join(':'));
}
}
nameParts.push('in');
}
nameParts.push(convertProcessNameToUserFriendlyName(processName));
return nameParts.join(' ');
}
// Specifications of properties reported by Chrome.
var CHROME_VALUE_PROPERTIES = {
'effective_size': buildChromeValueDescriptionPrefix.bind(undefined, {
userFriendlyPropertyName: 'effective size',
componentPreposition: 'of'
}),
'allocated_objects_size': buildChromeValueDescriptionPrefix.bind(
undefined, {
userFriendlyPropertyName: 'size of all objects allocated',
totalUserFriendlyPropertyName: 'size of all allocated objects',
componentPreposition: 'by'
}),
'locked_size': buildChromeValueDescriptionPrefix.bind(undefined, {
userFriendlyPropertyName: 'locked (pinned) size',
componentPreposition: 'of'
}),
'peak_size': buildChromeValueDescriptionPrefix.bind(undefined, {
userFriendlyPropertyName: 'peak size',
componentPreposition: 'of'
}),
};
var SYSTEM_TOTAL_VALUE_PROPERTIES = {
'resident_size': {
getPropertyFunction: function(processDump) {
return processDump.totals.residentBytes;
},
descriptionPrefixBuilder: buildOsValueDescriptionPrefix.bind(
undefined, 'resident set size (RSS)')
},
'peak_resident_size': {
getPropertyFunction: function(processDump) {
return processDump.totals.peakResidentBytes;
},
descriptionPrefixBuilder: buildOsValueDescriptionPrefix.bind(
undefined, 'peak resident set size')
}
};
/**
* Add heavy memory dump values calculated from heavy global memory dumps to
* |values|. In particular, this function adds the following values:
*
* * MEMORY USAGE REPORTED BY THE OS
* memory:{chrome, webview}:
* {browser_process, renderer_processes, ..., all_processes}:
* reported_by_os:system_memory:[{ashmem, native_heap, java_heap}:]
* {proportional_resident_size, private_dirty_size}
* memory:{chrome, webview}:
* {browser_process, renderer_processes, ..., all_processes}:
* reported_by_os:gpu_memory:[{gl, graphics, ...}:]
* proportional_resident_size
* type: tr.v.Histogram (over matching heavy global memory dumps)
* unit: sizeInBytes_smallerIsBetter
*
* * MEMORY USAGE REPORTED BY CHROME
* memory:{chrome, webview}:
* {browser_process, renderer_processes, ..., all_processes}:
* reported_by_chrome:v8:code_and_metadata_size
* type: tr.v.Histogram (over matching heavy global memory dumps)
* unit: sizeInBytes_smallerIsBetter
*/
function addDetailedMemoryDumpValues(browserNameToGlobalDumps, values) {
addMemoryDumpValues(browserNameToGlobalDumps,
g => g.levelOfDetail === DETAILED,
function(processDump, addProcessScalar) {
// Add memory:<browser-name>:<process-name>:reported_by_os:
// system_memory:... values.
tr.b.iterItems(
SYSTEM_VALUE_COMPONENTS,
function(componentName, componentSpec) {
tr.b.iterItems(
SYSTEM_VALUE_PROPERTIES,
function(propertyName, propertySpec) {
var node = getDescendantVmRegionClassificationNode(
processDump.vmRegions,
componentSpec.classificationPath);
var componentPath = ['system_memory'];
if (componentName)
componentPath.push(componentName);
addProcessScalar({
source: 'reported_by_os',
component: componentPath,
property: propertyName,
value: node === undefined ?
0 : (node.byteStats[propertySpec.byteStat] || 0),
unit: sizeInBytes_smallerIsBetter,
descriptionPrefixBuilder:
propertySpec.descriptionPrefixBuilder
});
});
});
// Add memory:<browser-name>:<process-name>:reported_by_os:
// gpu_memory:... values.
var memtrackDump = processDump.getMemoryAllocatorDumpByFullName(
'gpu/android_memtrack');
if (memtrackDump !== undefined) {
var descriptionPrefixBuilder = SYSTEM_VALUE_PROPERTIES[
'proportional_resident_size'].descriptionPrefixBuilder;
memtrackDump.children.forEach(function(memtrackChildDump) {
var childName = memtrackChildDump.name;
addProcessScalar({
source: 'reported_by_os',
component: ['gpu_memory', childName],
property: 'proportional_resident_size',
value: memtrackChildDump.numerics['memtrack_pss'],
descriptionPrefixBuilder: descriptionPrefixBuilder
});
});
}
}, function(componentTree) {}, values);
}
// Specifications of components reported by the system.
var SYSTEM_VALUE_COMPONENTS = {
'': {
classificationPath: [],
},
'java_heap': {
classificationPath: ['Android', 'Java runtime', 'Spaces'],
userFriendlyName: 'the Java heap'
},
'ashmem': {
classificationPath: ['Android', 'Ashmem'],
userFriendlyName: 'ashmem'
},
'native_heap': {
classificationPath: ['Native heap'],
userFriendlyName: 'the native heap'
}
};
// Specifications of properties reported by the system.
var SYSTEM_VALUE_PROPERTIES = {
'proportional_resident_size': {
byteStat: 'proportionalResident',
descriptionPrefixBuilder: buildOsValueDescriptionPrefix.bind(
undefined, 'proportional resident size (PSS)')
},
'private_dirty_size': {
byteStat: 'privateDirtyResident',
descriptionPrefixBuilder: buildOsValueDescriptionPrefix.bind(
undefined, 'private dirty size')
}
};
/**
* Build a description prefix for a memory:<browser-name>:<process-name>:
* reported_by_os:... value.
*
* @param {string} userFriendlyPropertyName User-friendly name of the
* underlying property (e.g. 'private dirty size').
* @param {!Array<string>} componentPath The underlying component path (e.g.
* ['system', 'java_heap']).
* @param {string} processName The canonical name of the process.
* @return {string} Prefix for the value's description (e.g.
* 'total private dirty size of the Java heal in the GPU process').
*/
function buildOsValueDescriptionPrefix(
userFriendlyPropertyName, componentPath, processName) {
if (componentPath.length > 2) {
throw new Error('OS value component path for \'' +
userFriendlyPropertyName + '\' too long: ' + componentPath.join(':'));
}
var nameParts = [];
if (componentPath.length < 2)
nameParts.push('total');
nameParts.push(userFriendlyPropertyName);
if (componentPath.length > 0) {
switch (componentPath[0]) {
case 'system_memory':
if (componentPath.length > 1) {
var userFriendlyComponentName =
SYSTEM_VALUE_COMPONENTS[componentPath[1]].userFriendlyName;
if (userFriendlyComponentName === undefined) {
throw new Error('System value sub-component for \'' +
userFriendlyPropertyName + '\' unknown: ' +
componentPath.join(':'));
}
nameParts.push('of', userFriendlyComponentName, 'in');
} else {
nameParts.push('of system memory (RAM) used by');
}
break;
case 'gpu_memory':
if (componentPath.length > 1) {
nameParts.push('of the', componentPath[1]);
nameParts.push('Android memtrack component in');
} else {
nameParts.push('of GPU memory (Android memtrack) used by');
}
break;
default:
throw new Error('OS value component for \'' +
userFriendlyPropertyName + '\' unknown: ' +
componentPath.join(':'));
}
} else {
nameParts.push('reported by the OS for');
}
nameParts.push(convertProcessNameToUserFriendlyName(processName));
return nameParts.join(' ');
}
/**
* Build a description prefix for a memory:<browser-name>:<process-name>:
* reported_by_chrome:...:code_and_metadata_size value.
*
* @param {!Array<string>} componentPath The underlying component path (e.g.
* ['v8']).
* @param {string} processName The canonical name of the process.
* @return {string} Prefix for the value's description (e.g.
* 'size of v8 code and metadata in').
*/
function buildCodeAndMetadataSizeValueDescriptionPrefix(
componentPath, processName) {
return buildChromeValueDescriptionPrefix({
userFriendlyPropertyNamePrefix: 'size of',
userFriendlyPropertyName: 'code and metadata'
}, componentPath, processName);
}
/**
* Get the descendant of a VM region classification |node| specified by the
* given |path| of child node titles. If |node| is undefined or such a
* descendant does not exist, this function returns undefined.
*/
function getDescendantVmRegionClassificationNode(node, path) {
for (var i = 0; i < path.length; i++) {
if (node === undefined)
break;
node = tr.b.findFirstInArray(node.children, c => c.title === path[i]);
}
return node;
}
/**
* Add global memory dump counts to |values|. In particular, this function
* adds the following values:
*
* * DUMP COUNTS
* memory:{chrome, webview}:all_processes:dump_count[:{light, detailed}]
* type: tr.v.Histogram
* unit: unitlessNumber_smallerIsBetter
*
* Note that unlike all other values generated by the memory metric, the
* global memory dump counts are NOT instances of tr.v.Histogram
* because it doesn't make sense to aggregate them (they are already counts
* over all global dumps associated with the relevant browser).
*/
function addMemoryDumpCountValues(browserNameToGlobalDumps, values) {
browserNameToGlobalDumps.forEach(function(globalDumps, browserName) {
var totalDumpCount = 0;
var levelOfDetailNameToDumpCount = {};
LEVEL_OF_DETAIL_NAMES.forEach(function(levelOfDetailName) {
levelOfDetailNameToDumpCount[levelOfDetailName] = 0;
});
globalDumps.forEach(function(globalDump) {
totalDumpCount++;
// Increment the level-of-detail-specific dump count (if possible).
var levelOfDetailName =
LEVEL_OF_DETAIL_NAMES.get(globalDump.levelOfDetail);
if (!(levelOfDetailName in levelOfDetailNameToDumpCount))
return; // Unknown level of detail.
levelOfDetailNameToDumpCount[levelOfDetailName]++;
});
// Add memory:<browser-name>:all_processes:dump_count[:<level>] values.
reportMemoryDumpCountAsValue(browserName, undefined /* total */,
totalDumpCount, values);
tr.b.iterItems(levelOfDetailNameToDumpCount,
function(levelOfDetailName, levelOfDetailDumpCount) {
reportMemoryDumpCountAsValue(browserName, levelOfDetailName,
levelOfDetailDumpCount, values);
});
});
}
/**
* Add a tr.v.Histogram value to |values| reporting that the number of
* |levelOfDetailName| memory dumps added by |browserName| was
* |levelOfDetailCount|.
*/
function reportMemoryDumpCountAsValue(
browserName, levelOfDetailName, levelOfDetailDumpCount, values) {
// Construct the name of the memory value.
var nameParts = ['memory', browserName, 'all_processes', 'dump_count'];
if (levelOfDetailName !== undefined)
nameParts.push(levelOfDetailName);
var name = nameParts.join(':');
// Build the underlying histogram for the memory value.
var histogram = new tr.v.Histogram(name, unitlessNumber_smallerIsBetter,
BOUNDARIES_FOR_UNIT_MAP.get(unitlessNumber_smallerIsBetter));
histogram.addSample(levelOfDetailDumpCount);
// Build the options for the memory value.
histogram.description = [
'total number of',
levelOfDetailName || 'all',
'memory dumps added by',
convertBrowserNameToUserFriendlyName(browserName),
'to the trace'
].join(' ');
// Report the memory value.
values.addHistogram(histogram);
}
/**
* Add generic values extracted from process memory dumps and aggregated by
* process name and component path into |values|.
*
* For each browser and set of global dumps in |browserNameToGlobalDumps|,
* |customProcessDumpValueExtractor| is applied to every process memory dump
* associated with the global memory dump. The second argument provided to the
* callback is a function for adding extracted values:
*
* function sampleProcessDumpCallback(processDump, addProcessValue) {
* ...
* addProcessScalar({
* source: 'reported_by_chrome',
* component: ['system', 'native_heap'],
* property: 'proportional_resident_size',
* value: pssExtractedFromProcessDump2,
* descriptionPrefixBuilder: function(componentPath) {
* return 'PSS of ' + componentPath.join('/') + ' in';
* }
* });
* ...
* }
*
* For each global memory dump, the extracted values are summed by process
* name (browser_process, renderer_processes, ..., all_processes) and
* component path (e.g. gpu is a sum of gpu:gl, gpu:graphics, ...). The sums
* are then aggregated over all global memory dumps associated with the given
* browser. For example, assuming that |customProcessDumpValueExtractor|
* extracts 'proportional_resident_size' values for component paths
* ['X', 'A'], ['X', 'B'] and ['Y'] under the same 'source' from each process
* memory dump, the following values will be reported (for Chrome):
*
* memory:chrome:browser_process:source:X:A:proportional_resident_size :
* Histogram aggregated over [
* sum of X:A in all 'browser' process dumps in global dump 1,
* ...
* sum of X:A in all 'browser' process dumps in global dump N
* ]
*
* memory:chrome:browser_process:source:X:B:proportional_resident_size :
* Histogram aggregated over [
* sum of X:B in all 'browser' process dumps in global dump 1,
* ...
* sum of X:B in all 'browser' process dumps in global dump N
* ]
*
* memory:chrome:browser_process:source:X:proportional_resident_size :
* Histogram aggregated over [
* sum of X:A+X:B in all 'browser' process dumps in global dump 1,
* ...
* sum of X:A+X:B in all 'browser' process dumps in global dump N
* ]
*
* memory:chrome:browser_process:source:Y:proportional_resident_size :
* Histogram aggregated over [
* sum of Y in all 'browser' process dumps in global dump 1,
* ...
* sum of Y in all 'browser' process dumps in global dump N
* ]
*
* memory:chrome:browser_process:source:proportional_resident_size :
* Histogram aggregated over [
* sum of X:A+X:B+Y in all 'browser' process dumps in global dump 1,
* ...
* sum of X:A+X:B+Y in all 'browser' process dumps in global dump N
* ]
*
* ...
*
* memory:chrome:all_processes:source:X:A:proportional_resident_size :
* Histogram aggregated over [
* sum of X:A in all process dumps in global dump 1,
* ...
* sum of X:A in all process dumps in global dump N,
* ]
*
* memory:chrome:all_processes:source:X:B:proportional_resident_size :
* Histogram aggregated over [
* sum of X:B in all process dumps in global dump 1,
* ...
* sum of X:B in all process dumps in global dump N,
* ]
*
* memory:chrome:all_processes:source:X:proportional_resident_size :
* Histogram aggregated over [
* sum of X:A+X:B in all process dumps in global dump 1,
* ...
* sum of X:A+X:B in all process dumps in global dump N,
* ]
*
* memory:chrome:all_processes:source:Y:proportional_resident_size :
* Histogram aggregated over [
* sum of Y in all process dumps in global dump 1,
* ...
* sum of Y in all process dumps in global dump N
* ]
*
* memory:chrome:all_processes:source:proportional_resident_size :
* Histogram aggregated over [
* sum of X:A+X:B+Y in all process dumps in global dump 1,
* ...
* sum of X:A+X:B+Y in all process dumps in global dump N
* ]
*
* where global dumps 1 to N are the global dumps associated with the given
* browser.
*
* @param {!Map<string, !Array<!tr.model.GlobalMemoryDump>}
* browserNameToGlobalDumps Map from browser names to arrays of global
* memory dumps. The generic values will be extracted from the associated
* process memory dumps.
* @param {!function(!tr.model.GlobalMemoryDump): boolean}
* customGlobalDumpFilter Predicate for filtering global memory dumps.
* @param {!function(
* !tr.model.ProcessMemoryDump,
* !function(!{
* source: string,
* componentPath: (!Array<string>|undefined),
* propertyName: (string|undefined),
* value: (!tr.v.Histogram|number|undefined),
* unit: (!tr.b.Unit|undefined),
* descriptionPrefixBuilder: (!function(!Array<string>): string)
* }))}
* customProcessDumpValueExtractor Callback for extracting values from a
* process memory dump.
* @param {!function(!tr.b.MultiDimensionalViewNode)}
* customComponentTreeModifier Callback applied to every component tree
* wrt each process name.
* @param {!tr.v.ValueSet} values List of values to which the
* resulting aggregated values are added.
*/
function addMemoryDumpValues(browserNameToGlobalDumps, customGlobalDumpFilter,
customProcessDumpValueExtractor, customComponentTreeModifier,
values) {
browserNameToGlobalDumps.forEach(function(globalDumps, browserName) {
var filteredGlobalDumps = globalDumps.filter(customGlobalDumpFilter);
var sourceToPropertyToData = extractDataFromGlobalDumps(
filteredGlobalDumps, customProcessDumpValueExtractor);
reportDataAsValues(sourceToPropertyToData, browserName,
customComponentTreeModifier, values);
});
}
/**
* For each global memory dump in |globalDumps|, calculate per-process-name
* sums of values extracted by |customProcessDumpValueExtractor| from the
* associated process memory dumps.
*
* This function returns the following nested map structure:
*
* Source name (Map key, e.g. 'reported_by_os')
* -> Property name (Map key, e.g. 'proportional_resident_size')
* -> {unit, descriptionPrefixBuilder, processAndComponentTreeBuilder}
*
* where |processAndComponentTreeBuilder| is a
* tr.b.MultiDimensionalViewBuilder:
*
* Browser name (0th dimension key, e.g. 'webview') x
* -> Component path (1st dimension keys, e.g. ['system', 'native_heap'])
* -> Sum of value over the processes (number).
*
* See addMemoryDumpValues for more details.
*/
function extractDataFromGlobalDumps(
globalDumps, customProcessDumpValueExtractor) {
var sourceToPropertyToData = new Map();
var dumpCount = globalDumps.length;
globalDumps.forEach(function(globalDump, dumpIndex) {
tr.b.iterItems(globalDump.processMemoryDumps, function(_, processDump) {
extractDataFromProcessDump(
processDump, sourceToPropertyToData, dumpIndex, dumpCount,
customProcessDumpValueExtractor);
});
});
return sourceToPropertyToData;
}
function extractDataFromProcessDump(processDump, sourceToPropertyToData,
dumpIndex, dumpCount, customProcessDumpValueExtractor) {
// Process name is typically 'browser', 'renderer', etc.
var rawProcessName = processDump.process.name;
var processNamePath = [canonicalizeProcessName(rawProcessName)];
customProcessDumpValueExtractor(
processDump,
function addProcessScalar(spec) {
if (spec.value === undefined)
return;
var component = spec.component || [];
function createDetailsForErrorMessage() {
var propertyUserFriendlyName =
spec.property === undefined ? '(undefined)' : spec.property;
var componentUserFriendlyName =
component.length === 0 ? '(empty)' : component.join(':');
return ['source=', spec.source, ', property=',
propertyUserFriendlyName, ', component=',
componentUserFriendlyName, ' in ',
processDump.process.userFriendlyName].join('');
}
var value, unit;
if (spec.value instanceof tr.v.ScalarNumeric) {
value = spec.value.value;
unit = spec.value.unit;
if (spec.unit !== undefined) {
throw new Error('Histogram value for ' +
createDetailsForErrorMessage() + ' already specifies a unit');
}
} else {
value = spec.value;
unit = spec.unit;
}
var propertyToData = sourceToPropertyToData.get(spec.source);
if (propertyToData === undefined) {
propertyToData = new Map();
sourceToPropertyToData.set(spec.source, propertyToData);
}
var data = propertyToData.get(spec.property);
if (data === undefined) {
data = {
processAndComponentTreeBuilder:
new tr.b.MultiDimensionalViewBuilder(
2 /* dimensions (process name and component path) */,
dumpCount /* valueCount */),
unit: unit,
descriptionPrefixBuilder: spec.descriptionPrefixBuilder
};
propertyToData.set(spec.property, data);
} else if (data.unit !== unit) {
throw new Error('Multiple units provided for ' +
createDetailsForErrorMessage() + ':' +
data.unit.unitName + ' and ' + unit.unitName);
} else if (data.descriptionPrefixBuilder !==
spec.descriptionPrefixBuilder) {
throw new Error(
'Multiple description prefix builders provided for' +
createDetailsForErrorMessage());
}
var values = new Array(dumpCount);
values[dumpIndex] = value;
data.processAndComponentTreeBuilder.addPath(
[processNamePath, component] /* path */, values,
tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL /* valueKind */);
});
}
function reportDataAsValues(sourceToPropertyToData, browserName,
customComponentTreeModifier, values) {
// For each source name (e.g. 'reported_by_os')...
sourceToPropertyToData.forEach(function(propertyToData, sourceName) {
// For each property name (e.g. 'effective_size')...
propertyToData.forEach(function(data, propertyName) {
var tree = data.processAndComponentTreeBuilder.buildTopDownTreeView();
var unit = data.unit;
var descriptionPrefixBuilder = data.descriptionPrefixBuilder;
// Total over 'all' processes...
customComponentTreeModifier(tree);
reportComponentDataAsValues(browserName, sourceName,
propertyName, 'all_processes', [] /* componentPath */, tree,
unit, descriptionPrefixBuilder, values);
// For each process name (e.g. 'renderer')...
tree.children[0].forEach(function(processTree, processName) {
if (processTree.children[0].size > 0) {
throw new Error('Multi-dimensional view node for source=' +
sourceName + ', property=' +
(propertyName === undefined ? '(undefined)' : propertyName) +
', process=' + processName +
' has children wrt the process name dimension');
}
customComponentTreeModifier(processTree);
reportComponentDataAsValues(browserName, sourceName,
propertyName, processName, [] /* componentPath */, processTree,
unit, descriptionPrefixBuilder, values);
});
});
});
}
/**
* For the given |browserName| (e.g. 'chrome'), |processName|
* (e.g. 'gpu_process'), |propertyName| (e.g. 'effective_size'),
* |componentPath| (e.g. ['v8']), add a tr.v.Histogram with |unit| aggregating
* the total values of the associated |componentNode| across all timestamps
* (corresponding to global memory dumps associated with the given browser)
* to |values|.
*
* See addMemoryDumpValues for more details.
*/
function reportComponentDataAsValues(
browserName, sourceName, propertyName, processName, componentPath,
componentNode, unit, descriptionPrefixBuilder, values) {
// Construct the name of the memory value.
var nameParts = ['memory', browserName, processName, sourceName].concat(
componentPath);
if (propertyName !== undefined)
nameParts.push(propertyName);
var name = nameParts.join(':');
// Build the underlying numeric for the memory value.
var numeric = buildMemoryNumericFromNode(name, componentNode, unit);
// Build the options for the memory value.
numeric.description = [
descriptionPrefixBuilder(componentPath, processName),
'in',
convertBrowserNameToUserFriendlyName(browserName)
].join(' ');
// Report the memory value.
values.addHistogram(numeric);
// Recursively report memory values for sub-components.
var depth = componentPath.length;
componentPath.push(undefined);
componentNode.children[1].forEach(function(childNode, childName) {
componentPath[depth] = childName;
reportComponentDataAsValues(
browserName, sourceName, propertyName, processName, componentPath,
childNode, unit, descriptionPrefixBuilder, values);
});
componentPath.pop();
}
/**
* Create a memory tr.v.Histogram with |unit| and add all total values in
* |node| to it.
*/
function buildMemoryNumericFromNode(name, node, unit) {
var histogram = new tr.v.Histogram(
name, unit, BOUNDARIES_FOR_UNIT_MAP.get(unit));
node.values.forEach(v => histogram.addSample(v.total));
return histogram;
}
tr.metrics.MetricRegistry.register(memoryMetric, {
supportsRangeOfInterest: true
});
return {
memoryMetric: memoryMetric
};
});
</script>