| <!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/iteration_helpers.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/value/numeric.html"> |
| <link rel="import" href="/tracing/value/ui/scalar_span.html"> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * @fileoverview Helper code for memory dump sub-views. |
| */ |
| tr.exportTo('tr.ui.analysis', function() { |
| |
| var NO_BREAK_SPACE = String.fromCharCode(160); |
| var RIGHTWARDS_ARROW = String.fromCharCode(8594); |
| |
| var COLLATOR = new Intl.Collator(undefined, {numeric: true}); |
| |
| /** |
| * A table column for displaying memory dump row titles. |
| * |
| * @constructor |
| */ |
| function TitleColumn(title) { |
| this.title = title; |
| } |
| |
| TitleColumn.prototype = { |
| supportsCellSelection: false, |
| |
| /** |
| * Get the title associated with a given row. |
| * |
| * This method will decorate the title with color and '+++'/'---' prefix if |
| * appropriate (as determined by the optional row.contexts field). |
| * Examples: |
| * |
| * +----------------------+-----------------+--------+--------+ |
| * | Contexts provided at | Interpretation | Prefix | Color | |
| * +----------------------+-----------------+--------+--------+ |
| * | 1111111111 | always present | | | |
| * | 0000111111 | added | +++ | red | |
| * | 1111111000 | deleted | --- | green | |
| * | 1100111111* | flaky | | purple | |
| * | 0001001111 | added + flaky | +++ | purple | |
| * | 1111100010 | deleted + flaky | --- | purple | |
| * +----------------------+-----------------+--------+--------+ |
| * |
| * *) This means that, given a selection of 10 memory dumps, a particular |
| * row (e.g. a process) was present in the first 2 and last 6 of them |
| * (but not in the third and fourth dump). |
| * |
| * This method should therefore NOT be overriden by subclasses. The |
| * formatTitle method should be overriden instead when necessary. |
| */ |
| value: function(row) { |
| var formattedTitle = this.formatTitle(row); |
| |
| var contexts = row.contexts; |
| if (contexts === undefined || contexts.length === 0) |
| return formattedTitle; |
| |
| // Determine if the row was provided in the first and last row and how |
| // many times it changed between being provided and not provided. |
| var firstContext = contexts[0]; |
| var lastContext = contexts[contexts.length - 1]; |
| var changeDefinedContextCount = 0; |
| for (var i = 1; i < contexts.length; i++) { |
| if ((contexts[i] === undefined) !== (contexts[i - 1] === undefined)) |
| changeDefinedContextCount++; |
| } |
| |
| // Determine the color and prefix of the title. |
| var color = undefined; |
| var prefix = undefined; |
| if (!firstContext && lastContext) { |
| // The row was added. |
| color = 'red'; |
| prefix = '+++'; |
| } else if (firstContext && !lastContext) { |
| // The row was removed. |
| color = 'green'; |
| prefix = '---'; |
| } |
| if (changeDefinedContextCount > 1) { |
| // The row was flaky (added/removed more than once). |
| color = 'purple'; |
| } |
| |
| if (color === undefined && prefix === undefined) |
| return formattedTitle; |
| |
| var titleEl = document.createElement('span'); |
| if (prefix !== undefined) { |
| var prefixEl = tr.ui.b.createSpan({textContent: prefix}); |
| // Enforce same width of '+++' and '---'. |
| prefixEl.style.fontFamily = 'monospace'; |
| Polymer.dom(titleEl).appendChild(prefixEl); |
| Polymer.dom(titleEl).appendChild( |
| tr.ui.b.asHTMLOrTextNode(NO_BREAK_SPACE)); |
| } |
| if (color !== undefined) |
| titleEl.style.color = color; |
| Polymer.dom(titleEl).appendChild( |
| tr.ui.b.asHTMLOrTextNode(formattedTitle)); |
| return titleEl; |
| }, |
| |
| /** |
| * Format the title associated with a given row. This method is intended to |
| * be overriden by subclasses. |
| */ |
| formatTitle: function(row) { |
| return row.title; |
| }, |
| |
| cmp: function(rowA, rowB) { |
| return COLLATOR.compare(rowA.title, rowB.title); |
| } |
| }; |
| |
| /** |
| * Abstract table column for displaying memory dump data. |
| * |
| * @constructor |
| */ |
| function MemoryColumn(name, cellPath, aggregationMode) { |
| this.name = name; |
| this.cellPath = cellPath; |
| |
| // See MemoryColumn.AggregationMode enum in this file. |
| this.aggregationMode = aggregationMode; |
| } |
| |
| /** |
| * Construct columns from cells in a hierarchy of rows and a list of rules. |
| * |
| * The list of rules contains objects with three fields: |
| * |
| * condition: Optional string or regular expression matched against the |
| * name of a cell. If omitted, the rule will match any cell. |
| * importance: Mandatory number which determines the final order of the |
| * columns. The column with the highest importance will be first in the |
| * returned array. |
| * columnConstructor: Mandatory memory column constructor. |
| * |
| * Example: |
| * |
| * var importanceRules = [ |
| * { |
| * condition: 'page_size', |
| * columnConstructor: NumericMemoryColumn, |
| * importance: 8 |
| * }, |
| * { |
| * condition: /size/, |
| * columnConstructor: CustomNumericMemoryColumn, |
| * importance: 10 |
| * }, |
| * { |
| * // No condition: matches all columns. |
| * columnConstructor: NumericMemoryColumn, |
| * importance: 9 |
| * } |
| * ]; |
| * |
| * Given a name of a cell, the corresponding column constructor and |
| * importance are determined by the first rule whose condition matches the |
| * column's name. For example, given a cell with name 'inner_size', the |
| * corresponding column will be constructed using CustomNumericMemoryColumn |
| * and its importance (for sorting purposes) will be 10 (second rule). |
| * |
| * After columns are constructed for all cell names, they are sorted in |
| * descending order of importance and the resulting list is returned. In the |
| * example above, the constructed columns will be sorted into three groups as |
| * follows: |
| * |
| * [most important, left in the resulting table] |
| * 1. columns whose name contains 'size' excluding 'page_size' because it |
| * would have already matched the first rule (Note that string matches |
| * must be exact so a column named 'page_size2' would not match the |
| * first rule and would therefore belong to this group). |
| * 2. columns whose name does not contain 'size'. |
| * 3. columns whose name is 'page_size'. |
| * [least important, right in the resulting table] |
| * |
| * where columns will be sorted alphabetically within each group. |
| */ |
| MemoryColumn.fromRows = function(rows, cellKey, aggregationMode, rules) { |
| // Recursively find the names of all cells of the rows (and their sub-rows). |
| var cellNames = new Set(); |
| function gatherCellNames(rows) { |
| rows.forEach(function(row) { |
| if (row === undefined) |
| return; |
| var fieldCells = row[cellKey]; |
| if (fieldCells !== undefined) { |
| tr.b.iterItems(fieldCells, function(fieldName, fieldCell) { |
| if (fieldCell === undefined || fieldCell.fields === undefined) |
| return; |
| cellNames.add(fieldName); |
| }); |
| } |
| var subRows = row.subRows; |
| if (subRows !== undefined) |
| gatherCellNames(subRows); |
| }); |
| } |
| gatherCellNames(rows); |
| |
| // Based on the provided list of rules, construct the columns and calculate |
| // their importance. |
| var positions = []; |
| cellNames.forEach(function(cellName) { |
| var cellPath = [cellKey, cellName]; |
| var matchingRule = MemoryColumn.findMatchingRule(cellName, rules); |
| var constructor = matchingRule.columnConstructor; |
| var column = new constructor(cellName, cellPath, aggregationMode); |
| positions.push({ |
| importance: matchingRule.importance, |
| column: column |
| }); |
| }); |
| |
| positions.sort(function(a, b) { |
| // Sort columns with the same importance alphabetically. |
| if (a.importance === b.importance) |
| return COLLATOR.compare(a.column.name, b.column.name); |
| |
| // Sort columns in descending order of importance. |
| return b.importance - a.importance; |
| }); |
| |
| return positions.map(function(position) { return position.column }); |
| }; |
| |
| MemoryColumn.spaceEqually = function(columns) { |
| var columnWidth = (100 / columns.length).toFixed(3) + '%'; |
| columns.forEach(function(column) { |
| column.width = columnWidth; |
| }); |
| }; |
| |
| MemoryColumn.findMatchingRule = function(name, rules) { |
| for (var i = 0; i < rules.length; i++) { |
| var rule = rules[i]; |
| if (MemoryColumn.nameMatchesCondition(name, rule.condition)) |
| return rule; |
| } |
| return undefined; |
| }; |
| |
| MemoryColumn.nameMatchesCondition = function(name, condition) { |
| // Rules without conditions match all columns. |
| if (condition === undefined) |
| return true; |
| |
| // String conditions must match the column name exactly. |
| if (typeof(condition) === 'string') |
| return name === condition; |
| |
| // If the condition is not a string, assume it is a RegExp. |
| return condition.test(name); |
| }; |
| |
| /** @enum */ |
| MemoryColumn.AggregationMode = { |
| DIFF: 0, |
| MAX: 1 |
| }; |
| |
| MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER = 'at some selected timestamps'; |
| |
| MemoryColumn.prototype = { |
| get title() { |
| return this.name; |
| }, |
| |
| cell: function(row) { |
| var cell = row; |
| var cellPath = this.cellPath; |
| for (var i = 0; i < cellPath.length; i++) { |
| if (cell === undefined) |
| return undefined; |
| cell = cell[cellPath[i]]; |
| } |
| return cell; |
| }, |
| |
| aggregateCells: function(row, subRows) { |
| // No generic aggregation. |
| }, |
| |
| fields: function(row) { |
| var cell = this.cell(row); |
| if (cell === undefined) |
| return undefined; |
| return cell.fields; |
| }, |
| |
| /** |
| * Format a cell associated with this column from the given row. This |
| * method is not intended to be overriden. |
| */ |
| value: function(row) { |
| var fields = this.fields(row); |
| if (this.hasAllRelevantFieldsUndefined(fields)) |
| return ''; |
| |
| // Determine the color and infos of the resulting element. |
| var contexts = row.contexts; |
| var color = this.color(fields, contexts); |
| var infos = []; |
| this.addInfos(fields, contexts, infos); |
| |
| // Format the actual fields. |
| var formattedFields = this.formatFields(fields); |
| |
| // If no color is specified and there are no infos, there is no need to |
| // wrap the value in a span element.# |
| if ((color === undefined || formattedFields === '') && infos.length === 0) |
| return formattedFields; |
| |
| var fieldEl = document.createElement('span'); |
| fieldEl.style.display = 'flex'; |
| fieldEl.style.alignItems = 'center'; |
| fieldEl.style.justifyContent = 'flex-end'; |
| Polymer.dom(fieldEl).appendChild( |
| tr.ui.b.asHTMLOrTextNode(formattedFields)); |
| |
| // Add info icons with tooltips. |
| infos.forEach(function(info) { |
| var infoEl = document.createElement('span'); |
| infoEl.style.paddingLeft = '4px'; |
| infoEl.style.cursor = 'help'; |
| infoEl.style.fontWeight = 'bold'; |
| Polymer.dom(infoEl).textContent = info.icon; |
| if (info.color !== undefined) |
| infoEl.style.color = info.color; |
| infoEl.title = info.message; |
| Polymer.dom(fieldEl).appendChild(infoEl); |
| }, this); |
| |
| // Set the color of the element. |
| if (color !== undefined) |
| fieldEl.style.color = color; |
| |
| return fieldEl; |
| }, |
| |
| /** |
| * Returns true iff all fields of a row which are relevant for the current |
| * aggregation mode (e.g. first and last field for diff mode) are undefined. |
| */ |
| hasAllRelevantFieldsUndefined: function(fields) { |
| if (fields === undefined) |
| return true; |
| |
| switch (this.aggregationMode) { |
| case MemoryColumn.AggregationMode.DIFF: |
| // Only the first and last field are relevant. |
| return fields[0] === undefined && |
| fields[fields.length - 1] === undefined; |
| |
| case MemoryColumn.AggregationMode.MAX: |
| default: |
| // All fields are relevant. |
| return fields.every(function(field) { return field === undefined; }); |
| } |
| }, |
| |
| /** |
| * Get the color of the given fields formatted by this column. At least one |
| * field relevant for the current aggregation mode is guaranteed to be |
| * defined. |
| */ |
| color: function(fields, contexts) { |
| return undefined; |
| }, |
| |
| /** |
| * Format an arbitrary number of fields. At least one field relevant for |
| * the current aggregation mode is guaranteed to be defined. |
| */ |
| formatFields: function(fields) { |
| if (fields.length === 1) |
| return this.formatSingleField(fields[0]); |
| else |
| return this.formatMultipleFields(fields); |
| }, |
| |
| /** |
| * Format a single defined field. |
| * |
| * This method is intended to be overriden by field type specific columns |
| * (e.g. show '1.0 KiB' instead of '1024' for ScalarNumeric(s) representing |
| * bytes). |
| */ |
| formatSingleField: function(field) { |
| throw new Error('Not implemented'); |
| }, |
| |
| /** |
| * Format multiple fields. At least one field relevant for the current |
| * aggregation mode is guaranteed to be defined. |
| * |
| * The aggregation mode specializations of this method (e.g. |
| * formatMultipleFieldsDiff) are intended to be overriden by field type |
| * specific columns. |
| */ |
| formatMultipleFields: function(fields) { |
| switch (this.aggregationMode) { |
| case MemoryColumn.AggregationMode.DIFF: |
| return this.formatMultipleFieldsDiff( |
| fields[0], fields[fields.length - 1]); |
| |
| case MemoryColumn.AggregationMode.MAX: |
| return this.formatMultipleFieldsMax(fields); |
| |
| default: |
| return tr.ui.b.createSpan({ |
| textContent: '(unsupported aggregation mode)', |
| italic: true |
| }); |
| } |
| }, |
| |
| formatMultipleFieldsDiff: function(firstField, lastField) { |
| throw new Error('Not implemented'); |
| }, |
| |
| formatMultipleFieldsMax: function(fields) { |
| return this.formatSingleField(this.getMaxField(fields)); |
| }, |
| |
| cmp: function(rowA, rowB) { |
| var fieldsA = this.fields(rowA); |
| var fieldsB = this.fields(rowB); |
| |
| // Sanity check. |
| if (fieldsA !== undefined && fieldsB !== undefined && |
| fieldsA.length !== fieldsB.length) |
| throw new Error('Different number of fields'); |
| |
| // Handle empty fields. |
| var undefinedA = this.hasAllRelevantFieldsUndefined(fieldsA); |
| var undefinedB = this.hasAllRelevantFieldsUndefined(fieldsB); |
| if (undefinedA && undefinedB) |
| return 0; |
| if (undefinedA) |
| return -1; |
| if (undefinedB) |
| return 1; |
| |
| return this.compareFields(fieldsA, fieldsB); |
| }, |
| |
| /** |
| * Compare a pair of single or multiple fields. At least one field relevant |
| * for the current aggregation mode is guaranteed to be defined in each of |
| * the two lists. |
| */ |
| compareFields: function(fieldsA, fieldsB) { |
| if (fieldsA.length === 1) |
| return this.compareSingleFields(fieldsA[0], fieldsB[0]); |
| else |
| return this.compareMultipleFields(fieldsA, fieldsB); |
| }, |
| |
| /** |
| * Compare a pair of single defined fields. |
| * |
| * This method is intended to be overriden by field type specific columns. |
| */ |
| compareSingleFields: function(fieldA, fieldB) { |
| throw new Error('Not implemented'); |
| }, |
| |
| /** |
| * Compare a pair of multiple fields. At least one field relevant for the |
| * current aggregation mode is guaranteed to be defined in each of the two |
| * lists. |
| * |
| * The aggregation mode specializations of this method (e.g. |
| * compareMultipleFieldsDiff) are intended to be overriden by field type |
| * specific columns. |
| */ |
| compareMultipleFields: function(fieldsA, fieldsB) { |
| switch (this.aggregationMode) { |
| case MemoryColumn.AggregationMode.DIFF: |
| return this.compareMultipleFieldsDiff( |
| fieldsA[0], fieldsA[fieldsA.length - 1], |
| fieldsB[0], fieldsB[fieldsB.length - 1]); |
| |
| case MemoryColumn.AggregationMode.MAX: |
| return this.compareMultipleFieldsMax(fieldsA, fieldsB); |
| |
| default: |
| return 0; |
| } |
| }, |
| |
| compareMultipleFieldsDiff: function(firstFieldA, lastFieldA, firstFieldB, |
| lastFieldB) { |
| throw new Error('Not implemented'); |
| }, |
| |
| compareMultipleFieldsMax: function(fieldsA, fieldsB) { |
| return this.compareSingleFields( |
| this.getMaxField(fieldsA), this.getMaxField(fieldsB)); |
| }, |
| |
| getMaxField: function(fields) { |
| return fields.reduce(function(accumulator, field) { |
| if (field === undefined) |
| return accumulator; |
| if (accumulator === undefined || |
| this.compareSingleFields(field, accumulator) > 0) { |
| return field; |
| } |
| return accumulator; |
| }.bind(this), undefined); |
| }, |
| |
| addInfos: function(fields, contexts, infos) { |
| // No generic infos. |
| }, |
| |
| getImportance: function(importanceRules) { |
| if (importanceRules.length === 0) |
| return 0; |
| |
| // Find the first matching rule. |
| var matchingRule = |
| MemoryColumn.findMatchingRule(this.name, importanceRules); |
| if (matchingRule !== undefined) |
| return matchingRule.importance; |
| |
| // No matching rule. Return lower importance than all rules. |
| var minImportance = importanceRules[0].importance; |
| for (var i = 1; i < importanceRules.length; i++) |
| minImportance = Math.min(minImportance, importanceRules[i].importance); |
| return minImportance - 1; |
| } |
| }; |
| |
| /** |
| * @constructor |
| */ |
| function StringMemoryColumn(name, cellPath, aggregationMode) { |
| MemoryColumn.call(this, name, cellPath, aggregationMode); |
| } |
| |
| StringMemoryColumn.prototype = { |
| __proto__: MemoryColumn.prototype, |
| |
| formatSingleField: function(string) { |
| return string; |
| }, |
| |
| formatMultipleFieldsDiff: function(firstString, lastString) { |
| if (firstString === undefined) { |
| // String was added ("+NEW_VALUE" in red). |
| var spanEl = tr.ui.b.createSpan({color: 'red'}); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('+')); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( |
| this.formatSingleField(lastString))); |
| return spanEl; |
| } else if (lastString === undefined) { |
| // String was removed ("-OLD_VALUE" in green). |
| var spanEl = tr.ui.b.createSpan({color: 'green'}); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode('-')); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( |
| this.formatSingleField(firstString))); |
| return spanEl; |
| } else if (firstString === lastString) { |
| // String didn't change ("VALUE" with unchanged color). |
| return this.formatSingleField(firstString); |
| } else { |
| // String changed ("OLD_VALUE -> NEW_VALUE" in orange). |
| var spanEl = tr.ui.b.createSpan({color: 'DarkOrange'}); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( |
| this.formatSingleField(firstString))); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( |
| ' ' + RIGHTWARDS_ARROW + ' ')); |
| Polymer.dom(spanEl).appendChild(tr.ui.b.asHTMLOrTextNode( |
| this.formatSingleField(lastString))); |
| return spanEl; |
| } |
| }, |
| |
| compareSingleFields: function(stringA, stringB) { |
| return COLLATOR.compare(stringA, stringB); |
| }, |
| |
| compareMultipleFieldsDiff: function(firstStringA, lastStringA, firstStringB, |
| lastStringB) { |
| // If one of the strings was added (and the other one wasn't), mark the |
| // corresponding diff as greater. |
| if (firstStringA === undefined && firstStringB !== undefined) |
| return 1; |
| if (firstStringA !== undefined && firstStringB === undefined) |
| return -1; |
| |
| // If both strings were added, compare the last values (greater last |
| // value implies greater diff). |
| if (firstStringA === undefined && firstStringB === undefined) |
| return this.compareSingleFields(lastStringA, lastStringB); |
| |
| // If one of the strings was removed (and the other one wasn't), mark the |
| // corresponding diff as lower. |
| if (lastStringA === undefined && lastStringB !== undefined) |
| return -1; |
| if (lastStringA !== undefined && lastStringB === undefined) |
| return 1; |
| |
| // If both strings were removed, compare the first values (greater first |
| // value implies smaller (!) diff). |
| if (lastStringA === undefined && lastStringB === undefined) |
| return this.compareSingleFields(firstStringB, firstStringA); |
| |
| var areStringsAEqual = firstStringA === lastStringA; |
| var areStringsBEqual = firstStringB === lastStringB; |
| |
| // Consider diffs of strings that did not change to be smaller than diffs |
| // of strings that did change. |
| if (areStringsAEqual && areStringsBEqual) |
| return 0; |
| if (areStringsAEqual) |
| return -1; |
| if (areStringsBEqual) |
| return 1; |
| |
| // Both strings changed. We are unable to determine the ordering of the |
| // diffs. |
| return 0; |
| } |
| }; |
| |
| /** |
| * @constructor |
| */ |
| function NumericMemoryColumn(name, cellPath, aggregationMode) { |
| MemoryColumn.call(this, name, cellPath, aggregationMode); |
| } |
| |
| // Avoid tiny positive/negative diffs (displayed in the UI as '+0.0 B' and |
| // '-0.0 B') due to imprecise floating-point arithmetic by treating all diffs |
| // within the (-DIFF_EPSILON, DIFF_EPSILON) range as zeros. |
| NumericMemoryColumn.DIFF_EPSILON = 0.0001; |
| |
| NumericMemoryColumn.prototype = { |
| __proto__: MemoryColumn.prototype, |
| |
| align: tr.ui.b.TableFormat.ColumnAlignment.RIGHT, |
| |
| aggregateCells: function(row, subRows) { |
| var subRowCells = subRows.map(this.cell, this); |
| |
| // Determine if there is at least one defined numeric in the sub-row |
| // cells and the timestamp count. |
| var hasDefinedSubRowNumeric = false; |
| var timestampCount = undefined; |
| subRowCells.forEach(function(subRowCell) { |
| if (subRowCell === undefined) |
| return; |
| |
| var subRowNumerics = subRowCell.fields; |
| if (subRowNumerics === undefined) |
| return; |
| |
| if (timestampCount === undefined) |
| timestampCount = subRowNumerics.length; |
| else if (timestampCount !== subRowNumerics.length) |
| throw new Error('Sub-rows have different numbers of timestamps'); |
| |
| if (hasDefinedSubRowNumeric) |
| return; // Avoid unnecessary traversals of the numerics. |
| hasDefinedSubRowNumeric = subRowNumerics.some(function(numeric) { |
| return numeric !== undefined; |
| }); |
| }); |
| if (!hasDefinedSubRowNumeric) |
| return; // No numeric to aggregate. |
| |
| // Get or create the row cell. |
| var cellPath = this.cellPath; |
| var rowCell = row; |
| for (var i = 0; i < cellPath.length; i++) { |
| var nextStepName = cellPath[i]; |
| var nextStep = rowCell[nextStepName]; |
| if (nextStep === undefined) { |
| if (i < cellPath.length - 1) |
| nextStep = {}; |
| else |
| nextStep = new MemoryCell(undefined); |
| rowCell[nextStepName] = nextStep; |
| } |
| rowCell = nextStep; |
| } |
| if (rowCell.fields === undefined) { |
| rowCell.fields = new Array(timestampCount); |
| } else if (rowCell.fields.length !== timestampCount) { |
| throw new Error( |
| 'Row has a different number of timestamps than sub-rows'); |
| } |
| |
| for (var i = 0; i < timestampCount; i++) { |
| if (rowCell.fields[i] !== undefined) |
| continue; |
| rowCell.fields[i] = tr.model.MemoryAllocatorDump.aggregateNumerics( |
| subRowCells.map(function(subRowCell) { |
| if (subRowCell === undefined || subRowCell.fields === undefined) |
| return undefined; |
| return subRowCell.fields[i]; |
| })); |
| } |
| }, |
| |
| formatSingleField: function(numeric) { |
| var formattingContext = this.getFormattingContext(numeric.unit); |
| var config = formattingContext !== undefined ? |
| { context: formattingContext } : undefined; |
| return tr.v.ui.createScalarSpan(numeric, config); |
| }, |
| |
| getFormattingContext: function(unit) { |
| return undefined; |
| }, |
| |
| formatMultipleFieldsDiff: function(firstNumeric, lastNumeric) { |
| return this.formatSingleField( |
| this.getDiffField_(firstNumeric, lastNumeric)); |
| }, |
| |
| compareSingleFields: function(numericA, numericB) { |
| return numericA.value - numericB.value; |
| }, |
| |
| compareMultipleFieldsDiff: function(firstNumericA, lastNumericA, |
| firstNumericB, lastNumericB) { |
| return this.getDiffFieldValue_(firstNumericA, lastNumericA) - |
| this.getDiffFieldValue_(firstNumericB, lastNumericB); |
| }, |
| |
| getDiffField_: function(firstNumeric, lastNumeric) { |
| var definedNumeric = firstNumeric || lastNumeric; |
| return new tr.v.ScalarNumeric(definedNumeric.unit.correspondingDeltaUnit, |
| this.getDiffFieldValue_(firstNumeric, lastNumeric)); |
| }, |
| |
| getDiffFieldValue_: function(firstNumeric, lastNumeric) { |
| var firstValue = firstNumeric === undefined ? 0 : firstNumeric.value; |
| var lastValue = lastNumeric === undefined ? 0 : lastNumeric.value; |
| var diff = lastValue - firstValue; |
| return Math.abs(diff) < NumericMemoryColumn.DIFF_EPSILON ? 0 : diff; |
| } |
| }; |
| |
| /** |
| * @constructor |
| */ |
| function MemoryCell(fields) { |
| this.fields = fields; |
| } |
| |
| MemoryCell.extractFields = function(cell) { |
| if (cell === undefined) |
| return undefined; |
| return cell.fields; |
| }; |
| |
| /** Limit for the number of sub-rows for recursive table row expansion. */ |
| var RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT = 10; |
| |
| function expandTableRowsRecursively(table) { |
| var currentLevelRows = table.tableRows; |
| var totalVisibleRowCount = currentLevelRows.length; |
| |
| while (currentLevelRows.length > 0) { |
| // Calculate the total number of sub-rows on the current level. |
| var nextLevelRowCount = 0; |
| currentLevelRows.forEach(function(currentLevelRow) { |
| var subRows = currentLevelRow.subRows; |
| if (subRows === undefined || subRows.length === 0) |
| return; |
| nextLevelRowCount += subRows.length; |
| }); |
| |
| // Determine whether expanding all rows on the current level would cause |
| // the total number of visible rows go over the limit. |
| if (totalVisibleRowCount + nextLevelRowCount > |
| RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT) { |
| break; |
| } |
| |
| // Expand all rows on the current level and gather their sub-rows. |
| var nextLevelRows = new Array(nextLevelRowCount); |
| var nextLevelRowIndex = 0; |
| currentLevelRows.forEach(function(currentLevelRow) { |
| var subRows = currentLevelRow.subRows; |
| if (subRows === undefined || subRows.length === 0) |
| return; |
| table.setExpandedForTableRow(currentLevelRow, true); |
| subRows.forEach(function(subRow) { |
| nextLevelRows[nextLevelRowIndex++] = subRow; |
| }); |
| }); |
| |
| // Update the total number of visible rows and progress to the next level. |
| totalVisibleRowCount += nextLevelRowCount; |
| currentLevelRows = nextLevelRows; |
| } |
| } |
| |
| function aggregateTableRowCellsRecursively(row, columns, opt_predicate) { |
| var subRows = row.subRows; |
| if (subRows === undefined || subRows.length === 0) |
| return; |
| |
| subRows.forEach(function(subRow) { |
| aggregateTableRowCellsRecursively(subRow, columns, opt_predicate); |
| }); |
| |
| if (opt_predicate === undefined || opt_predicate(row.contexts)) |
| aggregateTableRowCells(row, subRows, columns); |
| } |
| |
| function aggregateTableRowCells(row, subRows, columns) { |
| columns.forEach(function(column) { |
| if (!(column instanceof MemoryColumn)) |
| return; |
| column.aggregateCells(row, subRows); |
| }); |
| } |
| |
| function createCells(timeToValues, valueFieldsGetter, opt_this) { |
| opt_this = opt_this || this; |
| var fieldNameToFields = tr.b.invertArrayOfDicts( |
| timeToValues, valueFieldsGetter, opt_this); |
| return tr.b.mapItems(fieldNameToFields, function(fieldName, fields) { |
| return new tr.ui.analysis.MemoryCell(fields); |
| }); |
| } |
| |
| function createWarningInfo(message) { |
| return { |
| message: message, |
| icon: String.fromCharCode(9888), |
| color: 'red' |
| }; |
| } |
| |
| // TODO(petrcermak): Use a context manager instead |
| // (https://github.com/catapult-project/catapult/issues/2420). |
| function DetailsNumericMemoryColumn(name, cellPath, aggregationMode) { |
| NumericMemoryColumn.call(this, name, cellPath, aggregationMode); |
| } |
| |
| DetailsNumericMemoryColumn.prototype = { |
| __proto__: NumericMemoryColumn.prototype, |
| |
| getFormattingContext: function(unit) { |
| if (unit.baseUnit === tr.b.Unit.byName.sizeInBytes) |
| return { unitPrefix: tr.b.UnitScale.Binary.KIBI }; |
| return undefined; |
| } |
| }; |
| |
| return { |
| TitleColumn: TitleColumn, |
| MemoryColumn: MemoryColumn, |
| StringMemoryColumn: StringMemoryColumn, |
| NumericMemoryColumn: NumericMemoryColumn, |
| MemoryCell: MemoryCell, |
| expandTableRowsRecursively: expandTableRowsRecursively, |
| aggregateTableRowCellsRecursively: aggregateTableRowCellsRecursively, |
| aggregateTableRowCells: aggregateTableRowCells, |
| createCells: createCells, |
| createWarningInfo: createWarningInfo, |
| DetailsNumericMemoryColumn: DetailsNumericMemoryColumn |
| }; |
| }); |
| </script> |