blob: 411de1a91bf41f002a77a3c99ab825d4b8d233aa [file] [log] [blame]
const HeapSnapshotLoader = (function (exports) {
'use strict';
// Copyright (c) 2020 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.
/**
* @param {string} inputString
* @param {string} charsToEscape
* @return {string} the string with any matching chars escaped
*/
const escapeCharacters = (inputString, charsToEscape) => {
let foundChar = false;
for (let i = 0; i < charsToEscape.length; ++i) {
if (inputString.indexOf(charsToEscape.charAt(i)) !== -1) {
foundChar = true;
break;
}
}
if (!foundChar) {
return String(inputString);
}
let result = '';
for (let i = 0; i < inputString.length; ++i) {
if (charsToEscape.indexOf(inputString.charAt(i)) !== -1) {
result += '\\';
}
result += inputString.charAt(i);
}
return result;
};
/**
* @enum {string}
*/
const FORMATTER_TYPES = {
STRING: 'string',
SPECIFIER: 'specifier',
};
/**
* @param {string} formatString
* @param {!Object.<string, function(string, ...*):*>} formatters
* @return {!Array.<!FORMATTER_TOKEN>}
*/
const tokenizeFormatString = function(formatString, formatters) {
/** @type {!Array<!FORMATTER_TOKEN>} */
const tokens = [];
/**
* @param {string} str
*/
function addStringToken(str) {
if (!str) {
return;
}
if (tokens.length && tokens[tokens.length - 1].type === FORMATTER_TYPES.STRING) {
tokens[tokens.length - 1].value += str;
} else {
tokens.push({
type: FORMATTER_TYPES.STRING,
value: str,
specifier: undefined,
precision: undefined,
substitutionIndex: undefined
});
}
}
/**
* @param {string} specifier
* @param {number} precision
* @param {number} substitutionIndex
*/
function addSpecifierToken(specifier, precision, substitutionIndex) {
tokens.push({type: FORMATTER_TYPES.SPECIFIER, specifier, precision, substitutionIndex, value: undefined});
}
/**
* @param {number} code
*/
function addAnsiColor(code) {
/**
* @type {!Object<number, string>}
*/
const types = {3: 'color', 9: 'colorLight', 4: 'bgColor', 10: 'bgColorLight'};
const colorCodes = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightGray', '', 'default'];
const colorCodesLight =
['darkGray', 'lightRed', 'lightGreen', 'lightYellow', 'lightBlue', 'lightMagenta', 'lightCyan', 'white', ''];
/** @type {!Object<string, !Array<string>>} */
const colors = {color: colorCodes, colorLight: colorCodesLight, bgColor: colorCodes, bgColorLight: colorCodesLight};
const type = types[Math.floor(code / 10)];
if (!type) {
return;
}
const color = colors[type][code % 10];
if (!color) {
return;
}
tokens.push({
type: FORMATTER_TYPES.SPECIFIER,
specifier: 'c',
value: {description: (type.startsWith('bg') ? 'background : ' : 'color: ') + color},
precision: undefined,
substitutionIndex: undefined,
});
}
let textStart = 0;
let substitutionIndex = 0;
const re =
new RegExp(`%%|%(?:(\\d+)\\$)?(?:\\.(\\d*))?([${Object.keys(formatters).join('')}])|\\u001b\\[(\\d+)m`, 'g');
for (let match = re.exec(formatString); !!match; match = re.exec(formatString)) {
const matchStart = match.index;
if (matchStart > textStart) {
addStringToken(formatString.substring(textStart, matchStart));
}
if (match[0] === '%%') {
addStringToken('%');
} else if (match[0].startsWith('%')) {
// eslint-disable-next-line no-unused-vars
const [_, substitionString, precisionString, specifierString] = match;
if (substitionString && Number(substitionString) > 0) {
substitutionIndex = Number(substitionString) - 1;
}
const precision = precisionString ? Number(precisionString) : -1;
addSpecifierToken(specifierString, precision, substitutionIndex);
++substitutionIndex;
} else {
const code = Number(match[4]);
addAnsiColor(code);
}
textStart = matchStart + match[0].length;
}
addStringToken(formatString.substring(textStart));
return tokens;
};
/**
* @param {string} formatString
* @param {?ArrayLike<*>} substitutions
* @param {!Object.<string, function(string, ...*):*>} formatters
* @param {!T} initialValue
* @param {function(T, *): T} append
* @param {!Array.<!FORMATTER_TOKEN>=} tokenizedFormat
* @return {!{formattedResult: T, unusedSubstitutions: ?ArrayLike<*>}};
* @template T
*/
const format = function(formatString, substitutions, formatters, initialValue, append, tokenizedFormat) {
if (!formatString || ((!substitutions || !substitutions.length) && formatString.search(/\u001b\[(\d+)m/) === -1)) {
return {formattedResult: append(initialValue, formatString), unusedSubstitutions: substitutions};
}
function prettyFunctionName() {
return 'String.format("' + formatString + '", "' + Array.prototype.join.call(substitutions, '", "') + '")';
}
/**
* @param {string} msg
*/
function warn(msg) {
console.warn(prettyFunctionName() + ': ' + msg);
}
/**
* @param {string} msg
*/
function error(msg) {
console.error(prettyFunctionName() + ': ' + msg);
}
let result = initialValue;
const tokens = tokenizedFormat || tokenizeFormatString(formatString, formatters);
/** @type {!Object<number, boolean>} */
const usedSubstitutionIndexes = {};
/** @type {!ArrayLike<*>} */
const actualSubstitutions = substitutions || [];
for (let i = 0; i < tokens.length; ++i) {
const token = tokens[i];
if (token.type === FORMATTER_TYPES.STRING) {
result = append(result, token.value);
continue;
}
if (token.type !== FORMATTER_TYPES.SPECIFIER) {
error('Unknown token type "' + token.type + '" found.');
continue;
}
if (!token.value && token.substitutionIndex !== undefined &&
token.substitutionIndex >= actualSubstitutions.length) {
// If there are not enough substitutions for the current substitutionIndex
// just output the format specifier literally and move on.
error(
'not enough substitution arguments. Had ' + actualSubstitutions.length + ' but needed ' +
(token.substitutionIndex + 1) + ', so substitution was skipped.');
result = append(
result,
'%' + ((token.precision !== undefined && token.precision > -1) ? token.precision : '') + token.specifier);
continue;
}
if (!token.value && token.substitutionIndex !== undefined) {
usedSubstitutionIndexes[token.substitutionIndex] = true;
}
if (token.specifier === undefined || !(token.specifier in formatters)) {
// Encountered an unsupported format character, treat as a string.
warn('unsupported format character \u201C' + token.specifier + '\u201D. Treating as a string.');
result = append(
result,
(token.value || token.substitutionIndex === undefined) ? '' : actualSubstitutions[token.substitutionIndex]);
continue;
}
result = append(
result,
formatters[token.specifier](
token.value || (token.substitutionIndex !== undefined && actualSubstitutions[token.substitutionIndex]),
token));
}
const unusedSubstitutions = [];
for (let i = 0; i < actualSubstitutions.length; ++i) {
if (i in usedSubstitutionIndexes) {
continue;
}
unusedSubstitutions.push(actualSubstitutions[i]);
}
return {formattedResult: result, unusedSubstitutions: unusedSubstitutions};
};
const standardFormatters = {
/**
* @param {*} substitution
* @return {number}
*/
d: function(substitution) {
return /** @type {number} */ (!isNaN(substitution) ? substitution : 0);
},
/**
* @param {*} substitution
* @param {!FORMATTER_TOKEN} token
* @return {number}
*/
f: function(substitution, token) {
if (substitution && token.precision !== undefined && token.precision > -1) {
substitution = substitution.toFixed(token.precision);
}
const precision = (token.precision !== undefined && token.precision > -1) ? Number(0).toFixed(token.precision) : 0;
return /** @type number} */ (!isNaN(substitution) ? substitution : precision);
},
/**
* @param {*} substitution
* @return {string}
*/
s: function(substitution) {
return /** @type {string} */ (substitution);
}
};
/**
* @param {string} formatString
* @param {!Array.<*>} substitutions
* @return {string}
*/
const vsprintf = function(formatString, substitutions) {
// @ts-ignore
return format(formatString, substitutions, standardFormatters, '', function(a, b) {
return a + b;
}).formattedResult;
};
/**
* @param {string} format
* @param {...*} var_arg
* @return {string}
*/
const sprintf = function(format, var_arg) {
return vsprintf(format, Array.prototype.slice.call(arguments, 1));
};
/*
* Copyright (C) 2007 Apple Inc. All rights reserved.
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Still used in the test runners that can't use ES modules :(
String.sprintf = sprintf;
/**
* @param {string} chars
* @return {string}
*/
/**
* @return {string}
*/
String.regexSpecialCharacters = function() {
return '^[]{}()\\.^$*+?|-,';
};
/**
* @this {string}
* @return {string}
*/
String.prototype.escapeForRegExp = function() {
return escapeCharacters(this, String.regexSpecialCharacters());
};
/**
* @param {string} query
* @return {!RegExp}
*/
String.filterRegex = function(query) {
const toEscape = String.regexSpecialCharacters();
let regexString = '';
for (let i = 0; i < query.length; ++i) {
let c = query.charAt(i);
if (toEscape.indexOf(c) !== -1) {
c = '\\' + c;
}
if (i) {
regexString += '[^\\0' + c + ']*';
}
regexString += c;
}
return new RegExp(regexString, 'i');
};
/**
* @param {number} maxLength
* @return {string}
*/
String.prototype.trimMiddle = function(maxLength) {
if (this.length <= maxLength) {
return String(this);
}
let leftHalf = maxLength >> 1;
let rightHalf = maxLength - leftHalf - 1;
if (this.codePointAt(this.length - rightHalf - 1) >= 0x10000) {
--rightHalf;
++leftHalf;
}
if (leftHalf > 0 && this.codePointAt(leftHalf - 1) >= 0x10000) {
--leftHalf;
}
return this.substr(0, leftHalf) + '…' + this.substr(this.length - rightHalf, rightHalf);
};
/**
* @param {number} maxLength
* @return {string}
*/
String.prototype.trimEndWithMaxLength = function(maxLength) {
if (this.length <= maxLength) {
return String(this);
}
return this.substr(0, maxLength - 1) + '…';
};
/**
* @param {string|undefined} string
* @return {number}
*/
String.hashCode = function(string) {
if (!string) {
return 0;
}
// Hash algorithm for substrings is described in "Über die Komplexität der Multiplikation in
// eingeschränkten Branchingprogrammmodellen" by Woelfe.
// http://opendatastructures.org/versions/edition-0.1d/ods-java/node33.html#SECTION00832000000000000000
const p = ((1 << 30) * 4 - 5); // prime: 2^32 - 5
const z = 0x5033d967; // 32 bits from random.org
const z2 = 0x59d2f15d; // random odd 32 bit number
let s = 0;
let zi = 1;
for (let i = 0; i < string.length; i++) {
const xi = string.charCodeAt(i) * z2;
s = (s + zi * xi) % p;
zi = (zi * z) % p;
}
s = (s + zi * (p - 1)) % p;
return Math.abs(s | 0);
};
/**
* @param {string} a
* @param {string} b
* @return {number}
*/
String.naturalOrderComparator = function(a, b) {
const chunk = /^\d+|^\D+/;
let chunka, chunkb, anum, bnum;
while (1) {
if (a) {
if (!b) {
return 1;
}
} else {
if (b) {
return -1;
}
return 0;
}
chunka = a.match(chunk)[0];
chunkb = b.match(chunk)[0];
anum = !isNaN(chunka);
bnum = !isNaN(chunkb);
if (anum && !bnum) {
return -1;
}
if (bnum && !anum) {
return 1;
}
if (anum && bnum) {
const diff = chunka - chunkb;
if (diff) {
return diff;
}
if (chunka.length !== chunkb.length) {
if (!+chunka && !+chunkb) { // chunks are strings of all 0s (special case)
return chunka.length - chunkb.length;
}
return chunkb.length - chunka.length;
}
} else if (chunka !== chunkb) {
return (chunka < chunkb) ? -1 : 1;
}
a = a.substring(chunka.length);
b = b.substring(chunkb.length);
}
};
/**
* @param {string} a
* @param {string} b
* @return {number}
*/
String.caseInsensetiveComparator = function(a, b) {
a = a.toUpperCase();
b = b.toUpperCase();
if (a === b) {
return 0;
}
return a > b ? 1 : -1;
};
/**
* @param {string} value
* @return {string}
*/
Number.toFixedIfFloating = function(value) {
if (!value || isNaN(value)) {
return value;
}
const number = Number(value);
return number % 1 ? number.toFixed(3) : String(number);
};
(function() {
const partition = {
/**
* @this {Array.<number>}
* @param {function(number, number): number} comparator
* @param {number} left
* @param {number} right
* @param {number} pivotIndex
*/
value: function(comparator, left, right, pivotIndex) {
function swap(array, i1, i2) {
const temp = array[i1];
array[i1] = array[i2];
array[i2] = temp;
}
const pivotValue = this[pivotIndex];
swap(this, right, pivotIndex);
let storeIndex = left;
for (let i = left; i < right; ++i) {
if (comparator(this[i], pivotValue) < 0) {
swap(this, storeIndex, i);
++storeIndex;
}
}
swap(this, right, storeIndex);
return storeIndex;
},
configurable: true
};
Object.defineProperty(Array.prototype, 'partition', partition);
Object.defineProperty(Uint32Array.prototype, 'partition', partition);
const sortRange = {
/**
* @param {function(number, number): number} comparator
* @param {number} leftBound
* @param {number} rightBound
* @param {number} sortWindowLeft
* @param {number} sortWindowRight
* @return {!Array.<number>}
* @this {Array.<number>}
*/
value: function(comparator, leftBound, rightBound, sortWindowLeft, sortWindowRight) {
function quickSortRange(array, comparator, left, right, sortWindowLeft, sortWindowRight) {
if (right <= left) {
return;
}
const pivotIndex = Math.floor(Math.random() * (right - left)) + left;
const pivotNewIndex = array.partition(comparator, left, right, pivotIndex);
if (sortWindowLeft < pivotNewIndex) {
quickSortRange(array, comparator, left, pivotNewIndex - 1, sortWindowLeft, sortWindowRight);
}
if (pivotNewIndex < sortWindowRight) {
quickSortRange(array, comparator, pivotNewIndex + 1, right, sortWindowLeft, sortWindowRight);
}
}
if (leftBound === 0 && rightBound === (this.length - 1) && sortWindowLeft === 0 && sortWindowRight >= rightBound) {
this.sort(comparator);
} else {
quickSortRange(this, comparator, leftBound, rightBound, sortWindowLeft, sortWindowRight);
}
return this;
},
configurable: true
};
Object.defineProperty(Array.prototype, 'sortRange', sortRange);
Object.defineProperty(Uint32Array.prototype, 'sortRange', sortRange);
})();
Object.defineProperty(Array.prototype, 'lowerBound', {
/**
* Return index of the leftmost element that is equal or greater
* than the specimen object. If there's no such element (i.e. all
* elements are smaller than the specimen) returns right bound.
* The function works for sorted array.
* When specified, |left| (inclusive) and |right| (exclusive) indices
* define the search window.
*
* @param {!T} object
* @param {function(!T,!S):number=} comparator
* @param {number=} left
* @param {number=} right
* @return {number}
* @this {Array.<!S>}
* @template T,S
*/
value: function(object, comparator, left, right) {
function defaultComparator(a, b) {
return a < b ? -1 : (a > b ? 1 : 0);
}
comparator = comparator || defaultComparator;
let l = left || 0;
let r = right !== undefined ? right : this.length;
while (l < r) {
const m = (l + r) >> 1;
if (comparator(object, this[m]) > 0) {
l = m + 1;
} else {
r = m;
}
}
return r;
},
configurable: true
});
Object.defineProperty(Array.prototype, 'upperBound', {
/**
* Return index of the leftmost element that is greater
* than the specimen object. If there's no such element (i.e. all
* elements are smaller or equal to the specimen) returns right bound.
* The function works for sorted array.
* When specified, |left| (inclusive) and |right| (exclusive) indices
* define the search window.
*
* @param {!T} object
* @param {function(!T,!S):number=} comparator
* @param {number=} left
* @param {number=} right
* @return {number}
* @this {Array.<!S>}
* @template T,S
*/
value: function(object, comparator, left, right) {
function defaultComparator(a, b) {
return a < b ? -1 : (a > b ? 1 : 0);
}
comparator = comparator || defaultComparator;
let l = left || 0;
let r = right !== undefined ? right : this.length;
while (l < r) {
const m = (l + r) >> 1;
if (comparator(object, this[m]) >= 0) {
l = m + 1;
} else {
r = m;
}
}
return r;
},
configurable: true
});
Object.defineProperty(Uint32Array.prototype, 'lowerBound', {value: Array.prototype.lowerBound, configurable: true});
Object.defineProperty(Uint32Array.prototype, 'upperBound', {value: Array.prototype.upperBound, configurable: true});
Object.defineProperty(Int32Array.prototype, 'lowerBound', {value: Array.prototype.lowerBound, configurable: true});
Object.defineProperty(Int32Array.prototype, 'upperBound', {value: Array.prototype.upperBound, configurable: true});
Object.defineProperty(Float64Array.prototype, 'lowerBound', {value: Array.prototype.lowerBound, configurable: true});
Object.defineProperty(Array.prototype, 'binaryIndexOf', {
/**
* @param {!T} value
* @param {function(!T,!S):number} comparator
* @return {number}
* @this {Array.<!S>}
* @template T,S
*/
value: function(value, comparator) {
const index = this.lowerBound(value, comparator);
return index < this.length && comparator(value, this[index]) === 0 ? index : -1;
},
configurable: true
});
Object.defineProperty(Array.prototype, 'peekLast', {
/**
* @return {!T|undefined}
* @this {Array.<!T>}
* @template T
*/
value: function() {
return this[this.length - 1];
},
configurable: true
});
(function() {
/**
* @param {!Array.<T>} array1
* @param {!Array.<T>} array2
* @param {function(T,T):number} comparator
* @param {boolean} mergeNotIntersect
* @return {!Array.<T>}
* @template T
*/
function mergeOrIntersect(array1, array2, comparator, mergeNotIntersect) {
const result = [];
let i = 0;
let j = 0;
while (i < array1.length && j < array2.length) {
const compareValue = comparator(array1[i], array2[j]);
if (mergeNotIntersect || !compareValue) {
result.push(compareValue <= 0 ? array1[i] : array2[j]);
}
if (compareValue <= 0) {
i++;
}
if (compareValue >= 0) {
j++;
}
}
if (mergeNotIntersect) {
while (i < array1.length) {
result.push(array1[i++]);
}
while (j < array2.length) {
result.push(array2[j++]);
}
}
return result;
}
Object.defineProperty(Array.prototype, 'intersectOrdered', {
/**
* @param {!Array.<T>} array
* @param {function(T,T):number} comparator
* @return {!Array.<T>}
* @this {!Array.<T>}
* @template T
*/
value: function(array, comparator) {
return mergeOrIntersect(this, array, comparator, false);
},
configurable: true
});
Object.defineProperty(Array.prototype, 'mergeOrdered', {
/**
* @param {!Array.<T>} array
* @param {function(T,T):number} comparator
* @return {!Array.<T>}
* @this {!Array.<T>}
* @template T
*/
value: function(array, comparator) {
return mergeOrIntersect(this, array, comparator, true);
},
configurable: true
});
})();
/**
* @param {string} query
* @param {boolean} caseSensitive
* @param {boolean} isRegex
* @return {!RegExp}
*/
self.createSearchRegex = function(query, caseSensitive, isRegex) {
const regexFlags = caseSensitive ? 'g' : 'gi';
let regexObject;
if (isRegex) {
try {
regexObject = new RegExp(query, regexFlags);
} catch (e) {
// Silent catch.
}
}
if (!regexObject) {
regexObject = self.createPlainTextSearchRegex(query, regexFlags);
}
return regexObject;
};
/**
* @param {string} query
* @param {string=} flags
* @return {!RegExp}
*/
self.createPlainTextSearchRegex = function(query, flags) {
// This should be kept the same as the one in StringUtil.cpp.
const regexSpecialCharacters = String.regexSpecialCharacters();
let regex = '';
for (let i = 0; i < query.length; ++i) {
const c = query.charAt(i);
if (regexSpecialCharacters.indexOf(c) !== -1) {
regex += '\\';
}
regex += c;
}
return new RegExp(regex, flags || '');
};
/**
* @param {number} spacesCount
* @return {string}
*/
self.spacesPadding = function(spacesCount) {
return '\xA0'.repeat(spacesCount);
};
/**
* @param {number} value
* @param {number} symbolsCount
* @return {string}
*/
self.numberToStringWithSpacesPadding = function(value, symbolsCount) {
const numberString = value.toString();
const paddingLength = Math.max(0, symbolsCount - numberString.length);
return self.spacesPadding(paddingLength) + numberString;
};
/**
* @return {?T}
* @template T
*/
Set.prototype.firstValue = function() {
if (!this.size) {
return null;
}
return this.values().next().value;
};
/**
* @return {!Platform.Multimap<!KEY, !VALUE>}
*/
Map.prototype.inverse = function() {
const result = new Platform.Multimap();
for (const key of this.keys()) {
const value = this.get(key);
result.set(value, key);
}
return result;
};
/**
* @template K, V
*/
class Multimap {
constructor() {
/** @type {!Map.<K, !Set.<!V>>} */
this._map = new Map();
}
/**
* @param {K} key
* @param {V} value
*/
set(key, value) {
let set = this._map.get(key);
if (!set) {
set = new Set();
this._map.set(key, set);
}
set.add(value);
}
/**
* @param {K} key
* @return {!Set<!V>}
*/
get(key) {
return this._map.get(key) || new Set();
}
/**
* @param {K} key
* @return {boolean}
*/
has(key) {
return this._map.has(key);
}
/**
* @param {K} key
* @param {V} value
* @return {boolean}
*/
hasValue(key, value) {
const set = this._map.get(key);
if (!set) {
return false;
}
return set.has(value);
}
/**
* @return {number}
*/
get size() {
return this._map.size;
}
/**
* @param {K} key
* @param {V} value
* @return {boolean}
*/
delete(key, value) {
const values = this.get(key);
if (!values) {
return false;
}
const result = values.delete(value);
if (!values.size) {
this._map.delete(key);
}
return result;
}
/**
* @param {K} key
*/
deleteAll(key) {
this._map.delete(key);
}
/**
* @return {!Array.<K>}
*/
keysArray() {
return [...this._map.keys()];
}
/**
* @return {!Array.<!V>}
*/
valuesArray() {
const result = [];
for (const set of this._map.values()) {
result.push(...set.values());
}
return result;
}
clear() {
this._map.clear();
}
}
/**
* @param {*} value
*/
self.suppressUnused = function(value) {};
/**
* @param {function()} callback
* @return {number}
*/
self.setImmediate = function(callback) {
const args = [...arguments].slice(1);
Promise.resolve().then(() => callback(...args));
return 0;
};
/**
* TODO: move into its own module
* @param {function()} callback
* @suppressGlobalPropertiesCheck
*/
self.runOnWindowLoad = function(callback) {
/**
* @suppressGlobalPropertiesCheck
*/
function windowLoaded() {
self.removeEventListener('DOMContentLoaded', windowLoaded, false);
callback();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
callback();
} else {
self.addEventListener('DOMContentLoaded', windowLoaded, false);
}
};
const _singletonSymbol = Symbol('singleton');
/**
* @template T
* @param {function(new:T, ...)} constructorFunction
* @return {!T}
*/
self.singleton = function(constructorFunction) {
if (_singletonSymbol in constructorFunction) {
return constructorFunction[_singletonSymbol];
}
const instance = new constructorFunction();
constructorFunction[_singletonSymbol] = instance;
return instance;
};
/**
* @param {?string} content
* @return {number}
*/
self.base64ToSize = function(content) {
if (!content) {
return 0;
}
let size = content.length * 3 / 4;
if (content[content.length - 1] === '=') {
size--;
}
if (content.length > 1 && content[content.length - 2] === '=') {
size--;
}
return size;
};
/**
* @param {?string} input
* @return {string}
*/
self.unescapeCssString = function(input) {
// https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
const reCssEscapeSequence = /(?<!\\)\\(?:([a-fA-F0-9]{1,6})|(.))[\n\t\x20]?/gs;
return input.replace(reCssEscapeSequence, (_, $1, $2) => {
if ($2) { // Handle the single-character escape sequence.
return $2;
}
// Otherwise, handle the code point escape sequence.
const codePoint = parseInt($1, 16);
const isSurrogate = 0xD800 <= codePoint && codePoint <= 0xDFFF;
if (isSurrogate || codePoint === 0x0000 || codePoint > 0x10FFFF) {
return '\uFFFD';
}
return String.fromCodePoint(codePoint);
});
};
self.Platform = self.Platform || {};
Platform = Platform || {};
/** @constructor */
Platform.Multimap = Multimap;
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
* Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @param {string} string
* @param {...*} vararg
* @return {string}
*/
function UIString(string, ...vararg) {
return vsprintf(localize(string), Array.prototype.slice.call(arguments, 1));
}
/**
* @param {string} string
* @param {?ArrayLike<*>} values
* @return {string}
*/
function serializeUIString(string, values = []) {
const messageParts = [string];
const serializedMessage = {messageParts, values};
return JSON.stringify(serializedMessage);
}
/**
* @param {string=} serializedMessage
* @return {*}
*/
function deserializeUIString(serializedMessage) {
if (!serializedMessage) {
return {};
}
return JSON.parse(serializedMessage);
}
/**
* @param {string} string
* @return {string}
*/
function localize(string) {
return string;
}
/**
* @unrestricted
*/
class UIStringFormat {
/**
* @param {string} format
*/
constructor(format) {
/** @type {string} */
this._localizedFormat = localize(format);
/** @type {!Array.<!StringUtilities.FORMATTER_TOKEN>} */
this._tokenizedFormat =
tokenizeFormatString(this._localizedFormat, standardFormatters);
}
/**
* @param {string} a
* @param {*} b
* @return {string}
*/
static _append(a, b) {
return a + b;
}
/**
* @param {...*} vararg
* @return {string}
*/
format(vararg) {
// the code here uses odd generics that Closure likes but TS doesn't
// so rather than fight to typecheck this in a dodgy way we just let TS ignore it
// @ts-ignore
return format(
this._localizedFormat, arguments, standardFormatters, '', UIStringFormat._append,
this._tokenizedFormat)
.formattedResult;
}
}
const _substitutionStrings = new WeakMap();
/**
* @param {!ITemplateArray|string} strings
* @param {...*} vararg
* @return {string}
*/
function ls$1(strings, ...vararg) {
if (typeof strings === 'string') {
return strings;
}
let substitutionString = _substitutionStrings.get(strings);
if (!substitutionString) {
substitutionString = strings.join('%s');
_substitutionStrings.set(strings, substitutionString);
}
// @ts-ignore TS gets confused with the arguments slicing
return UIString(substitutionString, ...vararg);
}
var UIString$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
UIString: UIString,
serializeUIString: serializeUIString,
deserializeUIString: deserializeUIString,
localize: localize,
UIStringFormat: UIStringFormat,
ls: ls$1
});
/*
* Copyright (C) 2019 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
const ls = x => x;
// Copyright 2020 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.
/**
* Combine the two given colors according to alpha blending.
* @param {!Array<number>} fgRGBA
* @param {!Array<number>} bgRGBA
* @return {!Array<number>}
*/
function blendColors(fgRGBA, bgRGBA) {
const alpha = fgRGBA[3];
return [
((1 - alpha) * bgRGBA[0]) + (alpha * fgRGBA[0]),
((1 - alpha) * bgRGBA[1]) + (alpha * fgRGBA[1]),
((1 - alpha) * bgRGBA[2]) + (alpha * fgRGBA[2]),
alpha + (bgRGBA[3] * (1 - alpha)),
];
}
/**
* @param {!Array<number>} rgba
* @return {!Array<number>}
*/
function rgbaToHsla([r, g, b, a]) {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
const sum = max + min;
let h;
if (min === max) {
h = 0;
} else if (r === max) {
h = ((1 / 6 * (g - b) / diff) + 1) % 1;
} else if (g === max) {
h = (1 / 6 * (b - r) / diff) + 1 / 3;
} else {
h = (1 / 6 * (r - g) / diff) + 2 / 3;
}
const l = 0.5 * sum;
let s;
if (l === 0) {
s = 0;
} else if (l === 1) {
s = 0;
} else if (l <= 0.5) {
s = diff / sum;
} else {
s = diff / (2 - sum);
}
return [h, s, l, a];
}
/**
* Calculate the luminance of this color using the WCAG algorithm.
* See http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
* @param {!Array<number>} rgba
* @return {number}
*/
function luminance([rSRGB, gSRGB, bSRGB]) {
const r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055) / 1.055), 2.4);
const g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055) / 1.055), 2.4);
const b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055) / 1.055), 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
/*
* Copyright (C) 2009 Apple Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/** @type {?Map<string, string>} */
let _rgbaToNickname;
/**
* @unrestricted
*/
class Color {
/**
* @param {!Array.<number>} rgba
* @param {!Format} format
* @param {string=} originalText
*/
constructor(rgba, format, originalText) {
this._hsla = undefined;
this._rgba = rgba;
this._originalText = originalText || null;
this._originalTextIsValid = !!this._originalText;
this._format = format;
if (typeof this._rgba[3] === 'undefined') {
this._rgba[3] = 1;
}
for (let i = 0; i < 4; ++i) {
if (this._rgba[i] < 0) {
this._rgba[i] = 0;
this._originalTextIsValid = false;
}
if (this._rgba[i] > 1) {
this._rgba[i] = 1;
this._originalTextIsValid = false;
}
}
}
/**
* @param {string} text
* @return {?Color}
*/
static parse(text) {
// Simple - #hex, nickname
const value = text.toLowerCase().replace(/\s+/g, '');
const simple = /^(?:#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|(\w+))$/i;
let match = value.match(simple);
if (match) {
if (match[1]) { // hex
let hex = match[1].toLowerCase();
let format;
if (hex.length === 3) {
format = Format.ShortHEX;
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
} else if (hex.length === 4) {
format = Format.ShortHEXA;
hex = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2) +
hex.charAt(3) + hex.charAt(3);
} else if (hex.length === 6) {
format = Format.HEX;
} else {
format = Format.HEXA;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
let a = 1;
if (hex.length === 8) {
a = parseInt(hex.substring(6, 8), 16) / 255;
}
return new Color([r / 255, g / 255, b / 255, a], format, text);
}
if (match[2]) { // nickname
const nickname = match[2].toLowerCase();
if (nickname in Nicknames) {
const rgba = Nicknames[nickname];
const color = Color.fromRGBA(rgba);
color._format = Format.Nickname;
color._originalText = text;
return color;
}
return null;
}
return null;
}
// rgb/rgba(), hsl/hsla()
match = text.toLowerCase().match(/^\s*(?:(rgba?)|(hsla?))\((.*)\)\s*$/);
if (match) {
const components = match[3].trim();
let values = components.split(/\s*,\s*/);
if (values.length === 1) {
values = components.split(/\s+/);
if (values[3] === '/') {
values.splice(3, 1);
if (values.length !== 4) {
return null;
}
} else if ((values.length > 2 && values[2].indexOf('/') !== -1) || (values.length > 3 && values[3].indexOf('/') !== -1)) {
const alpha = values.slice(2, 4).join('');
values = values.slice(0, 2).concat(alpha.split(/\//)).concat(values.slice(4));
} else if (values.length >= 4) {
return null;
}
}
if (values.length !== 3 && values.length !== 4 || values.indexOf('') > -1) {
return null;
}
const hasAlpha = (values[3] !== undefined);
if (match[1]) { // rgb/rgba
const rgba = [
Color._parseRgbNumeric(values[0]), Color._parseRgbNumeric(values[1]), Color._parseRgbNumeric(values[2]),
hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1
];
if (rgba.indexOf(null) > -1) {
return null;
}
return new Color(/** @type {!Array.<number>} */ (rgba), hasAlpha ? Format.RGBA : Format.RGB, text);
}
if (match[2]) { // hsl/hsla
const hsla = [
Color._parseHueNumeric(values[0]), Color._parseSatLightNumeric(values[1]),
Color._parseSatLightNumeric(values[2]), hasAlpha ? Color._parseAlphaNumeric(values[3]) : 1
];
if (hsla.indexOf(null) > -1) {
return null;
}
/** @type {!Array.<number>} */
const rgba = [];
Color.hsl2rgb(/** @type {!Array.<number>} */ (hsla), rgba);
return new Color(rgba, hasAlpha ? Format.HSLA : Format.HSL, text);
}
}
return null;
}
/**
* @param {!Array.<number>} rgba
* @return {!Color}
*/
static fromRGBA(rgba) {
return new Color([rgba[0] / 255, rgba[1] / 255, rgba[2] / 255, rgba[3]], Format.RGBA);
}
/**
* @param {!Array.<number>} hsva
* @return {!Color}
*/
static fromHSVA(hsva) {
/** @type {!Array.<number>} */
const rgba = [];
Color.hsva2rgba(hsva, rgba);
return new Color(rgba, Format.HSLA);
}
/**
* @param {string} value
* @return {number|null}
*/
static _parsePercentOrNumber(value) {
// @ts-ignore: isNaN can accept strings
if (isNaN(value.replace('%', ''))) {
return null;
}
const parsed = parseFloat(value);
if (value.indexOf('%') !== -1) {
if (value.indexOf('%') !== value.length - 1) {
return null;
}
return parsed / 100;
}
return parsed;
}
/**
* @param {string} value
* @return {number|null}
*/
static _parseRgbNumeric(value) {
const parsed = Color._parsePercentOrNumber(value);
if (parsed === null) {
return null;
}
if (value.indexOf('%') !== -1) {
return parsed;
}
return parsed / 255;
}
/**
* @param {string} value
* @return {number|null}
*/
static _parseHueNumeric(value) {
const angle = value.replace(/(deg|g?rad|turn)$/, '');
// @ts-ignore: isNaN can accept strings
if (isNaN(angle) || value.match(/\s+(deg|g?rad|turn)/)) {
return null;
}
const number = parseFloat(angle);
if (value.indexOf('turn') !== -1) {
return number % 1;
}
if (value.indexOf('grad') !== -1) {
return (number / 400) % 1;
}
if (value.indexOf('rad') !== -1) {
return (number / (2 * Math.PI)) % 1;
}
return (number / 360) % 1;
}
/**
* @param {string} value
* @return {number|null}
*/
static _parseSatLightNumeric(value) {
// @ts-ignore: isNaN can accept strings
if (value.indexOf('%') !== value.length - 1 || isNaN(value.replace('%', ''))) {
return null;
}
const parsed = parseFloat(value);
return Math.min(1, parsed / 100);
}
/**
* @param {string} value
* @return {number|null}
*/
static _parseAlphaNumeric(value) {
return Color._parsePercentOrNumber(value);
}
/**
* @param {!Array.<number>} hsva
* @param {!Array.<number>} out_hsla
*/
static _hsva2hsla(hsva, out_hsla) {
const h = hsva[0];
let s = hsva[1];
const v = hsva[2];
const t = (2 - s) * v;
if (v === 0 || s === 0) {
s = 0;
} else {
s *= v / (t < 1 ? t : 2 - t);
}
out_hsla[0] = h;
out_hsla[1] = s;
out_hsla[2] = t / 2;
out_hsla[3] = hsva[3];
}
/**
* @param {!Array.<number>} hsl
* @param {!Array.<number>} out_rgb
*/
static hsl2rgb(hsl, out_rgb) {
const h = hsl[0];
let s = hsl[1];
const l = hsl[2];
/**
* @param {number} p
* @param {number} q
* @param {number} h
*/
function hue2rgb(p, q, h) {
if (h < 0) {
h += 1;
} else if (h > 1) {
h -= 1;
}
if ((h * 6) < 1) {
return p + (q - p) * h * 6;
}
if ((h * 2) < 1) {
return q;
}
if ((h * 3) < 2) {
return p + (q - p) * ((2 / 3) - h) * 6;
}
return p;
}
if (s < 0) {
s = 0;
}
let q;
if (l <= 0.5) {
q = l * (1 + s);
} else {
q = l + s - (l * s);
}
const p = 2 * l - q;
const tr = h + (1 / 3);
const tg = h;
const tb = h - (1 / 3);
out_rgb[0] = hue2rgb(p, q, tr);
out_rgb[1] = hue2rgb(p, q, tg);
out_rgb[2] = hue2rgb(p, q, tb);
out_rgb[3] = hsl[3];
}
/**
* @param {!Array<number>} hsva
* @param {!Array<number>} out_rgba
*/
static hsva2rgba(hsva, out_rgba) {
Color._hsva2hsla(hsva, _tmpHSLA);
Color.hsl2rgb(_tmpHSLA, out_rgba);
for (let i = 0; i < _tmpHSLA.length; i++) {
_tmpHSLA[i] = 0;
}
}
/**
* Compute a desired luminance given a given luminance and a desired contrast
* ratio.
* @param {number} luminance The given luminance.
* @param {number} contrast The desired contrast ratio.
* @param {boolean} lighter Whether the desired luminance is lighter or darker
* than the given luminance. If no luminance can be found which meets this
* requirement, a luminance which meets the inverse requirement will be
* returned.
* @return {number} The desired luminance.
*/
static desiredLuminance(luminance, contrast, lighter) {
function computeLuminance() {
if (lighter) {
return (luminance + 0.05) * contrast - 0.05;
}
return (luminance + 0.05) / contrast - 0.05;
}
let desiredLuminance = computeLuminance();
if (desiredLuminance < 0 || desiredLuminance > 1) {
lighter = !lighter;
desiredLuminance = computeLuminance();
}
return desiredLuminance;
}
/**
* Approach a value of the given component of `candidateHSVA` such that the
* calculated luminance of `candidateHSVA` approximates `desiredLuminance`.
* @param {!Array<number>} candidateHSVA
* @param {!Array<number>} bgRGBA
* @param {number} index - the index of the color component
* @param {number} desiredLuminance
* @return {?number} The new value for the modified component, or `null` if
* no suitable value exists.
*/
static approachColorValue(candidateHSVA, bgRGBA, index, desiredLuminance) {
const candidateLuminance = () => {
return luminance(blendColors(Color.fromHSVA(candidateHSVA).rgba(), bgRGBA));
};
const epsilon = 0.0002;
let x = candidateHSVA[index];
let multiplier = 1;
let dLuminance = candidateLuminance() - desiredLuminance;
let previousSign = Math.sign(dLuminance);
for (let guard = 100; guard; guard--) {
if (Math.abs(dLuminance) < epsilon) {
candidateHSVA[index] = x;
return x;
}
const sign = Math.sign(dLuminance);
if (sign !== previousSign) {
// If `x` overshoots the correct value, halve the step size.
multiplier /= 2;
previousSign = sign;
} else if (x < 0 || x > 1) {
// If there is no overshoot and `x` is out of bounds, there is no
// acceptable value for `x`.
return null;
}
// Adjust `x` by a multiple of `dLuminance` to decrease step size as
// the computed luminance converges on `desiredLuminance`.
x += multiplier * (index === 2 ? -dLuminance : dLuminance);
candidateHSVA[index] = x;
dLuminance = candidateLuminance() - desiredLuminance;
}
// The loop should always converge or go out of bounds on its own.
console.error('Loop exited unexpectedly');
return null;
}
/**
*
* @param {!Color} fgColor
* @param {!Color} bgColor
* @param {number} requiredContrast
* @return {?Color}
*/
static findFgColorForContrast(fgColor, bgColor, requiredContrast) {
const candidateHSVA = fgColor.hsva();
const bgRGBA = bgColor.rgba();
const candidateLuminance = () => {
return luminance(blendColors(Color.fromHSVA(candidateHSVA).rgba(), bgRGBA));
};
const bgLuminance = luminance(bgColor.rgba());
const fgLuminance = candidateLuminance();
const fgIsLighter = fgLuminance > bgLuminance;
const desiredLuminance = Color.desiredLuminance(bgLuminance, requiredContrast, fgIsLighter);
const saturationComponentIndex = 1;
const valueComponentIndex = 2;
if (Color.approachColorValue(candidateHSVA, bgRGBA, valueComponentIndex, desiredLuminance)) {
return Color.fromHSVA(candidateHSVA);
}
candidateHSVA[valueComponentIndex] = 1;
if (Color.approachColorValue(candidateHSVA, bgRGBA, saturationComponentIndex, desiredLuminance)) {
return Color.fromHSVA(candidateHSVA);
}
return null;
}
/**
* @return {!Format}
*/
format() {
return this._format;
}
/**
* @return {!Array.<number>} HSLA with components within [0..1]
*/
hsla() {
if (this._hsla) {
return this._hsla;
}
this._hsla = rgbaToHsla(this._rgba);
return this._hsla;
}
/**
* @return {!Array.<number>}
*/
canonicalHSLA() {
const hsla = this.hsla();
return [Math.round(hsla[0] * 360), Math.round(hsla[1] * 100), Math.round(hsla[2] * 100), hsla[3]];
}
/**
* @return {!Array.<number>} HSVA with components within [0..1]
*/
hsva() {
const hsla = this.hsla();
const h = hsla[0];
let s = hsla[1];
const l = hsla[2];
s *= l < 0.5 ? l : 1 - l;
return [h, s !== 0 ? 2 * s / (l + s) : 0, (l + s), hsla[3]];
}
/**
* @return {boolean}
*/
hasAlpha() {
return this._rgba[3] !== 1;
}
/**
* @return {!Format}
*/
detectHEXFormat() {
let canBeShort = true;
for (let i = 0; i < 4; ++i) {
const c = Math.round(this._rgba[i] * 255);
if (c % 17) {
canBeShort = false;
break;
}
}
const hasAlpha = this.hasAlpha();
const cf = Format;
if (canBeShort) {
return hasAlpha ? cf.ShortHEXA : cf.ShortHEX;
}
return hasAlpha ? cf.HEXA : cf.HEX;
}
/**
* @param {?string=} format
* @return {?string}
*/
asString(format) {
if (format === this._format && this._originalTextIsValid) {
return this._originalText;
}
if (!format) {
format = this._format;
}
/**
* @param {number} value
* @return {number}
*/
function toRgbValue(value) {
return Math.round(value * 255);
}
/**
* @param {number} value
* @return {string}
*/
function toHexValue(value) {
const hex = Math.round(value * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
/**
* @param {number} value
* @return {string}
*/
function toShortHexValue(value) {
return (Math.round(value * 255) / 17).toString(16);
}
switch (format) {
case Format.Original: {
return this._originalText;
}
case Format.RGB:
case Format.RGBA: {
const start = sprintf(
'rgb(%d %d %d', toRgbValue(this._rgba[0]), toRgbValue(this._rgba[1]), toRgbValue(this._rgba[2]));
if (this.hasAlpha()) {
return start + sprintf(' / %d%)', Math.round(this._rgba[3] * 100));
}
return start + ')';
}
case Format.HSL:
case Format.HSLA: {
const hsla = this.hsla();
const start = sprintf(
'hsl(%ddeg %d% %d%', Math.round(hsla[0] * 360), Math.round(hsla[1] * 100), Math.round(hsla[2] * 100));
if (this.hasAlpha()) {
return start + sprintf(' / %d%)', Math.round(hsla[3] * 100));
}
return start + ')';
}
case Format.HEXA: {
return sprintf(
'#%s%s%s%s', toHexValue(this._rgba[0]), toHexValue(this._rgba[1]), toHexValue(this._rgba[2]),
toHexValue(this._rgba[3]))
.toLowerCase();
}
case Format.HEX: {
if (this.hasAlpha()) {
return null;
}
return sprintf('#%s%s%s', toHexValue(this._rgba[0]), toHexValue(this._rgba[1]), toHexValue(this._rgba[2]))
.toLowerCase();
}
case Format.ShortHEXA: {
const hexFormat = this.detectHEXFormat();
if (hexFormat !== Format.ShortHEXA && hexFormat !== Format.ShortHEX) {
return null;
}
return sprintf(
'#%s%s%s%s', toShortHexValue(this._rgba[0]), toShortHexValue(this._rgba[1]),
toShortHexValue(this._rgba[2]), toShortHexValue(this._rgba[3]))
.toLowerCase();
}
case Format.ShortHEX: {
if (this.hasAlpha()) {
return null;
}
if (this.detectHEXFormat() !== Format.ShortHEX) {
return null;
}
return sprintf(
'#%s%s%s', toShortHexValue(this._rgba[0]), toShortHexValue(this._rgba[1]),
toShortHexValue(this._rgba[2]))
.toLowerCase();
}
case Format.Nickname: {
return this.nickname();
}
}
return this._originalText;
}
/**
* @return {!Array<number>}
*/
rgba() {
return this._rgba.slice();
}
/**
* @return {!Array.<number>}
*/
canonicalRGBA() {
const rgba = new Array(4);
for (let i = 0; i < 3; ++i) {
rgba[i] = Math.round(this._rgba[i] * 255);
}
rgba[3] = this._rgba[3];
return rgba;
}
/**
* @return {?string} nickname
*/
nickname() {
if (!_rgbaToNickname) {
_rgbaToNickname = new Map();
for (const nickname in Nicknames) {
let rgba = Nicknames[nickname];
if (rgba.length !== 4) {
rgba = rgba.concat(1);
}
_rgbaToNickname.set(String(rgba), nickname);
}
}
return _rgbaToNickname.get(String(this.canonicalRGBA())) || null;
}
/**
* @return {!{r: number, g: number, b: number, a: (number|undefined)}}
*/
toProtocolRGBA() {
const rgba = this.canonicalRGBA();
/** @type {!{r: number, g: number, b: number, a: (number|undefined)}} */
const result = {r: rgba[0], g: rgba[1], b: rgba[2], a: undefined};
if (rgba[3] !== 1) {
result.a = rgba[3];
}
return result;
}
/**
* @return {!Color}
*/
invert() {
const rgba = [];
rgba[0] = 1 - this._rgba[0];
rgba[1] = 1 - this._rgba[1];
rgba[2] = 1 - this._rgba[2];
rgba[3] = this._rgba[3];
return new Color(rgba, Format.RGBA);
}
/**
* @param {number} alpha
* @return {!Color}
*/
setAlpha(alpha) {
const rgba = this._rgba.slice();
rgba[3] = alpha;
return new Color(rgba, Format.RGBA);
}
/**
* @param {!Color} fgColor
* @return {!Color}
*/
blendWith(fgColor) {
/** @type {!Array.<number>} */
const rgba = blendColors(fgColor._rgba, this._rgba);
return new Color(rgba, Format.RGBA);
}
/**
* @param {!Format} format
*/
setFormat(format) {
this._format = format;
}
}
/**
* @enum {string}
*/
const Format = {
Original: 'original',
Nickname: 'nickname',
HEX: 'hex',
ShortHEX: 'shorthex',
HEXA: 'hexa',
ShortHEXA: 'shorthexa',
RGB: 'rgb',
RGBA: 'rgba',
HSL: 'hsl',
HSLA: 'hsla'
};
/** @type {!Object<string, !Array.<number>>} */
const Nicknames = {
'aliceblue': [240, 248, 255],
'antiquewhite': [250, 235, 215],
'aqua': [0, 255, 255],
'aquamarine': [127, 255, 212],
'azure': [240, 255, 255],
'beige': [245, 245, 220],
'bisque': [255, 228, 196],
'black': [0, 0, 0],
'blanchedalmond': [255, 235, 205],
'blue': [0, 0, 255],
'blueviolet': [138, 43, 226],
'brown': [165, 42, 42],
'burlywood': [222, 184, 135],
'cadetblue': [95, 158, 160],
'chartreuse': [127, 255, 0],
'chocolate': [210, 105, 30],
'coral': [255, 127, 80],
'cornflowerblue': [100, 149, 237],
'cornsilk': [255, 248, 220],
'crimson': [237, 20, 61],
'cyan': [0, 255, 255],
'darkblue': [0, 0, 139],
'darkcyan': [0, 139, 139],
'darkgoldenrod': [184, 134, 11],
'darkgray': [169, 169, 169],
'darkgrey': [169, 169, 169],
'darkgreen': [0, 100, 0],
'darkkhaki': [189, 183, 107],
'darkmagenta': [139, 0, 139],
'darkolivegreen': [85, 107, 47],
'darkorange': [255, 140, 0],
'darkorchid': [153, 50, 204],
'darkred': [139, 0, 0],
'darksalmon': [233, 150, 122],
'darkseagreen': [143, 188, 143],
'darkslateblue': [72, 61, 139],
'darkslategray': [47, 79, 79],
'darkslategrey': [47, 79, 79],
'darkturquoise': [0, 206, 209],
'darkviolet': [148, 0, 211],
'deeppink': [255, 20, 147],
'deepskyblue': [0, 191, 255],
'dimgray': [105, 105, 105],
'dimgrey': [105, 105, 105],
'dodgerblue': [30, 144, 255],
'firebrick': [178, 34, 34],
'floralwhite': [255, 250, 240],
'forestgreen': [34, 139, 34],
'fuchsia': [255, 0, 255],
'gainsboro': [220, 220, 220],
'ghostwhite': [248, 248, 255],
'gold': [255, 215, 0],
'goldenrod': [218, 165, 32],
'gray': [128, 128, 128],
'grey': [128, 128, 128],
'green': [0, 128, 0],
'greenyellow': [173, 255, 47],
'honeydew': [240, 255, 240],
'hotpink': [255, 105, 180],
'indianred': [205, 92, 92],
'indigo': [75, 0, 130],
'ivory': [255, 255, 240],
'khaki': [240, 230, 140],
'lavender': [230, 230, 250],
'lavenderblush': [255, 240, 245],
'lawngreen': [124, 252, 0],
'lemonchiffon': [255, 250, 205],
'lightblue': [173, 216, 230],
'lightcoral': [240, 128, 128],
'lightcyan': [224, 255, 255],
'lightgoldenrodyellow': [250, 250, 210],
'lightgreen': [144, 238, 144],
'lightgray': [211, 211, 211],
'lightgrey': [211, 211, 211],
'lightpink': [255, 182, 193],
'lightsalmon': [255, 160, 122],
'lightseagreen': [32, 178, 170],
'lightskyblue': [135, 206, 250],
'lightslategray': [119, 136, 153],
'lightslategrey': [119, 136, 153],
'lightsteelblue': [176, 196, 222],
'lightyellow': [255, 255, 224],
'lime': [0, 255, 0],
'limegreen': [50, 205, 50],
'linen': [250, 240, 230],
'magenta': [255, 0, 255],
'maroon': [128, 0, 0],
'mediumaquamarine': [102, 205, 170],
'mediumblue': [0, 0, 205],
'mediumorchid': [186, 85, 211],
'mediumpurple': [147, 112, 219],
'mediumseagreen': [60, 179, 113],
'mediumslateblue': [123, 104, 238],
'mediumspringgreen': [0, 250, 154],
'mediumturquoise': [72, 209, 204],
'mediumvioletred': [199, 21, 133],
'midnightblue': [25, 25, 112],
'mintcream': [245, 255, 250],
'mistyrose': [255, 228, 225],
'moccasin': [255, 228, 181],
'navajowhite': [255, 222, 173],
'navy': [0, 0, 128],
'oldlace': [253, 245, 230],
'olive': [128, 128, 0],
'olivedrab': [107, 142, 35],
'orange': [255, 165, 0],
'orangered': [255, 69, 0],
'orchid': [218, 112, 214],
'palegoldenrod': [238, 232, 170],
'palegreen': [152, 251, 152],
'paleturquoise': [175, 238, 238],
'palevioletred': [219, 112, 147],
'papayawhip': [255, 239, 213],
'peachpuff': [255, 218, 185],
'peru': [205, 133, 63],
'pink': [255, 192, 203],
'plum': [221, 160, 221],
'powderblue': [176, 224, 230],
'purple': [128, 0, 128],
'rebeccapurple': [102, 51, 153],
'red': [255, 0, 0],
'rosybrown': [188, 143, 143],
'royalblue': [65, 105, 225],
'saddlebrown': [139, 69, 19],
'salmon': [250, 128, 114],
'sandybrown': [244, 164, 96],
'seagreen': [46, 139, 87],
'seashell': [255, 245, 238],
'sienna': [160, 82, 45],
'silver': [192, 192, 192],
'skyblue': [135, 206, 235],
'slateblue': [106, 90, 205],
'slategray': [112, 128, 144],
'slategrey': [112, 128, 144],
'snow': [255, 250, 250],
'springgreen': [0, 255, 127],
'steelblue': [70, 130, 180],
'tan': [210, 180, 140],
'teal': [0, 128, 128],
'thistle': [216, 191, 216],
'tomato': [255, 99, 71],
'turquoise': [64, 224, 208],
'violet': [238, 130, 238],
'wheat': [245, 222, 179],
'white': [255, 255, 255],
'whitesmoke': [245, 245, 245],
'yellow': [255, 255, 0],
'yellowgreen': [154, 205, 50],
'transparent': [0, 0, 0, 0],
};
const PageHighlight = {
Content: Color.fromRGBA([111, 168, 220, .66]),
ContentLight: Color.fromRGBA([111, 168, 220, .5]),
ContentOutline: Color.fromRGBA([9, 83, 148]),
Padding: Color.fromRGBA([147, 196, 125, .55]),
PaddingLight: Color.fromRGBA([147, 196, 125, .4]),
Border: Color.fromRGBA([255, 229, 153, .66]),
BorderLight: Color.fromRGBA([255, 229, 153, .5]),
Margin: Color.fromRGBA([246, 178, 107, .66]),
MarginLight: Color.fromRGBA([246, 178, 107, .5]),
EventTarget: Color.fromRGBA([255, 196, 196, .66]),
Shape: Color.fromRGBA([96, 82, 177, 0.8]),
ShapeMargin: Color.fromRGBA([96, 82, 127, .6]),
CssGrid: Color.fromRGBA([0x4b, 0, 0x82, 1]),
GridRowLine: Color.fromRGBA([127, 32, 210, 1]),
GridColumnLine: Color.fromRGBA([127, 32, 210, 1]),
GridBorder: Color.fromRGBA([127, 32, 210, 1]),
GridRowGapBackground: Color.fromRGBA([127, 32, 210, .3]),
GridColumnGapBackground: Color.fromRGBA([127, 32, 210, .3]),
GridRowGapHatch: Color.fromRGBA([127, 32, 210, .8]),
GridColumnGapHatch: Color.fromRGBA([127, 32, 210, .8]),
GridAreaBorder: Color.fromRGBA([26, 115, 232, 1]),
};
const SourceOrderHighlight = {
ParentOutline: Color.fromRGBA([224, 90, 183, 1]),
ChildOutline: Color.fromRGBA([0, 120, 212, 1]),
};
const _tmpHSLA = [0, 0, 0, 0];
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
class ParsedURL {
/**
* @param {string} url
*/
constructor(url) {
this.isValid = false;
this.url = url;
this.scheme = '';
this.user = '';
this.host = '';
this.port = '';
this.path = '';
this.queryParams = '';
this.fragment = '';
this.folderPathComponents = '';
this.lastPathComponent = '';
const isBlobUrl = this.url.startsWith('blob:');
const urlToMatch = isBlobUrl ? url.substring(5) : url;
const match = urlToMatch.match(ParsedURL._urlRegex());
if (match) {
this.isValid = true;
if (isBlobUrl) {
this._blobInnerScheme = match[2].toLowerCase();
this.scheme = 'blob';
} else {
this.scheme = match[2].toLowerCase();
}
this.user = match[3];
this.host = match[4];
this.port = match[5];
this.path = match[6] || '/';
this.queryParams = match[7] || '';
this.fragment = match[8];
} else {
if (this.url.startsWith('data:')) {
this.scheme = 'data';
return;
}
if (this.url.startsWith('blob:')) {
this.scheme = 'blob';
return;
}
if (this.url === 'about:blank') {
this.scheme = 'about';
return;
}
this.path = this.url;
}
const lastSlashIndex = this.path.lastIndexOf('/');
if (lastSlashIndex !== -1) {
this.folderPathComponents = this.path.substring(0, lastSlashIndex);
this.lastPathComponent = this.path.substring(lastSlashIndex + 1);
} else {
this.lastPathComponent = this.path;
}
}
/**
* @param {string} string
* @return {?ParsedURL}
*/
static fromString(string) {
const parsedURL = new ParsedURL(string.toString());
if (parsedURL.isValid) {
return parsedURL;
}
return null;
}
/**
* @param {string} fileSystemPath
* @return {string}
*/
static platformPathToURL(fileSystemPath) {
fileSystemPath = fileSystemPath.replace(/\\/g, '/');
if (!fileSystemPath.startsWith('file://')) {
if (fileSystemPath.startsWith('/')) {
fileSystemPath = 'file://' + fileSystemPath;
} else {
fileSystemPath = 'file:///' + fileSystemPath;
}
}
return fileSystemPath;
}
/**
* @param {string} fileURL
* @param {boolean=} isWindows
* @return {string}
*/
static urlToPlatformPath(fileURL, isWindows) {
console.assert(fileURL.startsWith('file://'), 'This must be a file URL.');
if (isWindows) {
return fileURL.substr('file:///'.length).replace(/\//g, '\\');
}
return fileURL.substr('file://'.length);
}
/**
* @param {string} url
* @return {string}
*/
static urlWithoutHash(url) {
const hashIndex = url.indexOf('#');
if (hashIndex !== -1) {
return url.substr(0, hashIndex);
}
return url;
}
/**
* @return {!RegExp}
*/
static _urlRegex() {
if (ParsedURL._urlRegexInstance) {
return ParsedURL._urlRegexInstance;
}
// RegExp groups:
// 1 - scheme, hostname, ?port
// 2 - scheme (using the RFC3986 grammar)
// 3 - ?user:password
// 4 - hostname
// 5 - ?port
// 6 - ?path
// 7 - ?query
// 8 - ?fragment
const schemeRegex = /([A-Za-z][A-Za-z0-9+.-]*):\/\//;
const userRegex = /(?:([A-Za-z0-9\-._~%!$&'()*+,;=:]*)@)?/;
const hostRegex = /((?:\[::\d?\])|(?:[^\s\/:]*))/;
const portRegex = /(?::([\d]+))?/;
const pathRegex = /(\/[^#?]*)?/;
const queryRegex = /(?:\?([^#]*))?/;
const fragmentRegex = /(?:#(.*))?/;
ParsedURL._urlRegexInstance = new RegExp(
'^(' + schemeRegex.source + userRegex.source + hostRegex.source + portRegex.source + ')' + pathRegex.source +
queryRegex.source + fragmentRegex.source + '$');
return ParsedURL._urlRegexInstance;
}
/**
* @param {string} url
* @return {string}
*/
static extractPath(url) {
const parsedURL = this.fromString(url);
return parsedURL ? parsedURL.path : '';
}
/**
* @param {string} url
* @return {string}
*/
static extractOrigin(url) {
const parsedURL = this.fromString(url);
return parsedURL ? parsedURL.securityOrigin() : '';
}
/**
* @param {string} url
* @return {string}
*/
static extractExtension(url) {
url = ParsedURL.urlWithoutHash(url);
const indexOfQuestionMark = url.indexOf('?');
if (indexOfQuestionMark !== -1) {
url = url.substr(0, indexOfQuestionMark);
}
const lastIndexOfSlash = url.lastIndexOf('/');
if (lastIndexOfSlash !== -1) {
url = url.substr(lastIndexOfSlash + 1);
}
const lastIndexOfDot = url.lastIndexOf('.');
if (lastIndexOfDot !== -1) {
url = url.substr(lastIndexOfDot + 1);
const lastIndexOfPercent = url.indexOf('%');
if (lastIndexOfPercent !== -1) {
return url.substr(0, lastIndexOfPercent);
}
return url;
}
return '';
}
/**
* @param {string} url
* @return {string}
*/
static extractName(url) {
let index = url.lastIndexOf('/');
const pathAndQuery = index !== -1 ? url.substr(index + 1) : url;
index = pathAndQuery.indexOf('?');
return index < 0 ? pathAndQuery : pathAndQuery.substr(0, index);
}
/**
* @param {string} baseURL
* @param {string} href
* @return {?string}
*/
static completeURL(baseURL, href) {
// Return special URLs as-is.
const trimmedHref = href.trim();
if (trimmedHref.startsWith('data:') || trimmedHref.startsWith('blob:') || trimmedHref.startsWith('javascript:') ||
trimmedHref.startsWith('mailto:')) {
return href;
}
// Return absolute URLs as-is.
const parsedHref = this.fromString(trimmedHref);
if (parsedHref && parsedHref.scheme) {
return trimmedHref;
}
const parsedURL = this.fromString(baseURL);
if (!parsedURL) {
return null;
}
if (parsedURL.isDataURL()) {
return href;
}
if (href.length > 1 && href.charAt(0) === '/' && href.charAt(1) === '/') {
// href starts with "//" which is a full URL with the protocol dropped (use the baseURL protocol).
return parsedURL.scheme + ':' + href;
}
const securityOrigin = parsedURL.securityOrigin();
const pathText = parsedURL.path;
const queryText = parsedURL.queryParams ? '?' + parsedURL.queryParams : '';
// Empty href resolves to a URL without fragment.
if (!href.length) {
return securityOrigin + pathText + queryText;
}
if (href.charAt(0) === '#') {
return securityOrigin + pathText + queryText + href;
}
if (href.charAt(0) === '?') {
return securityOrigin + pathText + href;
}
const hrefMatches = href.match(/^[^#?]*/);
if (!hrefMatches || !href.length) {
throw new Error('Invalid href');
}
let hrefPath = hrefMatches[0];
const hrefSuffix = href.substring(hrefPath.length);
if (hrefPath.charAt(0) !== '/') {
hrefPath = parsedURL.folderPathComponents + '/' + hrefPath;
}
// @ts-ignore Runtime needs to be properly exported
return securityOrigin + Root.Runtime.normalizePath(hrefPath) + hrefSuffix;
}
/**
* @param {string} string
* @return {!{url: string, lineNumber: (number|undefined), columnNumber: (number|undefined)}}
*/
static splitLineAndColumn(string) {
// Only look for line and column numbers in the path to avoid matching port numbers.
const beforePathMatch = string.match(ParsedURL._urlRegex());
let beforePath = '';
let pathAndAfter = string;
if (beforePathMatch) {
beforePath = beforePathMatch[1];
pathAndAfter = string.substring(beforePathMatch[1].length);
}
const lineColumnRegEx = /(?::(\d+))?(?::(\d+))?$/;
const lineColumnMatch = lineColumnRegEx.exec(pathAndAfter);
let lineNumber;
let columnNumber;
console.assert(!!lineColumnMatch);
if (!lineColumnMatch) {
return { url: string, lineNumber: 0, columnNumber: 0 };
}
if (typeof(lineColumnMatch[1]) === 'string') {
lineNumber = parseInt(lineColumnMatch[1], 10);
// Immediately convert line and column to 0-based numbers.
lineNumber = isNaN(lineNumber) ? undefined : lineNumber - 1;
}
if (typeof(lineColumnMatch[2]) === 'string') {
columnNumber = parseInt(lineColumnMatch[2], 10);
columnNumber = isNaN(columnNumber) ? undefined : columnNumber - 1;
}
return {
url: beforePath + pathAndAfter.substring(0, pathAndAfter.length - lineColumnMatch[0].length),
lineNumber: lineNumber,
columnNumber: columnNumber
};
}
/**
* @param {string} url
* @return {string}
*/
static removeWasmFunctionInfoFromURL(url) {
const wasmFunctionRegEx = /:wasm-function\[\d+\]/;
const wasmFunctionIndex = url.search(wasmFunctionRegEx);
if (wasmFunctionIndex === -1) {
return url;
}
return url.substring(0, wasmFunctionIndex);
}
/**
* @param {string} url
* @return {boolean}
*/
static isRelativeURL(url) {
return !(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(url));
}
get displayName() {
if (this._displayName) {
return this._displayName;
}
if (this.isDataURL()) {
return this.dataURLDisplayName();
}
if (this.isBlobURL()) {
return this.url;
}
if (this.isAboutBlank()) {
return this.url;
}
this._displayName = this.lastPathComponent;
if (!this._displayName) {
this._displayName = (this.host || '') + '/';
}
if (this._displayName === '/') {
this._displayName = this.url;
}
return this._displayName;
}
/**
* @return {string}
*/
dataURLDisplayName() {
if (this._dataURLDisplayName) {
return this._dataURLDisplayName;
}
if (!this.isDataURL()) {
return '';
}
this._dataURLDisplayName = this.url.trimEndWithMaxLength(20);
return this._dataURLDisplayName;
}
/**
* @return {boolean}
*/
isAboutBlank() {
return this.url === 'about:blank';
}
/**
* @return {boolean}
*/
isDataURL() {
return this.scheme === 'data';
}
/**
* @return {boolean}
*/
isBlobURL() {
return this.url.startsWith('blob:');
}
/**
* @return {string}
*/
lastPathComponentWithFragment() {
return this.lastPathComponent + (this.fragment ? '#' + this.fragment : '');
}
/**
* @return {string}
*/
domain() {
if (this.isDataURL()) {
return 'data:';
}
return this.host + (this.port ? ':' + this.port : '');
}
/**
* @return {string}
*/
securityOrigin() {
if (this.isDataURL()) {
return 'data:';
}
const scheme = this.isBlobURL() ? this._blobInnerScheme : this.scheme;
return scheme + '://' + this.domain();
}
/**
* @return {string}
*/
urlWithoutScheme() {
if (this.scheme && this.url.startsWith(this.scheme + '://')) {
return this.url.substring(this.scheme.length + 3);
}
return this.url;
}
}
/** @type {?RegExp} */
ParsedURL._urlRegexInstance = null;
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
class ResourceType {
/**
* @param {string} name
* @param {string} title
* @param {!ResourceCategory} category
* @param {boolean} isTextType
*/
constructor(name, title, category, isTextType) {
this._name = name;
this._title = title;
this._category = category;
this._isTextType = isTextType;
}
/**
* @param {?string} mimeType
* @return {!ResourceType}
*/
static fromMimeType(mimeType) {
if (!mimeType) {
return resourceTypes.Other;
}
if (mimeType.startsWith('text/html')) {
return resourceTypes.Document;
}
if (mimeType.startsWith('text/css')) {
return resourceTypes.Stylesheet;
}
if (mimeType.startsWith('image/')) {
return resourceTypes.Image;
}
if (mimeType.startsWith('text/')) {
return resourceTypes.Script;
}
if (mimeType.includes('font')) {
return resourceTypes.Font;
}
if (mimeType.includes('script')) {
return resourceTypes.Script;
}
if (mimeType.includes('octet')) {
return resourceTypes.Other;
}
if (mimeType.includes('application')) {
return resourceTypes.Script;
}
return resourceTypes.Other;
}
/**
* @param {string} url
* @return {?ResourceType}
*/
static fromURL(url) {
return _resourceTypeByExtension.get(ParsedURL.extractExtension(url)) || null;
}
/**
* @param {string} name
* @return {?ResourceType}
*/
static fromName(name) {
for (const resourceTypeId in resourceTypes) {
const resourceType = /** @type {!Object<string, !ResourceType>} */(resourceTypes)[resourceTypeId];
if (resourceType.name() === name) {
return resourceType;
}
}
return null;
}
/**
* @param {string} url
* @return {string|undefined}
*/
static mimeFromURL(url) {
const name = ParsedURL.extractName(url);
if (_mimeTypeByName.has(name)) {
return _mimeTypeByName.get(name);
}
const ext = ParsedURL.extractExtension(url).toLowerCase();
return _mimeTypeByExtension.get(ext);
}
/**
* @param {string} ext
* @return {string|undefined}
*/
static mimeFromExtension(ext) {
return _mimeTypeByExtension.get(ext);
}
/**
* @return {string}
*/
name() {
return this._name;
}
/**
* @return {string}
*/
title() {
return this._title;
}
/**
* @return {!ResourceCategory}
*/
category() {
return this._category;
}
/**
* @return {boolean}
*/
isTextType() {
return this._isTextType;
}
/**
* @return {boolean}
*/
isScript() {
return this._name === 'script' || this._name === 'sm-script';
}
/**
* @return {boolean}
*/
hasScripts() {
return this.isScript() || this.isDocument();
}
/**
* @return {boolean}
*/
isStyleSheet() {
return this._name === 'stylesheet' || this._name === 'sm-stylesheet';
}
/**
* @return {boolean}
*/
isDocument() {
return this._name === 'document';
}
/**
* @return {boolean}
*/
isDocumentOrScriptOrStyleSheet() {
return this.isDocument() || this.isScript() || this.isStyleSheet();
}
/**
* @return {boolean}
*/
isFromSourceMap() {
return this._name.startsWith('sm-');
}
/**
* @override
* @return {string}
*/
toString() {
return this._name;
}
/**
* @return {string}
*/
canonicalMimeType() {
if (this.isDocument()) {
return 'text/html';
}
if (this.isScript()) {
return 'text/javascript';
}
if (this.isStyleSheet()) {
return 'text/css';
}
return '';
}
}
/**
* @unrestricted
*/
class ResourceCategory {
/**
* @param {string} title
* @param {string} shortTitle
*/
constructor(title, shortTitle) {
this.title = title;
this.shortTitle = shortTitle;
}
}
/**
* @enum {!ResourceCategory}
*/
const resourceCategories = {
XHR: new ResourceCategory(ls`XHR and Fetch`, ls`XHR`),
Script: new ResourceCategory(ls`Scripts`, ls`JS`),
Stylesheet: new ResourceCategory(ls`Stylesheets`, ls`CSS`),
Image: new ResourceCategory(ls`Images`, ls`Img`),
Media: new ResourceCategory(ls`Media`, ls`Media`),
Font: new ResourceCategory(ls`Fonts`, ls`Font`),
Document: new ResourceCategory(ls`Documents`, ls`Doc`),
WebSocket: new ResourceCategory(ls`WebSockets`, ls`WS`),
Manifest: new ResourceCategory(ls`Manifest`, ls`Manifest`),
Other: new ResourceCategory(ls`Other`, ls`Other`),
};
/**
* Keep these in sync with WebCore::InspectorPageAgent::resourceTypeJson
* @enum {!ResourceType}
*/
const resourceTypes = {
Document: new ResourceType('document', ls`Document`, resourceCategories.Document, true),
Stylesheet: new ResourceType('stylesheet', ls`Stylesheet`, resourceCategories.Stylesheet, true),
Image: new ResourceType('image', ls`Image`, resourceCategories.Image, false),
Media: new ResourceType('media', ls`Media`, resourceCategories.Media, false),
Font: new ResourceType('font', ls`Font`, resourceCategories.Font, false),
Script: new ResourceType('script', ls`Script`, resourceCategories.Script, true),
TextTrack: new ResourceType('texttrack', ls`TextTrack`, resourceCategories.Other, true),
XHR: new ResourceType('xhr', ls`XHR`, resourceCategories.XHR, true),
Fetch: new ResourceType('fetch', ls`Fetch`, resourceCategories.XHR, true),
EventSource: new ResourceType('eventsource', ls`EventSource`, resourceCategories.XHR, true),
WebSocket: new ResourceType('websocket', ls`WebSocket`, resourceCategories.WebSocket, false),
Manifest: new ResourceType('manifest', ls`Manifest`, resourceCategories.Manifest, true),
SignedExchange: new ResourceType('signed-exchange', ls`SignedExchange`, resourceCategories.Other, false),
Ping: new ResourceType('ping', ls`Ping`, resourceCategories.Other, false),
CSPViolationReport: new ResourceType('csp-violation-report', ls`CSPViolationReport`, resourceCategories.Other, false),
Other: new ResourceType('other', ls`Other`, resourceCategories.Other, false),
SourceMapScript: new ResourceType('sm-script', ls`Script`, resourceCategories.Script, true),
SourceMapStyleSheet: new ResourceType('sm-stylesheet', ls`Stylesheet`, resourceCategories.Stylesheet, true),
};
const _mimeTypeByName = new Map([
// CoffeeScript
['Cakefile', 'text/x-coffeescript']
]);
const _resourceTypeByExtension = new Map([
['js', resourceTypes.Script], ['mjs', resourceTypes.Script],
['css', resourceTypes.Stylesheet], ['xsl', resourceTypes.Stylesheet],
['jpeg', resourceTypes.Image], ['jpg', resourceTypes.Image], ['svg', resourceTypes.Image],
['gif', resourceTypes.Image], ['png', resourceTypes.Image], ['ico', resourceTypes.Image],
['tiff', resourceTypes.Image], ['tif', resourceTypes.Image], ['bmp', resourceTypes.Image],
['webp', resourceTypes.Media],
['ttf', resourceTypes.Font], ['otf', resourceTypes.Font], ['ttc', resourceTypes.Font], ['woff', resourceTypes.Font]
]);
const _mimeTypeByExtension = new Map([
// Web extensions
['js', 'text/javascript'], ['mjs', 'text/javascript'], ['css', 'text/css'], ['html', 'text/html'],
['htm', 'text/html'], ['xml', 'application/xml'], ['xsl', 'application/xml'],
// HTML Embedded Scripts, ASP], JSP
['asp', 'application/x-aspx'], ['aspx', 'application/x-aspx'], ['jsp', 'application/x-jsp'],
// C/C++
['c', 'text/x-c++src'], ['cc', 'text/x-c++src'], ['cpp', 'text/x-c++src'], ['h', 'text/x-c++src'],
['m', 'text/x-c++src'], ['mm', 'text/x-c++src'],
// CoffeeScript
['coffee', 'text/x-coffeescript'],
// Dart
['dart', 'text/javascript'],
// TypeScript
['ts', 'text/typescript'], ['tsx', 'text/typescript-jsx'],
// JSON
['json', 'application/json'], ['gyp', 'application/json'], ['gypi', 'application/json'],
// C#
['cs', 'text/x-csharp'],
// Java
['java', 'text/x-java'],
// Less
['less', 'text/x-less'],
// PHP
['php', 'text/x-php'], ['phtml', 'application/x-httpd-php'],
// Python
['py', 'text/x-python'],
// Shell
['sh', 'text/x-sh'],
// SCSS
['scss', 'text/x-scss'],
// Video Text Tracks.
['vtt', 'text/vtt'],
// LiveScript
['ls', 'text/x-livescript'],
// Markdown
['md', 'text/markdown'],
// ClojureScript
['cljs', 'text/x-clojure'], ['cljc', 'text/x-clojure'], ['cljx', 'text/x-clojure'],
// Stylus
['styl', 'text/x-styl'],
// JSX
['jsx', 'text/jsx'],
// Image
['jpeg', 'image/jpeg'], ['jpg', 'image/jpeg'], ['svg', 'image/svg+xml'], ['gif', 'image/gif'], ['webp', 'image/webp'],
['png', 'image/png'], ['ico', 'image/ico'], ['tiff', 'image/tiff'], ['tif', 'image/tif'], ['bmp', 'image/bmp'],
// Font
['ttf', 'font/opentype'], ['otf', 'font/opentype'], ['ttc', 'font/opentype'], ['woff', 'application/font-woff']
]);
// Copyright 2020 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.
const REMOTE_MODULE_FALLBACK_REVISION = '@9c7912d3335c02d62f63be2749d84b2d0b788982';
const instanceSymbol = Symbol('instance');
const originalConsole = console;
const originalAssert = console.assert;
/** @type {!URLSearchParams} */
const queryParamsObject = new URLSearchParams(location.search);
// The following two variables are initialized all the way at the bottom of this file
/** @type {?string} */
let remoteBase;
/** @type {string} */
let importScriptPathPrefix;
let runtimePlatform = '';
/** @type {function(string):string} */
let l10nCallback;
/** @type {!Runtime} */
let runtimeInstance;
/**
* @unrestricted
*/
class Runtime {
/**
* @private
* @param {!Array.<!ModuleDescriptor>} descriptors
*/
constructor(descriptors) {
/** @type {!Array<!Module>} */
this._modules = [];
/** @type {!Object<string, !Module>} */
this._modulesMap = {};
/** @type {!Array<!Extension>} */
this._extensions = [];
/** @type {!Object<string, function(new:Object):void>} */
this._cachedTypeClasses = {};
/** @type {!Object<string, !ModuleDescriptor>} */
this._descriptorsMap = {};
for (let i = 0; i < descriptors.length; ++i) {
this._registerModule(descriptors[i]);
}
}
/**
* @param {{forceNew: ?boolean, moduleDescriptors: ?Array.<!ModuleDescriptor>}=} opts
* @return {!Runtime}
*/
static instance(opts = {forceNew: null, moduleDescriptors: null}) {
const {forceNew, moduleDescriptors} = opts;
if (!moduleDescriptors || forceNew) {
if (!moduleDescriptors) {
throw new Error(
`Unable to create settings: targetManager and workspace must be provided: ${new Error().stack}`);
}
runtimeInstance = new Runtime(moduleDescriptors);
}
return runtimeInstance;
}
/**
* @param {string} url
* @return {!Promise.<!ArrayBuffer>}
*/
loadBinaryResourcePromise(url) {
return internalLoadResourcePromise(url, true);
}
/**
* http://tools.ietf.org/html/rfc3986#section-5.2.4
* @param {string} path
* @return {string}
*/
static normalizePath(path) {
if (path.indexOf('..') === -1 && path.indexOf('.') === -1) {
return path;
}
const normalizedSegments = [];
const segments = path.split('/');
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (segment === '.') {
continue;
} else if (segment === '..') {
normalizedSegments.pop();
} else if (segment) {
normalizedSegments.push(segment);
}
}
let normalizedPath = normalizedSegments.join('/');
if (normalizedPath[normalizedPath.length - 1] === '/') {
return normalizedPath;
}
if (path[0] === '/' && normalizedPath) {
normalizedPath = '/' + normalizedPath;
}
if ((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') ||
(segments[segments.length - 1] === '..')) {
normalizedPath = normalizedPath + '/';
}
return normalizedPath;
}
/**
* @param {string} name
* @return {?string}
*/
static queryParam(name) {
return queryParamsObject.get(name);
}
/**
* @return {!Object<string,boolean>}
*/
static _experimentsSetting() {
try {
return /** @type {!Object<string,boolean>} */ (
JSON.parse(self.localStorage && self.localStorage['experiments'] ? self.localStorage['experiments'] : '{}'));
} catch (e) {
console.error('Failed to parse localStorage[\'experiments\']');
return {};
}
}
/**
* @param {*} value
* @param {string} message
*/
static _assert(value, message) {
if (value) {
return;
}
originalAssert.call(originalConsole, value, message + ' ' + new Error().stack);
}
/**
* @param {string} platform
*/
static setPlatform(platform) {
runtimePlatform = platform;
}
/**
* @param {!ModuleDescriptor|!RuntimeExtensionDescriptor} descriptor
* @return {boolean}
*/
static _isDescriptorEnabled(descriptor) {
const activatorExperiment = descriptor['experiment'];
if (activatorExperiment === '*') {
return true;
}
if (activatorExperiment && activatorExperiment.startsWith('!') &&
experiments.isEnabled(activatorExperiment.substring(1))) {
return false;
}
if (activatorExperiment && !activatorExperiment.startsWith('!') && !experiments.isEnabled(activatorExperiment)) {
return false;
}
const condition = descriptor['condition'];
if (condition && !condition.startsWith('!') && !Runtime.queryParam(condition)) {
return false;
}
if (condition && condition.startsWith('!') && Runtime.queryParam(condition.substring(1))) {
return false;
}
return true;
}
/**
* @param {string} path
* @return {string}
*/
static resolveSourceURL(path) {
let sourceURL = self.location.href;
if (self.location.search) {
sourceURL = sourceURL.replace(self.location.search, '');
}
sourceURL = sourceURL.substring(0, sourceURL.lastIndexOf('/') + 1) + path;
return '\n/*# sourceURL=' + sourceURL + ' */';
}
/**
* @param {function(string):string} localizationFunction
*/
static setL10nCallback(localizationFunction) {
l10nCallback = localizationFunction;
}
useTestBase() {
remoteBase = 'http://localhost:8000/inspector-sources/';
if (Runtime.queryParam('debugFrontend')) {
remoteBase += 'debug/';
}
}
/**
* @param {string} moduleName
* @return {!Module}
*/
module(moduleName) {
return this._modulesMap[moduleName];
}
/**
* @param {!ModuleDescriptor} descriptor
*/
_registerModule(descriptor) {
const module = new Module(this, descriptor);
this._modules.push(module);
this._modulesMap[descriptor['name']] = module;
}
/**
* @param {string} moduleName
* @return {!Promise.<boolean>}
*/
loadModulePromise(moduleName) {
return this._modulesMap[moduleName]._loadPromise();
}
/**
* @param {!Array.<string>} moduleNames
* @return {!Promise.<!Array.<*>>}
*/
loadAutoStartModules(moduleNames) {
const promises = [];
for (let i = 0; i < moduleNames.length; ++i) {
promises.push(this.loadModulePromise(moduleNames[i]));
}
return Promise.all(promises);
}
/**
* @param {!Extension} extension
* @param {?function(function(new:Object)):boolean} predicate
* @return {boolean}
*/
_checkExtensionApplicability(extension, predicate) {
if (!predicate) {
return false;
}
const contextTypes = extension.descriptor().contextTypes;
if (!contextTypes) {
return true;
}
for (let i = 0; i < contextTypes.length; ++i) {
const contextType = this._resolve(contextTypes[i]);
const isMatching = !!contextType && predicate(contextType);
if (isMatching) {
return true;
}
}
return false;
}
/**
* @param {!Extension} extension
* @param {?Object} context
* @return {boolean}
*/
isExtensionApplicableToContext(extension, context) {
if (!context) {
return true;
}
return this._checkExtensionApplicability(extension, isInstanceOf);
/**
* @param {!Function} targetType
* @return {boolean}
*/
function isInstanceOf(targetType) {
return context instanceof targetType;
}
}
/**
* @param {!Extension} extension
* @param {!Set.<function(new:Object, ...?):void>} currentContextTypes
* @return {boolean}
*/
isExtensionApplicableToContextTypes(extension, currentContextTypes) {
if (!extension.descriptor().contextTypes) {
return true;
}
let callback = null;
if (currentContextTypes) {
/**
* @param {function(new:Object, ...?):void} targetType
* @return {boolean}
*/
callback = targetType => {
return currentContextTypes.has(targetType);
};
}
return this._checkExtensionApplicability(extension, callback);
}
/**
* @param {*} type
* @param {?Object=} context
* @param {boolean=} sortByTitle
* @return {!Array.<!Extension>}
*/
extensions(type, context, sortByTitle) {
return this._extensions.filter(filter).sort(sortByTitle ? titleComparator : orderComparator);
/**
* @param {!Extension} extension
* @return {boolean}
*/
function filter(extension) {
if (extension._type !== type && extension._typeClass() !== type) {
return false;
}
if (!extension.enabled()) {
return false;
}
return !context || extension.isApplicable(context);
}
/**
* @param {!Extension} extension1
* @param {!Extension} extension2
* @return {number}
*/
function orderComparator(extension1, extension2) {
const order1 = extension1.descriptor()['order'] || 0;
const order2 = extension2.descriptor()['order'] || 0;
return order1 - order2;
}
/**
* @param {!Extension} extension1
* @param {!Extension} extension2
* @return {number}
*/
function titleComparator(extension1, extension2) {
const title1 = extension1.title() || '';
const title2 = extension2.title() || '';
return title1.localeCompare(title2);
}
}
/**
* @param {*} type
* @param {?Object=} context
* @return {?Extension}
*/
extension(type, context) {
return this.extensions(type, context)[0] || null;
}
/**
* @param {*} type
* @param {?Object=} context
* @return {!Promise.<!Array.<!Object>>}
*/
allInstances(type, context) {
return Promise.all(this.extensions(type, context).map(extension => extension.instance()));
}
/**
* @param {string} typeName
* @return {?function(new:Object)}
*/
_resolve(typeName) {
if (!this._cachedTypeClasses[typeName]) {
/** @type {!Array<string>} */
const path = typeName.split('.');
/** @type {*} */
let object = self;
for (let i = 0; object && (i < path.length); ++i) {
object = object[path[i]];
}
if (object) {
this._cachedTypeClasses[typeName] = /** @type {function(new:Object):void} */ (object);
}
}
return this._cachedTypeClasses[typeName] || null;
}
/**
* @param {function(new:T)} constructorFunction
* @return {!T}
* @template T
*/
sharedInstance(constructorFunction) {
if (instanceSymbol in constructorFunction &&
Object.getOwnPropertySymbols(constructorFunction).includes(instanceSymbol)) {
// @ts-ignore Usage of symbols
return constructorFunction[instanceSymbol];
}
const instance = new constructorFunction();
// @ts-ignore Usage of symbols
constructorFunction[instanceSymbol] = instance;
return instance;
}
}
// Module namespaces.
// NOTE: Update scripts/build/special_case_namespaces.json if you add a special cased namespace.
/** @type {!Object<string,string>} */
const specialCases = {
'sdk': 'SDK',
'js_sdk': 'JSSDK',
'browser_sdk': 'BrowserSDK',
'ui': 'UI',
'object_ui': 'ObjectUI',
'javascript_metadata': 'JavaScriptMetadata',
'perf_ui': 'PerfUI',
'har_importer': 'HARImporter',
'sdk_test_runner': 'SDKTestRunner',
'cpu_profiler_test_runner': 'CPUProfilerTestRunner'
};
/**
* @unrestricted
*/
class Module {
/**
* @param {!Runtime} manager
* @param {!ModuleDescriptor} descriptor
*/
constructor(manager, descriptor) {
this._manager = manager;
this._descriptor = descriptor;
this._name = descriptor.name;
/** @type {!Array<!Extension>} */
this._extensions = [];
/** @type {!Map<string, !Array<!Extension>>} */
this._extensionsByClassName = new Map();
const extensions = /** @type {?Array.<!RuntimeExtensionDescriptor>} */ (descriptor.extensions);
for (let i = 0; extensions && i < extensions.length; ++i) {
const extension = new Extension(this, extensions[i]);
this._manager._extensions.push(extension);
this._extensions.push(extension);
}
this._loadedForTest = false;
}
/**
* @return {string}
*/
name() {
return this._name;
}
/**
* @return {boolean}
*/
enabled() {
return Runtime._isDescriptorEnabled(this._descriptor);
}
/**
* @param {string} name
* @return {string}
*/
resource(name) {
const fullName = this._name + '/' + name;
const content = self.Runtime.cachedResources[fullName];
if (!content) {
throw new Error(fullName + ' not preloaded. Check module.json');
}
return content;
}
/**
* @return {!Promise.<boolean>}
*/
_loadPromise() {
if (!this.enabled()) {
return Promise.reject(new Error('Module ' + this._name + ' is not enabled'));
}
if (this._pendingLoadPromise) {
return this._pendingLoadPromise;
}
const dependencies = this._descriptor.dependencies;
const dependencyPromises = [];
for (let i = 0; dependencies && i < dependencies.length; ++i) {
dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise());
}
this._pendingLoadPromise = Promise.all(dependencyPromises)
.then(this._loadResources.bind(this))
.then(this._loadModules.bind(this))
.then(this._loadScripts.bind(this))
.then(() => {
this._loadedForTest = true;
return this._loadedForTest;
});
return this._pendingLoadPromise;
}
/**
* @return {!Promise.<void>}
* @this {Module}
*/
_loadResources() {
const resources = this._descriptor['resources'];
if (!resources || !resources.length) {
return Promise.resolve();
}
const promises = [];
for (const resource of resources) {
const url = this._modularizeURL(resource);
const shouldAppendSourceURL = !(url.endsWith('.html') || url.endsWith('.md'));
promises.push(loadResourceIntoCache(url, shouldAppendSourceURL));
}
return Promise.all(promises).then(undefined);
}
_loadModules() {
if (!this._descriptor.modules || !this._descriptor.modules.length) {
return Promise.resolve();
}
const namespace = this._computeNamespace();
// @ts-ignore Legacy global namespace instantation
self[namespace] = self[namespace] || {};
const legacyFileName = `${this._name}-legacy.js`;
const fileName = this._descriptor.modules.includes(legacyFileName) ? legacyFileName : `${this._name}.js`;
// TODO(crbug.com/1011811): Remove eval when we use TypeScript which does support dynamic imports
return eval(`import('../${this._name}/${fileName}')`);
}
/**
* @return {!Promise.<void>}
*/
_loadScripts() {
if (!this._descriptor.scripts || !this._descriptor.scripts.length) {
return Promise.resolve();
}
const namespace = this._computeNamespace();
// @ts-ignore Legacy global namespace instantation
self[namespace] = self[namespace] || {};
return loadScriptsPromise(this._descriptor.scripts.map(this._modularizeURL, this), this._remoteBase());
}
/**
* @return {string}
*/
_computeNamespace() {
return specialCases[this._name] ||
this._name.split('_').map(a => a.substring(0, 1).toUpperCase() + a.substring(1)).join('');
}
/**
* @param {string} resourceName
*/
_modularizeURL(resourceName) {
return Runtime.normalizePath(this._name + '/' + resourceName);
}
/**
* @return {string|undefined}
*/
_remoteBase() {
return !Runtime.queryParam('debugFrontend') && this._descriptor.remote && remoteBase || undefined;
}
/**
* @param {string} resourceName
* @return {!Promise.<string>}
*/
fetchResource(resourceName) {
const base = this._remoteBase();
const sourceURL = getResourceURL(this._modularizeURL(resourceName), base);
return base ? loadResourcePromiseWithFallback(sourceURL) : loadResourcePromise(sourceURL);
}
/**
* @param {string} value
* @return {string}
*/
substituteURL(value) {
const base = this._remoteBase() || '';
return value.replace(/@url\(([^\)]*?)\)/g, convertURL.bind(this));
/**
* @param {string} match
* @param {string} url
* @this {Module}
*/
function convertURL(match, url) {
return base + this._modularizeURL(url);
}
}
}
/**
* @unrestricted
*/
class Extension {
/**
* @param {!Module} moduleParam
* @param {!RuntimeExtensionDescriptor} descriptor
*/
constructor(moduleParam, descriptor) {
this._module = moduleParam;
this._descriptor = descriptor;
this._type = descriptor.type;
this._hasTypeClass = this._type.charAt(0) === '@';
/**
* @type {?string}
*/
this._className = descriptor.className || null;
this._factoryName = descriptor.factoryName || null;
}
/**
* @return {!RuntimeExtensionDescriptor}
*/
descriptor() {
return this._descriptor;
}
/**
* @return {!Module}
*/
module() {
return this._module;
}
/**
* @return {boolean}
*/
enabled() {
return this._module.enabled() && Runtime._isDescriptorEnabled(this.descriptor());
}
/**
* @return {?function(new:Object)}
*/
_typeClass() {
if (!this._hasTypeClass) {
return null;
}
return this._module._manager._resolve(this._type.substring(1));
}
/**
* @param {?Object} context
* @return {boolean}
*/
isApplicable(context) {
return this._module._manager.isExtensionApplicableToContext(this, context);
}
/**
* @return {!Promise.<!Object>}
*/
instance() {
return this._module._loadPromise().then(this._createInstance.bind(this));
}
/**
* @return {boolean}
*/
canInstantiate() {
return !!(this._className || this._factoryName);
}
/**
* @return {!Object}
*/
_createInstance() {
const className = this._className || this._factoryName;
if (!className) {
throw new Error('Could not instantiate extension with no class');
}
const constructorFunction = self.eval(/** @type {string} */ (className));
if (!(constructorFunction instanceof Function)) {
throw new Error('Could not instantiate: ' + className);
}
if (this._className) {
return this._module._manager.sharedInstance(constructorFunction);
}
return new constructorFunction(this);
}
/**
* @return {string}
*/
title() {
// @ts-ignore Magic lookup for objects
const title = this._descriptor['title-' + runtimePlatform] || this._descriptor['title'];
if (title && l10nCallback) {
return l10nCallback(title);
}
return title;
}
/**
* @param {function(new:Object, ...?):void} contextType
* @return {boolean}
*/
hasContextType(contextType) {
const contextTypes = this.descriptor().contextTypes;
if (!contextTypes) {
return false;
}
for (let i = 0; i < contextTypes.length; ++i) {
if (contextType === this._module._manager._resolve(contextTypes[i])) {
return true;
}
}
return false;
}
}
/**
* @unrestricted
*/
class ExperimentsSupport {
constructor() {
/** @type {!Array<!Experiment>} */
this._experiments = [];
/** @type {!Object<string,boolean>} */
this._experimentNames = {};
/** @type {!Object<string,boolean>} */
this._enabledTransiently = {};
/** @type {!Set<string>} */
this._serverEnabled = new Set();
}
/**
* @return {!Array.<!Experiment>}
*/
allConfigurableExperiments() {
const result = [];
for (let i = 0; i < this._experiments.length; i++) {
const experiment = this._experiments[i];
if (!this._enabledTransiently[experiment.name]) {
result.push(experiment);
}
}
return result;
}
/**
* @return {!Array.<!Experiment>}
*/
enabledExperiments() {
return this._experiments.filter(experiment => experiment.isEnabled());
}
/**
* @param {!Object} value
*/
_setExperimentsSetting(value) {
if (!self.localStorage) {
return;
}
self.localStorage['experiments'] = JSON.stringify(value);
}
/**
* @param {string} experimentName
* @param {string} experimentTitle
* @param {boolean=} unstable
*/
register(experimentName, experimentTitle, unstable) {
Runtime._assert(!this._experimentNames[experimentName], 'Duplicate registration of experiment ' + experimentName);
this._experimentNames[experimentName] = true;
this._experiments.push(new Experiment(this, experimentName, experimentTitle, !!unstable));
}
/**
* @param {string} experimentName
* @return {boolean}
*/
isEnabled(experimentName) {
this._checkExperiment(experimentName);
// Check for explicitly disabled experiments first - the code could call setEnable(false) on the experiment enabled
// by default and we should respect that.
if (Runtime._experimentsSetting()[experimentName] === false) {
return false;
}
if (this._enabledTransiently[experimentName]) {
return true;
}
if (this._serverEnabled.has(experimentName)) {
return true;
}
return !!Runtime._experimentsSetting()[experimentName];
}
/**
* @param {string} experimentName
* @param {boolean} enabled
*/
setEnabled(experimentName, enabled) {
this._checkExperiment(experimentName);
const experimentsSetting = Runtime._experimentsSetting();
experimentsSetting[experimentName] = enabled;
this._setExperimentsSetting(experimentsSetting);
}
/**
* @param {!Array.<string>} experimentNames
*/
setDefaultExperiments(experimentNames) {
for (let i = 0; i < experimentNames.length; ++i) {
this._checkExperiment(experimentNames[i]);
this._enabledTransiently[experimentNames[i]] = true;
}
}
/**
* @param {!Array.<string>} experimentNames
*/
setServerEnabledExperiments(experimentNames) {
for (const experiment of experimentNames) {
this._checkExperiment(experiment);
this._serverEnabled.add(experiment);
}
}
/**
* @param {string} experimentName
*/
enableForTest(experimentName) {
this._checkExperiment(experimentName);
this._enabledTransiently[experimentName] = true;
}
clearForTest() {
this._experiments = [];
this._experimentNames = {};
this._enabledTransiently = {};
this._serverEnabled.clear();
}
cleanUpStaleExperiments() {
const experimentsSetting = Runtime._experimentsSetting();
/** @type {!Object<string,boolean>} */
const cleanedUpExperimentSetting = {};
for (let i = 0; i < this._experiments.length; ++i) {
const experimentName = this._experiments[i].name;
if (experimentsSetting[experimentName]) {
cleanedUpExperimentSetting[experimentName] = true;
}
}
this._setExperimentsSetting(cleanedUpExperimentSetting);
}
/**
* @param {string} experimentName
*/
_checkExperiment(experimentName) {
Runtime._assert(this._experimentNames[experimentName], 'Unknown experiment ' + experimentName);
}
}
/**
* @unrestricted
*/
class Experiment {
/**
* @param {!ExperimentsSupport} experiments
* @param {string} name
* @param {string} title
* @param {boolean} unstable
*/
constructor(experiments, name, title, unstable) {
this.name = name;
this.title = title;
this.unstable = unstable;
this._experiments = experiments;
}
/**
* @return {boolean}
*/
isEnabled() {
return this._experiments.isEnabled(this.name);
}
/**
* @param {boolean} enabled
*/
setEnabled(enabled) {
this._experiments.setEnabled(this.name, enabled);
}
}
/**
* @private
* @param {string} url
* @param {boolean} asBinary
* @template T
* @return {!Promise.<T>}
*/
function internalLoadResourcePromise(url, asBinary) {
return new Promise(load);
/**
* @param {function(?):void} fulfill
* @param {function(*):void} reject
*/
function load(fulfill, reject) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
if (asBinary) {
xhr.responseType = 'arraybuffer';
}
xhr.onreadystatechange = onreadystatechange;
/**
* @param {!Event} e
*/
function onreadystatechange(e) {
if (xhr.readyState !== XMLHttpRequest.DONE) {
return;
}
const {response} = /** @type {*} */ (e.target);
const text = asBinary ? new TextDecoder().decode(response) : response;
// DevTools Proxy server can mask 404s as 200s, check the body to be sure
const status = /^HTTP\/1.1 404/.test(text) ? 404 : xhr.status;
if ([0, 200, 304].indexOf(status) === -1) // Testing harness file:/// results in 0.
{
reject(new Error('While loading from url ' + url + ' server responded with a status of ' + status));
} else {
fulfill(response);
}
}
xhr.send(null);
}
}
/**
* @type {!Object<string,boolean>}
*/
const loadedScripts = {};
/**
* @param {!Array.<string>} scriptNames
* @param {string=} base
* @return {!Promise.<void>}
*/
function loadScriptsPromise(scriptNames, base) {
/** @type {!Array<!Promise<void>>} */
const promises = [];
/** @type {!Array<string>} */
const urls = [];
const sources = new Array(scriptNames.length);
let scriptToEval = 0;
for (let i = 0; i < scriptNames.length; ++i) {
const scriptName = scriptNames[i];
const sourceURL = getResourceURL(scriptName, base);
if (loadedScripts[sourceURL]) {
continue;
}
urls.push(sourceURL);
const promise = base ? loadResourcePromiseWithFallback(sourceURL) : loadResourcePromise(sourceURL);
promises.push(promise.then(scriptSourceLoaded.bind(null, i), scriptSourceLoaded.bind(null, i, undefined)));
}
return Promise.all(promises).then(undefined);
/**
* @param {number} scriptNumber
* @param {string=} scriptSource
*/
function scriptSourceLoaded(scriptNumber, scriptSource) {
sources[scriptNumber] = scriptSource || '';
// Eval scripts as fast as possible.
while (typeof sources[scriptToEval] !== 'undefined') {
evaluateScript(urls[scriptToEval], sources[scriptToEval]);
++scriptToEval;
}
}
/**
* @param {string} sourceURL
* @param {string=} scriptSource
*/
function evaluateScript(sourceURL, scriptSource) {
loadedScripts[sourceURL] = true;
if (!scriptSource) {
// Do not reject, as this is normal in the hosted mode.
console.error('Empty response arrived for script \'' + sourceURL + '\'');
return;
}
self.eval(scriptSource + '\n//# sourceURL=' + sourceURL);
}
}
/**
* @param {string} url
* @return {!Promise.<string>}
*/
function loadResourcePromiseWithFallback(url) {
return loadResourcePromise(url).catch(err => {
const urlWithFallbackVersion = url.replace(/@[0-9a-f]{40}/, REMOTE_MODULE_FALLBACK_REVISION);
// TODO(phulce): mark fallbacks in module.json and modify build script instead
if (urlWithFallbackVersion === url || !url.includes('lighthouse_worker_module')) {
throw err;
}
return loadResourcePromise(urlWithFallbackVersion);
});
}
/**
* @param {string} url
* @param {boolean} appendSourceURL
* @return {!Promise<void>}
*/
function loadResourceIntoCache(url, appendSourceURL) {
return loadResourcePromise(url).then(cacheResource.bind(null, url), cacheResource.bind(null, url, undefined));
/**
* @param {string} path
* @param {string=} content
*/
function cacheResource(path, content) {
if (!content) {
console.error('Failed to load resource: ' + path);
return;
}
const sourceURL = appendSourceURL ? Runtime.resolveSourceURL(path) : '';
self.Runtime.cachedResources[path] = content + sourceURL;
}
}
/**
* @param {string} url
* @return {!Promise.<string>}
*/
function loadResourcePromise(url) {
return internalLoadResourcePromise(url, false);
}
/**
* @param {string} scriptName
* @param {string=} base
* @return {string}
*/
function getResourceURL(scriptName, base) {
const sourceURL = (base || importScriptPathPrefix) + scriptName;
const schemaIndex = sourceURL.indexOf('://') + 3;
let pathIndex = sourceURL.indexOf('/', schemaIndex);
if (pathIndex === -1) {
pathIndex = sourceURL.length;
}
return sourceURL.substring(0, pathIndex) + Runtime.normalizePath(sourceURL.substring(pathIndex));
}
(function validateRemoteBase() {
if (location.href.startsWith('devtools://devtools/bundled/')) {
const queryParam = Runtime.queryParam('remoteBase');
if (queryParam) {
const versionMatch = /\/serve_file\/(@[0-9a-zA-Z]+)\/?$/.exec(queryParam);
if (versionMatch) {
remoteBase = `${location.origin}/remote/serve_file/${versionMatch[1]}/`;
}
}
}
})();
(function() {
const baseUrl = self.location ? self.location.origin + self.location.pathname : '';
importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
})();
// This must be constructed after the query parameters have been parsed.
const experiments = new ExperimentsSupport();
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
class BalancedJSONTokenizer {
/**
* @param {function(string):void} callback
* @param {boolean=} findMultiple
*/
constructor(callback, findMultiple) {
this._callback = callback;
/** @type {number} */
this._index = 0;
this._balance = 0;
/** @type {string} */
this._buffer = '';
this._findMultiple = findMultiple || false;
this._closingDoubleQuoteRegex = /[^\\](?:\\\\)*"/g;
}
/**
* @param {string} chunk
* @return {boolean}
*/
write(chunk) {
this._buffer += chunk;
const lastIndex = this._buffer.length;
const buffer = this._buffer;
let index;
for (index = this._index; index < lastIndex; ++index) {
const character = buffer[index];
if (character === '"') {
this._closingDoubleQuoteRegex.lastIndex = index;
if (!this._closingDoubleQuoteRegex.test(buffer)) {
break;
}
index = this._closingDoubleQuoteRegex.lastIndex - 1;
} else if (character === '{') {
++this._balance;
} else if (character === '}') {
--this._balance;
if (this._balance < 0) {
this._reportBalanced();
return false;
}
if (!this._balance) {
this._lastBalancedIndex = index + 1;
if (!this._findMultiple) {
break;
}
}
} else if (character === ']' && !this._balance) {
this._reportBalanced();
return false;
}
}
this._index = index;
this._reportBalanced();
return true;
}
_reportBalanced() {
if (!this._lastBalancedIndex) {
return;
}
this._callback(this._buffer.slice(0, this._lastBalancedIndex));
this._buffer = this._buffer.slice(this._lastBalancedIndex);
this._index -= this._lastBalancedIndex;
this._lastBalancedIndex = 0;
}
/**
* @return {string}
*/
remainder() {
return this._buffer;
}
}
/*
* Copyright (C) 2014 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// @ts-nocheck
// TODO(crbug.com/1011811): Enable TypeScript compiler checks
const HeapSnapshotProgressEvent = {
Update: 'ProgressUpdate',
BrokenSnapshot: 'BrokenSnapshot'
};
const baseSystemDistance = 100000000;
/**
* @unrestricted
*/
class AllocationNodeCallers {
/**
* @param {!Array.<!SerializedAllocationNode>} nodesWithSingleCaller
* @param {!Array.<!SerializedAllocationNode>} branchingCallers
*/
constructor(nodesWithSingleCaller, branchingCallers) {
/** @type {!Array.<!SerializedAllocationNode>} */
this.nodesWithSingleCaller = nodesWithSingleCaller;
/** @type {!Array.<!SerializedAllocationNode>} */
this.branchingCallers = branchingCallers;
}
}
/**
* @unrestricted
*/
class SerializedAllocationNode {
/**
* @param {number} nodeId
* @param {string} functionName
* @param {string} scriptName
* @param {number} scriptId
* @param {number} line
* @param {number} column
* @param {number} count
* @param {number} size
* @param {number} liveCount
* @param {number} liveSize
* @param {boolean} hasChildren
*/
constructor(nodeId, functionName, scriptName, scriptId, line, column, count, size, liveCount, liveSize, hasChildren) {
/** @type {number} */
this.id = nodeId;
/** @type {string} */
this.name = functionName;
/** @type {string} */
this.scriptName = scriptName;
/** @type {number} */
this.scriptId = scriptId;
/** @type {number} */
this.line = line;
/** @type {number} */
this.column = column;
/** @type {number} */
this.count = count;
/** @type {number} */
this.size = size;
/** @type {number} */
this.liveCount = liveCount;
/** @type {number} */
this.liveSize = liveSize;
/** @type {boolean} */
this.hasChildren = hasChildren;
}
}
/**
* @unrestricted
*/
class AllocationStackFrame {
/**
* @param {string} functionName
* @param {string} scriptName
* @param {number} scriptId
* @param {number} line
* @param {number} column
*/
constructor(functionName, scriptName, scriptId, line, column) {
/** @type {string} */
this.functionName = functionName;
/** @type {string} */
this.scriptName = scriptName;
/** @type {number} */
this.scriptId = scriptId;
/** @type {number} */
this.line = line;
/** @type {number} */
this.column = column;
}
}
/**
* @unrestricted
*/
class Node {
/**
* @param {number} id
* @param {string} name
* @param {number} distance
* @param {number} nodeIndex
* @param {number} retainedSize
* @param {number} selfSize
* @param {string} type
*/
constructor(id, name, distance, nodeIndex, retainedSize, selfSize, type) {
this.id = id;
this.name = name;
this.distance = distance;
this.nodeIndex = nodeIndex;
this.retainedSize = retainedSize;
this.selfSize = selfSize;
this.type = type;
this.canBeQueried = false;
this.detachedDOMTreeNode = false;
}
}
/**
* @unrestricted
*/
class Edge {
/**
* @param {string} name
* @param {!Node} node
* @param {string} type
* @param {number} edgeIndex
*/
constructor(name, node, type, edgeIndex) {
this.name = name;
this.node = node;
this.type = type;
this.edgeIndex = edgeIndex;
}
}
/**
* @unrestricted
*/
class AggregateForDiff {
constructor() {
/** @type {!Array.<number>} */
this.indexes = [];
/** @type {!Array.<string>} */
this.ids = [];
/** @type {!Array.<number>} */
this.selfSizes = [];
}
}
/**
* @unrestricted
*/
class Diff {
constructor() {
/** @type {number} */
this.addedCount = 0;
/** @type {number} */
this.removedCount = 0;
/** @type {number} */
this.addedSize = 0;
/** @type {number} */
this.removedSize = 0;
/** @type {!Array.<number>} */
this.deletedIndexes = [];
/** @type {!Array.<number>} */
this.addedIndexes = [];
}
}
/**
* @unrestricted
*/
class ItemsRange {
/**
* @param {number} startPosition
* @param {number} endPosition
* @param {number} totalLength
* @param {!Array.<*>} items
*/
constructor(startPosition, endPosition, totalLength, items) {
/** @type {number} */
this.startPosition = startPosition;
/** @type {number} */
this.endPosition = endPosition;
/** @type {number} */
this.totalLength = totalLength;
/** @type {!Array.<*>} */
this.items = items;
}
}
/**
* @unrestricted
*/
class StaticData {
/**
* @param {number} nodeCount
* @param {number} rootNodeIndex
* @param {number} totalSize
* @param {number} maxJSObjectId
*/
constructor(nodeCount, rootNodeIndex, totalSize, maxJSObjectId) {
/** @type {number} */
this.nodeCount = nodeCount;
/** @type {number} */
this.rootNodeIndex = rootNodeIndex;
/** @type {number} */
this.totalSize = totalSize;
/** @type {number} */
this.maxJSObjectId = maxJSObjectId;
}
}
/**
* @unrestricted
*/
class Statistics {
constructor() {
/** @type {number} */
this.total;
/** @type {number} */
this.v8heap;
/** @type {number} */
this.native;
/** @type {number} */
this.code;
/** @type {number} */
this.jsArrays;
/** @type {number} */
this.strings;
/** @type {number} */
this.system;
}
}
/**
* @unrestricted
*/
class Samples {
/**
* @param {!Array.<number>} timestamps
* @param {!Array.<number>} lastAssignedIds
* @param {!Array.<number>} sizes
*/
constructor(timestamps, lastAssignedIds, sizes) {
this.timestamps = timestamps;
this.lastAssignedIds = lastAssignedIds;
this.sizes = sizes;
}
}
/**
* @unrestricted
*/
class Location {
/**
* @param {number} scriptId
* @param {number} lineNumber
* @param {number} columnNumber
*/
constructor(scriptId, lineNumber, columnNumber) {
this.scriptId = scriptId;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
}
}
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
class AllocationProfile {
constructor(profile, liveObjectStats) {
this._strings = profile.strings;
this._liveObjectStats = liveObjectStats;
this._nextNodeId = 1;
this._functionInfos = [];
this._idToNode = {};
this._idToTopDownNode = {};
this._collapsedTopNodeIdToFunctionInfo = {};
this._traceTops = null;
this._buildFunctionAllocationInfos(profile);
this._traceTree = this._buildAllocationTree(profile, liveObjectStats);
}
_buildFunctionAllocationInfos(profile) {
const strings = this._strings;
const functionInfoFields = profile.snapshot.meta.trace_function_info_fields;
const functionNameOffset = functionInfoFields.indexOf('name');
const scriptNameOffset = functionInfoFields.indexOf('script_name');
const scriptIdOffset = functionInfoFields.indexOf('script_id');
const lineOffset = functionInfoFields.indexOf('line');
const columnOffset = functionInfoFields.indexOf('column');
const functionInfoFieldCount = functionInfoFields.length;
const rawInfos = profile.trace_function_infos;
const infoLength = rawInfos.length;
const functionInfos = this._functionInfos = new Array(infoLength / functionInfoFieldCount);
let index = 0;
for (let i = 0; i < infoLength; i += functionInfoFieldCount) {
functionInfos[index++] = new FunctionAllocationInfo(
strings[rawInfos[i + functionNameOffset]], strings[rawInfos[i + scriptNameOffset]],
rawInfos[i + scriptIdOffset], rawInfos[i + lineOffset], rawInfos[i + columnOffset]);
}
}
_buildAllocationTree(profile, liveObjectStats) {
const traceTreeRaw = profile.trace_tree;
const functionInfos = this._functionInfos;
const idToTopDownNode = this._idToTopDownNode;
const traceNodeFields = profile.snapshot.meta.trace_node_fields;
const nodeIdOffset = traceNodeFields.indexOf('id');
const functionInfoIndexOffset = traceNodeFields.indexOf('function_info_index');
const allocationCountOffset = traceNodeFields.indexOf('count');
const allocationSizeOffset = traceNodeFields.indexOf('size');
const childrenOffset = traceNodeFields.indexOf('children');
const nodeFieldCount = traceNodeFields.length;
function traverseNode(rawNodeArray, nodeOffset, parent) {
const functionInfo = functionInfos[rawNodeArray[nodeOffset + functionInfoIndexOffset]];
const id = rawNodeArray[nodeOffset + nodeIdOffset];
const stats = liveObjectStats[id];
const liveCount = stats ? stats.count : 0;
const liveSize = stats ? stats.size : 0;
const result = new TopDownAllocationNode(
id, functionInfo, rawNodeArray[nodeOffset + allocationCountOffset],
rawNodeArray[nodeOffset + allocationSizeOffset], liveCount, liveSize, parent);
idToTopDownNode[id] = result;
functionInfo.addTraceTopNode(result);
const rawChildren = rawNodeArray[nodeOffset + childrenOffset];
for (let i = 0; i < rawChildren.length; i += nodeFieldCount) {
result.children.push(traverseNode(rawChildren, i, result));
}
return result;
}
return traverseNode(traceTreeRaw, 0, null);
}
/**
* @return {!Array.<!SerializedAllocationNode>}
*/
serializeTraceTops() {
if (this._traceTops) {
return this._traceTops;
}
const result = this._traceTops = [];
const functionInfos = this._functionInfos;
for (let i = 0; i < functionInfos.length; i++) {
const info = functionInfos[i];
if (info.totalCount === 0) {
continue;
}
const nodeId = this._nextNodeId++;
const isRoot = i === 0;
result.push(this._serializeNode(
nodeId, info, info.totalCount, info.totalSize, info.totalLiveCount, info.totalLiveSize, !isRoot));
this._collapsedTopNodeIdToFunctionInfo[nodeId] = info;
}
result.sort(function(a, b) {
return b.size - a.size;
});
return result;
}
/**
* @param {number} nodeId
* @return {!AllocationNodeCallers}
*/
serializeCallers(nodeId) {
let node = this._ensureBottomUpNode(nodeId);
const nodesWithSingleCaller = [];
while (node.callers().length === 1) {
node = node.callers()[0];
nodesWithSingleCaller.push(this._serializeCaller(node));
}
const branchingCallers = [];
const callers = node.callers();
for (let i = 0; i < callers.length; i++) {
branchingCallers.push(this._serializeCaller(callers[i]));
}
return new AllocationNodeCallers(nodesWithSingleCaller, branchingCallers);
}
/**
* @param {number} traceNodeId
* @return {!Array.<!AllocationStackFrame>}
*/
serializeAllocationStack(traceNodeId) {
let node = this._idToTopDownNode[traceNodeId];
const result = [];
while (node) {
const functionInfo = node.functionInfo;
result.push(new AllocationStackFrame(
functionInfo.functionName, functionInfo.scriptName, functionInfo.scriptId, functionInfo.line,
functionInfo.column));
node = node.parent;
}
return result;
}
/**
* @param {number} allocationNodeId
* @return {!Array.<number>}
*/
traceIds(allocationNodeId) {
return this._ensureBottomUpNode(allocationNodeId).traceTopIds;
}
/**
* @param {number} nodeId
* @return {!BottomUpAllocationNode}
*/
_ensureBottomUpNode(nodeId) {
let node = this._idToNode[nodeId];
if (!node) {
const functionInfo = this._collapsedTopNodeIdToFunctionInfo[nodeId];
node = functionInfo.bottomUpRoot();
delete this._collapsedTopNodeIdToFunctionInfo[nodeId];
this._idToNode[nodeId] = node;
}
return node;
}
/**
* @param {!BottomUpAllocationNode} node
* @return {!SerializedAllocationNode}
*/
_serializeCaller(node) {
const callerId = this._nextNodeId++;
this._idToNode[callerId] = node;
return this._serializeNode(
callerId, node.functionInfo, node.allocationCount, node.allocationSize, node.liveCount, node.liveSize,
node.hasCallers());
}
/**
* @param {number} nodeId
* @param {!FunctionAllocationInfo} functionInfo
* @param {number} count
* @param {number} size
* @param {number} liveCount
* @param {number} liveSize
* @param {boolean} hasChildren
* @return {!SerializedAllocationNode}
*/
_serializeNode(nodeId, functionInfo, count, size, liveCount, liveSize, hasChildren) {
return new SerializedAllocationNode(
nodeId, functionInfo.functionName, functionInfo.scriptName, functionInfo.scriptId, functionInfo.line,
functionInfo.column, count, size, liveCount, liveSize, hasChildren);
}
}
/**
* @unrestricted
*/
class TopDownAllocationNode {
/**
* @param {number} id
* @param {!FunctionAllocationInfo} functionInfo
* @param {number} count
* @param {number} size
* @param {number} liveCount
* @param {number} liveSize
* @param {?TopDownAllocationNode} parent
*/
constructor(id, functionInfo, count, size, liveCount, liveSize, parent) {
this.id = id;
this.functionInfo = functionInfo;
this.allocationCount = count;
this.allocationSize = size;
this.liveCount = liveCount;
this.liveSize = liveSize;
this.parent = parent;
this.children = [];
}
}
/**
* @unrestricted
*/
class BottomUpAllocationNode {
/**
* @param {!FunctionAllocationInfo} functionInfo
*/
constructor(functionInfo) {
this.functionInfo = functionInfo;
this.allocationCount = 0;
this.allocationSize = 0;
this.liveCount = 0;
this.liveSize = 0;
this.traceTopIds = [];
this._callers = [];
}
/**
* @param {!TopDownAllocationNode} traceNode
* @return {!BottomUpAllocationNode}
*/
addCaller(traceNode) {
const functionInfo = traceNode.functionInfo;
let result;
for (let i = 0; i < this._callers.length; i++) {
const caller = this._callers[i];
if (caller.functionInfo === functionInfo) {
result = caller;
break;
}
}
if (!result) {
result = new BottomUpAllocationNode(functionInfo);
this._callers.push(result);
}
return result;
}
/**
* @return {!Array.<!BottomUpAllocationNode>}
*/
callers() {
return this._callers;
}
/**
* @return {boolean}
*/
hasCallers() {
return this._callers.length > 0;
}
}
/**
* @unrestricted
*/
class FunctionAllocationInfo {
/**
* @param {string} functionName
* @param {string} scriptName
* @param {number} scriptId
* @param {number} line
* @param {number} column
*/
constructor(functionName, scriptName, scriptId, line, column) {
this.functionName = functionName;
this.scriptName = scriptName;
this.scriptId = scriptId;
this.line = line;
this.column = column;
this.totalCount = 0;
this.totalSize = 0;
this.totalLiveCount = 0;
this.totalLiveSize = 0;
this._traceTops = [];
}
/**
* @param {!TopDownAllocationNode} node
*/
addTraceTopNode(node) {
if (node.allocationCount === 0) {
return;
}
this._traceTops.push(node);
this.totalCount += node.allocationCount;
this.totalSize += node.allocationSize;
this.totalLiveCount += node.liveCount;
this.totalLiveSize += node.liveSize;
}
/**
* @return {?BottomUpAllocationNode}
*/
bottomUpRoot() {
if (!this._traceTops.length) {
return null;
}
if (!this._bottomUpTree) {
this._buildAllocationTraceTree();
}
return this._bottomUpTree;
}
_buildAllocationTraceTree() {
this._bottomUpTree = new BottomUpAllocationNode(this);
for (let i = 0; i < this._traceTops.length; i++) {
let node = this._traceTops[i];
let bottomUpNode = this._bottomUpTree;
const count = node.allocationCount;
const size = node.allocationSize;
const liveCount = node.liveCount;
const liveSize = node.liveSize;
const traceId = node.id;
while (true) {
bottomUpNode.allocationCount += count;
bottomUpNode.allocationSize += size;
bottomUpNode.liveCount += liveCount;
bottomUpNode.liveSize += liveSize;
bottomUpNode.traceTopIds.push(traceId);
node = node.parent;
if (node === null) {
break;
}
bottomUpNode = bottomUpNode.addCaller(node);
}
}
}
}
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @implements {HeapSnapshotItem}
* @unrestricted
*/
class HeapSnapshotEdge {
/**
* @param {!HeapSnapshot} snapshot
* @param {number=} edgeIndex
*/
constructor(snapshot, edgeIndex) {
this._snapshot = snapshot;
this._edges = snapshot.containmentEdges;
this.edgeIndex = edgeIndex || 0;
}
/**
* @return {!HeapSnapshotEdge}
*/
clone() {
return new HeapSnapshotEdge(this._snapshot, this.edgeIndex);
}
/**
* @return {boolean}
*/
hasStringName() {
throw new Error('Not implemented');
}
/**
* @return {string}
*/
name() {
throw new Error('Not implemented');
}
/**
* @return {!HeapSnapshotNode}
*/
node() {
return this._snapshot.createNode(this.nodeIndex());
}
/**
* @return {number}
*/
nodeIndex() {
return this._edges[this.edgeIndex + this._snapshot._edgeToNodeOffset];
}
/**
* @override
* @return {string}
*/
toString() {
return 'HeapSnapshotEdge: ' + this.name();
}
/**
* @return {string}
*/
type() {
return this._snapshot._edgeTypes[this.rawType()];
}
/**
* @override
* @return {number}
*/
itemIndex() {
return this.edgeIndex;
}
/**
* @override
* @return {!Edge}
*/
serialize() {
return new Edge(
this.name(), this.node().serialize(), this.type(), this.edgeIndex);
}
/**
* @protected
* @return {number}
*/
rawType() {
return this._edges[this.edgeIndex + this._snapshot._edgeTypeOffset];
}
}
/**
* @implements {HeapSnapshotItemIndexProvider}
* @unrestricted
*/
class HeapSnapshotNodeIndexProvider {
/**
* @param {!HeapSnapshot} snapshot
*/
constructor(snapshot) {
this._node = snapshot.createNode();
}
/**
* @override
* @param {number} index
* @return {!HeapSnapshotNode}
*/
itemForIndex(index) {
this._node.nodeIndex = index;
return this._node;
}
}
/**
* @implements {HeapSnapshotItemIndexProvider}
* @unrestricted
*/
class HeapSnapshotEdgeIndexProvider {
/**
* @param {!HeapSnapshot} snapshot
*/
constructor(snapshot) {
this._edge = snapshot.createEdge(0);
}
/**
* @override
* @param {number} index
* @return {!HeapSnapshotEdge}
*/
itemForIndex(index) {
this._edge.edgeIndex = index;
return this._edge;
}
}
/**
* @implements {HeapSnapshotItemIndexProvider}
* @unrestricted
*/
class HeapSnapshotRetainerEdgeIndexProvider {
/**
* @param {!HeapSnapshot} snapshot
*/
constructor(snapshot) {
this._retainerEdge = snapshot.createRetainingEdge(0);
}
/**
* @override
* @param {number} index
* @return {!HeapSnapshotRetainerEdge}
*/
itemForIndex(index) {
this._retainerEdge.setRetainerIndex(index);
return this._retainerEdge;
}
}
/**
* @implements {HeapSnapshotItemIterator}
* @unrestricted
*/
class HeapSnapshotEdgeIterator {
/**
* @param {!HeapSnapshotNode} node
*/
constructor(node) {
this._sourceNode = node;
this.edge = node._snapshot.createEdge(node.edgeIndexesStart());
}
/**
* @override
* @return {boolean}
*/
hasNext() {
return this.edge.edgeIndex < this._sourceNode.edgeIndexesEnd();
}
/**
* @override
* @return {!HeapSnapshotEdge}
*/
item() {
return this.edge;
}
/**
* @override
*/
next() {
this.edge.edgeIndex += this.edge._snapshot._edgeFieldsCount;
}
}
/**
* @implements {HeapSnapshotItem}
* @unrestricted
*/
class HeapSnapshotRetainerEdge {
/**
* @param {!HeapSnapshot} snapshot
* @param {number} retainerIndex
*/
constructor(snapshot, retainerIndex) {
this._snapshot = snapshot;
this.setRetainerIndex(retainerIndex);
}
/**
* @return {!HeapSnapshotRetainerEdge}
*/
clone() {
return new HeapSnapshotRetainerEdge(this._snapshot, this.retainerIndex());
}
/**
* @return {boolean}
*/
hasStringName() {
return this._edge().hasStringName();
}
/**
* @return {string}
*/
name() {
return this._edge().name();
}
/**
* @return {!HeapSnapshotNode}
*/
node() {
return this._node();
}
/**
* @return {number}
*/
nodeIndex() {
return this._retainingNodeIndex;
}
/**
* @return {number}
*/
retainerIndex() {
return this._retainerIndex;
}
/**
* @param {number} retainerIndex
*/
setRetainerIndex(retainerIndex) {
if (retainerIndex === this._retainerIndex) {
return;
}
this._retainerIndex = retainerIndex;
this._globalEdgeIndex = this._snapshot._retainingEdges[retainerIndex];
this._retainingNodeIndex = this._snapshot._retainingNodes[retainerIndex];
this._edgeInstance = null;
this._nodeInstance = null;
}
/**
* @param {number} edgeIndex
*/
set edgeIndex(edgeIndex) {
this.setRetainerIndex(edgeIndex);
}
_node() {
if (!this._nodeInstance) {
this._nodeInstance = this._snapshot.createNode(this._retainingNodeIndex);
}
return this._nodeInstance;
}
_edge() {
if (!this._edgeInstance) {
this._edgeInstance = this._snapshot.createEdge(this._globalEdgeIndex);
}
return this._edgeInstance;
}
/**
* @override
* @return {string}
*/
toString() {
return this._edge().toString();
}
/**
* @override
* @return {number}
*/
itemIndex() {
return this._retainerIndex;
}
/**
* @override
* @return {!Edge}
*/
serialize() {
return new Edge(
this.name(), this.node().serialize(), this.type(), this._globalEdgeIndex);
}
/**
* @return {string}
*/
type() {
return this._edge().type();
}
}
/**
* @implements {HeapSnapshotItemIterator}
* @unrestricted
*/
class HeapSnapshotRetainerEdgeIterator {
/**
* @param {!HeapSnapshotNode} retainedNode
*/
constructor(retainedNode) {
const snapshot = retainedNode._snapshot;
const retainedNodeOrdinal = retainedNode.ordinal();
const retainerIndex = snapshot._firstRetainerIndex[retainedNodeOrdinal];
this._retainersEnd = snapshot._firstRetainerIndex[retainedNodeOrdinal + 1];
this.retainer = snapshot.createRetainingEdge(retainerIndex);
}
/**
* @override
* @return {boolean}
*/
hasNext() {
return this.retainer.retainerIndex() < this._retainersEnd;
}
/**
* @override
* @return {!HeapSnapshotRetainerEdge}
*/
item() {
return this.retainer;
}
/**
* @override
*/
next() {
this.retainer.setRetainerIndex(this.retainer.retainerIndex() + 1);
}
}
/**
* @implements {HeapSnapshotItem}
* @unrestricted
*/
class HeapSnapshotNode {
/**
* @param {!HeapSnapshot} snapshot
* @param {number=} nodeIndex
*/
constructor(snapshot, nodeIndex) {
this._snapshot = snapshot;
this.nodeIndex = nodeIndex || 0;
}
/**
* @return {number}
*/
distance() {
return this._snapshot._nodeDistances[this.nodeIndex / this._snapshot._nodeFieldCount];
}
/**
* @return {string}
*/
className() {
throw new Error('Not implemented');
}
/**
* @return {number}
*/
classIndex() {
throw new Error('Not implemented');
}
/**
* @return {number}
*/
dominatorIndex() {
const nodeFieldCount = this._snapshot._nodeFieldCount;
return this._snapshot._dominatorsTree[this.nodeIndex / this._snapshot._nodeFieldCount] * nodeFieldCount;
}
/**
* @return {!HeapSnapshotEdgeIterator}
*/
edges() {
return new HeapSnapshotEdgeIterator(this);
}
/**
* @return {number}
*/
edgesCount() {
return (this.edgeIndexesEnd() - this.edgeIndexesStart()) / this._snapshot._edgeFieldsCount;
}
/**
* @return {number}
*/
id() {
throw new Error('Not implemented');
}
/**
* @return {boolean}
*/
isRoot() {
return this.nodeIndex === this._snapshot._rootNodeIndex;
}
/**
* @return {string}
*/
name() {
return this._snapshot.strings[this._name()];
}
/**
* @return {number}
*/
retainedSize() {
return this._snapshot._retainedSizes[this.ordinal()];
}
/**
* @return {!HeapSnapshotRetainerEdgeIterator}
*/
retainers() {
return new HeapSnapshotRetainerEdgeIterator(this);
}
/**
* @return {number}
*/
retainersCount() {
const snapshot = this._snapshot;
const ordinal = this.ordinal();
return snapshot._firstRetainerIndex[ordinal + 1] - snapshot._firstRetainerIndex[ordinal];
}
/**
* @return {number}
*/
selfSize() {
const snapshot = this._snapshot;
return snapshot.nodes[this.nodeIndex + snapshot._nodeSelfSizeOffset];
}
/**
* @return {string}
*/
type() {
return this._snapshot._nodeTypes[this.rawType()];
}
/**
* @return {number}
*/
traceNodeId() {
const snapshot = this._snapshot;
return snapshot.nodes[this.nodeIndex + snapshot._nodeTraceNodeIdOffset];
}
/**
* @override
* @return {number}
*/
itemIndex() {
return this.nodeIndex;
}
/**
* @override
* @return {!Node}
*/
serialize() {
return new Node(
this.id(), this.name(), this.distance(), this.nodeIndex, this.retainedSize(), this.selfSize(), this.type());
}
/**
* @return {number}
*/
_name() {
const snapshot = this._snapshot;
return snapshot.nodes[this.nodeIndex + snapshot._nodeNameOffset];
}
/**
* @return {number}
*/
edgeIndexesStart() {
return this._snapshot._firstEdgeIndexes[this.ordinal()];
}
/**
* @return {number}
*/
edgeIndexesEnd() {
return this._snapshot._firstEdgeIndexes[this.ordinal() + 1];
}
/**
* @return {number}
*/
ordinal() {
return this.nodeIndex / this._snapshot._nodeFieldCount;
}
/**
* @return {number}
*/
_nextNodeIndex() {
return this.nodeIndex + this._snapshot._nodeFieldCount;
}
/**
* @protected
* @return {number}
*/
rawType() {
const snapshot = this._snapshot;
return snapshot.nodes[this.nodeIndex + snapshot._nodeTypeOffset];
}
}
/**
* @implements {HeapSnapshotItemIterator}
* @unrestricted
*/
class HeapSnapshotNodeIterator {
/**
* @param {!HeapSnapshotNode} node
*/
constructor(node) {
this.node = node;
this._nodesLength = node._snapshot.nodes.length;
}
/**
* @override
* @return {boolean}
*/
hasNext() {
return this.node.nodeIndex < this._nodesLength;
}
/**
* @override
* @return {!HeapSnapshotNode}
*/
item() {
return this.node;
}
/**
* @override
*/
next() {
this.node.nodeIndex = this.node._nextNodeIndex();
}
}
/**
* @implements {HeapSnapshotItemIterator}
* @unrestricted
*/
class HeapSnapshotIndexRangeIterator {
/**
* @param {!HeapSnapshotItemIndexProvider} itemProvider
* @param {!Array.<number>|!Uint32Array} indexes
*/
constructor(itemProvider, indexes) {
this._itemProvider = itemProvider;
this._indexes = indexes;
this._position = 0;
}
/**
* @override
* @return {boolean}
*/
hasNext() {
return this._position < this._indexes.length;
}
/**
* @override
* @return {!HeapSnapshotItem}
*/
item() {
const index = this._indexes[this._position];
return this._itemProvider.itemForIndex(index);
}
/**
* @override
*/
next() {
++this._position;
}
}
/**
* @implements {HeapSnapshotItemIterator}
* @unrestricted
*/
class HeapSnapshotFilteredIterator {
/**
* @param {!HeapSnapshotItemIterator} iterator
* @param {function(!HeapSnapshotItem):boolean=} filter
*/
constructor(iterator, filter) {
this._iterator = iterator;
this._filter = filter;
this._skipFilteredItems();
}
/**
* @override
* @return {boolean}
*/
hasNext() {
return this._iterator.hasNext();
}
/**
* @override
* @return {!HeapSnapshotItem}
*/
item() {
return this._iterator.item();
}
/**
* @override
*/
next() {
this._iterator.next();
this._skipFilteredItems();
}
_skipFilteredItems() {
while (this._iterator.hasNext() && !this._filter(this._iterator.item())) {
this._iterator.next();
}
}
}
/**
* @unrestricted
*/
class HeapSnapshotProgress {
/**
* @param {!HeapSnapshotWorkerDispatcher=} dispatcher
*/
constructor(dispatcher) {
this._dispatcher = dispatcher;
}
/**
* @param {string} status
*/
updateStatus(status) {
this._sendUpdateEvent(serializeUIString(status));
}
/**
* @param {string} title
* @param {number} value
* @param {number} total
*/
updateProgress(title, value, total) {
const percentValue = ((total ? (value / total) : 0) * 100).toFixed(0);
this._sendUpdateEvent(serializeUIString(title, [percentValue]));
}
/**
* @param {string} error
*/
reportProblem(error) {
// May be undefined in tests.
if (this._dispatcher) {
this._dispatcher.sendEvent(HeapSnapshotProgressEvent.BrokenSnapshot, error);
}
}
/**
* @param {string} serializedText
*/
_sendUpdateEvent(serializedText) {
// May be undefined in tests.
if (this._dispatcher) {
this._dispatcher.sendEvent(HeapSnapshotProgressEvent.Update, serializedText);
}
}
}
/**
* @unrestricted
*/
class HeapSnapshotProblemReport {
/**
* @param {string} title
*/
constructor(title) {
this._errors = [title];
}
/**
* @param {string} error
*/
addError(error) {
if (this._errors.length > 100) {
return;
}
this._errors.push(error);
}
/**
* @override
* @return {string}
*/
toString() {
return this._errors.join('\n ');
}
}
/**
* @unrestricted
*/
class HeapSnapshot {
/**
* @param {!Object} profile
* @param {!HeapSnapshotProgress} progress
*/
constructor(profile, progress) {
/** @type {!Uint32Array} */
this.nodes = profile.nodes;
/** @type {!Uint32Array} */
this.containmentEdges = profile.edges;
/** @type {!HeapSnapshotMetainfo} */
this._metaNode = profile.snapshot.meta;
/** @type {!Array.<number>} */
this._rawSamples = profile.samples;
/** @type {?Samples} */
this._samples = null;
/** @type {!Array.<string>} */
this.strings = profile.strings;
/** @type {!Array.<number>} */
this._locations = profile.locations;
this._progress = progress;
this._noDistance = -5;
this._rootNodeIndex = 0;
if (profile.snapshot.root_index) {
this._rootNodeIndex = profile.snapshot.root_index;
}
this._snapshotDiffs = {};
this._aggregatesForDiff = null;
this._aggregates = {};
this._aggregatesSortedFlags = {};
this._profile = profile;
}
/**
* @protected
*/
initialize() {
const meta = this._metaNode;
this._nodeTypeOffset = meta.node_fields.indexOf('type');
this._nodeNameOffset = meta.node_fields.indexOf('name');
this._nodeIdOffset = meta.node_fields.indexOf('id');
this._nodeSelfSizeOffset = meta.node_fields.indexOf('self_size');
this._nodeEdgeCountOffset = meta.node_fields.indexOf('edge_count');
this._nodeTraceNodeIdOffset = meta.node_fields.indexOf('trace_node_id');
this._nodeFieldCount = meta.node_fields.length;
this._nodeTypes = meta.node_types[this._nodeTypeOffset];
this._nodeArrayType = this._nodeTypes.indexOf('array');
this._nodeHiddenType = this._nodeTypes.indexOf('hidden');
this._nodeObjectType = this._nodeTypes.indexOf('object');
this._nodeNativeType = this._nodeTypes.indexOf('native');
this._nodeConsStringType = this._nodeTypes.indexOf('concatenated string');
this._nodeSlicedStringType = this._nodeTypes.indexOf('sliced string');
this._nodeCodeType = this._nodeTypes.indexOf('code');
this._nodeSyntheticType = this._nodeTypes.indexOf('synthetic');
this._edgeFieldsCount = meta.edge_fields.length;
this._edgeTypeOffset = meta.edge_fields.indexOf('type');
this._edgeNameOffset = meta.edge_fields.indexOf('name_or_index');
this._edgeToNodeOffset = meta.edge_fields.indexOf('to_node');
this._edgeTypes = meta.edge_types[this._edgeTypeOffset];
this._edgeTypes.push('invisible');
this._edgeElementType = this._edgeTypes.indexOf('element');
this._edgeHiddenType = this._edgeTypes.indexOf('hidden');
this._edgeInternalType = this._edgeTypes.indexOf('internal');
this._edgeShortcutType = this._edgeTypes.indexOf('shortcut');
this._edgeWeakType = this._edgeTypes.indexOf('weak');
this._edgeInvisibleType = this._edgeTypes.indexOf('invisible');
const location_fields = meta.location_fields || [];
this._locationIndexOffset = location_fields.indexOf('object_index');
this._locationScriptIdOffset = location_fields.indexOf('script_id');
this._locationLineOffset = location_fields.indexOf('line');
this._locationColumnOffset = location_fields.indexOf('column');
this._locationFieldCount = location_fields.length;
this.nodeCount = this.nodes.length / this._nodeFieldCount;
this._edgeCount = this.containmentEdges.length / this._edgeFieldsCount;
this._retainedSizes = new Float64Array(this.nodeCount);
this._firstEdgeIndexes = new Uint32Array(this.nodeCount + 1);
this._retainingNodes = new Uint32Array(this._edgeCount);
this._retainingEdges = new Uint32Array(this._edgeCount);
this._firstRetainerIndex = new Uint32Array(this.nodeCount + 1);
this._nodeDistances = new Int32Array(this.nodeCount);
this._firstDominatedNodeIndex = new Uint32Array(this.nodeCount + 1);
this._dominatedNodes = new Uint32Array(this.nodeCount - 1);
this._progress.updateStatus(ls`Building edge indexes…`);
this._buildEdgeIndexes();
this._progress.updateStatus(ls`Building retainers…`);
this._buildRetainers();
this._progress.updateStatus(ls`Calculating node flags…`);
this.calculateFlags();
this._progress.updateStatus(ls`Calculating distances…`);
this.calculateDistances();
this._progress.updateStatus(ls`Building postorder index…`);
const result = this._buildPostOrderIndex();
// Actually it is array that maps node ordinal number to dominator node ordinal number.
this._progress.updateStatus(ls`Building dominator tree…`);
this._dominatorsTree =
this._buildDominatorTree(result.postOrderIndex2NodeOrdinal, result.nodeOrdinal2PostOrderIndex);
this._progress.updateStatus(ls`Calculating retained sizes…`);
this._calculateRetainedSizes(result.postOrderIndex2NodeOrdinal);
this._progress.updateStatus(ls`Building dominated nodes…`);
this._buildDominatedNodes();
this._progress.updateStatus(ls`Calculating statistics…`);
this.calculateStatistics();
this._progress.updateStatus(ls`Calculating samples…`);
this._buildSamples();
this._progress.updateStatus(ls`Building locations…`);
this._buildLocationMap();
this._progress.updateStatus(ls`Finished processing.`);
if (this._profile.snapshot.trace_function_count) {
this._progress.updateStatus(ls`Building allocation statistics…`);
const nodes = this.nodes;
const nodesLength = nodes.length;
const nodeFieldCount = this._nodeFieldCount;
const node = this.rootNode();
const liveObjects = {};
for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) {
node.nodeIndex = nodeIndex;
const traceNodeId = node.traceNodeId();
let stats = liveObjects[traceNodeId];
if (!stats) {
liveObjects[traceNodeId] = stats = {count: 0, size: 0, ids: []};
}
stats.count++;
stats.size += node.selfSize();
stats.ids.push(node.id());
}
this._allocationProfile = new AllocationProfile(this._profile, liveObjects);
this._progress.updateStatus(ls`Done`);
}
}
_buildEdgeIndexes() {
const nodes = this.nodes;
const nodeCount = this.nodeCount;
const firstEdgeIndexes = this._firstEdgeIndexes;
const nodeFieldCount = this._nodeFieldCount;
const edgeFieldsCount = this._edgeFieldsCount;
const nodeEdgeCountOffset = this._nodeEdgeCountOffset;
firstEdgeIndexes[nodeCount] = this.containmentEdges.length;
for (let nodeOrdinal = 0, edgeIndex = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) {
firstEdgeIndexes[nodeOrdinal] = edgeIndex;
edgeIndex += nodes[nodeOrdinal * nodeFieldCount + nodeEdgeCountOffset] * edgeFieldsCount;
}
}
_buildRetainers() {
const retainingNodes = this._retainingNodes;
const retainingEdges = this._retainingEdges;
// Index of the first retainer in the _retainingNodes and _retainingEdges
// arrays. Addressed by retained node index.
const firstRetainerIndex = this._firstRetainerIndex;
const containmentEdges = this.containmentEdges;
const edgeFieldsCount = this._edgeFieldsCount;
const nodeFieldCount = this._nodeFieldCount;
const edgeToNodeOffset = this._edgeToNodeOffset;
const firstEdgeIndexes = this._firstEdgeIndexes;
const nodeCount = this.nodeCount;
for (let toNodeFieldIndex = edgeToNodeOffset, l = containmentEdges.length; toNodeFieldIndex < l;
toNodeFieldIndex += edgeFieldsCount) {
const toNodeIndex = containmentEdges[toNodeFieldIndex];
if (toNodeIndex % nodeFieldCount) {
throw new Error('Invalid toNodeIndex ' + toNodeIndex);
}
++firstRetainerIndex[toNodeIndex / nodeFieldCount];
}
for (let i = 0, firstUnusedRetainerSlot = 0; i < nodeCount; i++) {
const retainersCount = firstRetainerIndex[i];
firstRetainerIndex[i] = firstUnusedRetainerSlot;
retainingNodes[firstUnusedRetainerSlot] = retainersCount;
firstUnusedRetainerSlot += retainersCount;
}
firstRetainerIndex[nodeCount] = retainingNodes.length;
let nextNodeFirstEdgeIndex = firstEdgeIndexes[0];
for (let srcNodeOrdinal = 0; srcNodeOrdinal < nodeCount; ++srcNodeOrdinal) {
const firstEdgeIndex = nextNodeFirstEdgeIndex;
nextNodeFirstEdgeIndex = firstEdgeIndexes[srcNodeOrdinal + 1];
const srcNodeIndex = srcNodeOrdinal * nodeFieldCount;
for (let edgeIndex = firstEdgeIndex; edgeIndex < nextNodeFirstEdgeIndex; edgeIndex += edgeFieldsCount) {
const toNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
if (toNodeIndex % nodeFieldCount) {
throw new Error('Invalid toNodeIndex ' + toNodeIndex);
}
const firstRetainerSlotIndex = firstRetainerIndex[toNodeIndex / nodeFieldCount];
const nextUnusedRetainerSlotIndex = firstRetainerSlotIndex + (--retainingNodes[firstRetainerSlotIndex]);
retainingNodes[nextUnusedRetainerSlotIndex] = srcNodeIndex;
retainingEdges[nextUnusedRetainerSlotIndex] = edgeIndex;
}
}
}
/**
* @param {number=} nodeIndex
*/
createNode(nodeIndex) {
throw new Error('Not implemented');
}
/**
* @param {number} edgeIndex
* @return {!JSHeapSnapshotEdge}
*/
createEdge(edgeIndex) {
throw new Error('Not implemented');
}
/**
* @param {number} retainerIndex
* @return {!JSHeapSnapshotRetainerEdge}
*/
createRetainingEdge(retainerIndex) {
throw new Error('Not implemented');
}
/**
* @return {!HeapSnapshotNodeIterator}
*/
_allNodes() {
return new HeapSnapshotNodeIterator(this.rootNode());
}
/**
* @return {!HeapSnapshotNode}
*/
rootNode() {
return this.createNode(this._rootNodeIndex);
}
/**
* @return {number}
*/
get rootNodeIndex() {
return this._rootNodeIndex;
}
/**
* @return {number}
*/
get totalSize() {
return this.rootNode().retainedSize();
}
/**
* @param {number} nodeIndex
* @return {number}
*/
_getDominatedIndex(nodeIndex) {
if (nodeIndex % this._nodeFieldCount) {
throw new Error('Invalid nodeIndex: ' + nodeIndex);
}
return this._firstDominatedNodeIndex[nodeIndex / this._nodeFieldCount];
}
/**
* @param {!NodeFilter} nodeFilter
* @return {undefined|function(!HeapSnapshotNode):boolean}
*/
_createFilter(nodeFilter) {
const minNodeId = nodeFilter.minNodeId;
const maxNodeId = nodeFilter.maxNodeId;
const allocationNodeId = nodeFilter.allocationNodeId;
let filter;
if (typeof allocationNodeId === 'number') {
filter = this._createAllocationStackFilter(allocationNodeId);
filter.key = 'AllocationNodeId: ' + allocationNodeId;
} else if (typeof minNodeId === 'number' && typeof maxNodeId === 'number') {
filter = this._createNodeIdFilter(minNodeId, maxNodeId);
filter.key = 'NodeIdRange: ' + minNodeId + '..' + maxNodeId;
}
return filter;
}
/**
* @param {!SearchConfig} searchConfig
* @param {!NodeFilter} nodeFilter
* @return {!Array.<number>}
*/
search(searchConfig, nodeFilter) {
const query = searchConfig.query;
function filterString(matchedStringIndexes, string, index) {
if (string.indexOf(query) !== -1) {
matchedStringIndexes.add(index);
}
return matchedStringIndexes;
}
const regexp = searchConfig.isRegex ? new RegExp(query) : createPlainTextSearchRegex(query, 'i');
function filterRegexp(matchedStringIndexes, string, index) {
if (regexp.test(string)) {
matchedStringIndexes.add(index);
}
return matchedStringIndexes;
}
const stringFilter = (searchConfig.isRegex || !searchConfig.caseSensitive) ? filterRegexp : filterString;
const stringIndexes = this.strings.reduce(stringFilter, new Set());
if (!stringIndexes.size) {
return [];
}
const filter = this._createFilter(nodeFilter);
const nodeIds = [];
const nodesLength = this.nodes.length;
const nodes = this.nodes;
const nodeNameOffset = this._nodeNameOffset;
const nodeIdOffset = this._nodeIdOffset;
const nodeFieldCount = this._nodeFieldCount;
const node = this.rootNode();
for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) {
node.nodeIndex = nodeIndex;
if (filter && !filter(node)) {
continue;
}
if (stringIndexes.has(nodes[nodeIndex + nodeNameOffset])) {
nodeIds.push(nodes[nodeIndex + nodeIdOffset]);
}
}
return nodeIds;
}
/**
* @param {!NodeFilter} nodeFilter
* @return {!Object.<string, !Aggregate>}
*/
aggregatesWithFilter(nodeFilter) {
const filter = this._createFilter(nodeFilter);
const key = filter ? filter.key : 'allObjects';
return this.aggregates(false, key, filter);
}
/**
* @param {number} minNodeId
* @param {number} maxNodeId
* @return {function(!HeapSnapshotNode):boolean}
*/
_createNodeIdFilter(minNodeId, maxNodeId) {
/**
* @param {!HeapSnapshotNode} node
* @return {boolean}
*/
function nodeIdFilter(node) {
const id = node.id();
return id > minNodeId && id <= maxNodeId;
}
return nodeIdFilter;
}
/**
* @param {number} bottomUpAllocationNodeId
* @return {function(!HeapSnapshotNode):boolean|undefined}
*/
_createAllocationStackFilter(bottomUpAllocationNodeId) {
const traceIds = this._allocationProfile.traceIds(bottomUpAllocationNodeId);
if (!traceIds.length) {
return undefined;
}
const set = {};
for (let i = 0; i < traceIds.length; i++) {
set[traceIds[i]] = true;
}
/**
* @param {!HeapSnapshotNode} node
* @return {boolean}
*/
function traceIdFilter(node) {
return !!set[node.traceNodeId()];
}
return traceIdFilter;
}
/**
* @param {boolean} sortedIndexes
* @param {string=} key
* @param {function(!HeapSnapshotNode):boolean=} filter
* @return {!Object.<string, !Aggregate>}
*/
aggregates(sortedIndexes, key, filter) {
let aggregatesByClassName = key && this._aggregates[key];
if (!aggregatesByClassName) {
const aggregates = this._buildAggregates(filter);
this._calculateClassesRetainedSize(aggregates.aggregatesByClassIndex, filter);
aggregatesByClassName = aggregates.aggregatesByClassName;
if (key) {
this._aggregates[key] = aggregatesByClassName;
}
}
if (sortedIndexes && (!key || !this._aggregatesSortedFlags[key])) {
this._sortAggregateIndexes(aggregatesByClassName);
if (key) {
this._aggregatesSortedFlags[key] = sortedIndexes;
}
}
return aggregatesByClassName;
}
/**
* @return {!Array.<!SerializedAllocationNode>}
*/
allocationTracesTops() {
return this._allocationProfile.serializeTraceTops();
}
/**
* @param {number} nodeId
* @return {!AllocationNodeCallers}
*/
allocationNodeCallers(nodeId) {
return this._allocationProfile.serializeCallers(nodeId);
}
/**
* @param {number} nodeIndex
* @return {?Array.<!AllocationStackFrame>}
*/
allocationStack(nodeIndex) {
const node = this.createNode(nodeIndex);
const allocationNodeId = node.traceNodeId();
if (!allocationNodeId) {
return null;
}
return this._allocationProfile.serializeAllocationStack(allocationNodeId);
}
/**
* @return {!Object.<string, !AggregateForDiff>}
*/
aggregatesForDiff() {
if (this._aggregatesForDiff) {
return this._aggregatesForDiff;
}
const aggregatesByClassName = this.aggregates(true, 'allObjects');
this._aggregatesForDiff = {};
const node = this.createNode();
for (const className in aggregatesByClassName) {
const aggregate = aggregatesByClassName[className];
const indexes = aggregate.idxs;
const ids = new Array(indexes.length);
const selfSizes = new Array(indexes.length);
for (let i = 0; i < indexes.length; i++) {
node.nodeIndex = indexes[i];
ids[i] = node.id();
selfSizes[i] = node.selfSize();
}
this._aggregatesForDiff[className] = {indexes: indexes, ids: ids, selfSizes: selfSizes};
}
return this._aggregatesForDiff;
}
/**
* @protected
* @param {!HeapSnapshotNode} node
* @return {boolean}
*/
isUserRoot(node) {
return true;
}
/**
* @param {function(!HeapSnapshotNode,!HeapSnapshotEdge):boolean=} filter
*/
calculateDistances(filter) {
const nodeCount = this.nodeCount;
const distances = this._nodeDistances;
const noDistance = this._noDistance;
for (let i = 0; i < nodeCount; ++i) {
distances[i] = noDistance;
}
const nodesToVisit = new Uint32Array(this.nodeCount);
let nodesToVisitLength = 0;
// BFS for user root objects.
for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) {
const node = iter.edge.node();
if (this.isUserRoot(node)) {
distances[node.ordinal()] = 1;
nodesToVisit[nodesToVisitLength++] = node.nodeIndex;
}
}
this._bfs(nodesToVisit, nodesToVisitLength, distances, filter);
// BFS for objects not reached from user roots.
distances[this.rootNode().ordinal()] =
nodesToVisitLength > 0 ? baseSystemDistance : 0;
nodesToVisit[0] = this.rootNode().nodeIndex;
nodesToVisitLength = 1;
this._bfs(nodesToVisit, nodesToVisitLength, distances, filter);
}
/**
* @param {!Uint32Array} nodesToVisit
* @param {number} nodesToVisitLength
* @param {!Int32Array} distances
* @param {function(!HeapSnapshotNode,!HeapSnapshotEdge):boolean=} filter
*/
_bfs(nodesToVisit, nodesToVisitLength, distances, filter) {
// Preload fields into local variables for better performance.
const edgeFieldsCount = this._edgeFieldsCount;
const nodeFieldCount = this._nodeFieldCount;
const containmentEdges = this.containmentEdges;
const firstEdgeIndexes = this._firstEdgeIndexes;
const edgeToNodeOffset = this._edgeToNodeOffset;
const edgeTypeOffset = this._edgeTypeOffset;
const nodeCount = this.nodeCount;
const edgeWeakType = this._edgeWeakType;
const noDistance = this._noDistance;
let index = 0;
const edge = this.createEdge(0);
const node = this.createNode(0);
while (index < nodesToVisitLength) {
const nodeIndex = nodesToVisit[index++]; // shift generates too much garbage.
const nodeOrdinal = nodeIndex / nodeFieldCount;
const distance = distances[nodeOrdinal] + 1;
const firstEdgeIndex = firstEdgeIndexes[nodeOrdinal];
const edgesEnd = firstEdgeIndexes[nodeOrdinal + 1];
node.nodeIndex = nodeIndex;
for (let edgeIndex = firstEdgeIndex; edgeIndex < edgesEnd; edgeIndex += edgeFieldsCount) {
const edgeType = containmentEdges[edgeIndex + edgeTypeOffset];
if (edgeType === edgeWeakType) {
continue;
}
const childNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
const childNodeOrdinal = childNodeIndex / nodeFieldCount;
if (distances[childNodeOrdinal] !== noDistance) {
continue;
}
edge.edgeIndex = edgeIndex;
if (filter && !filter(node, edge)) {
continue;
}
distances[childNodeOrdinal] = distance;
nodesToVisit[nodesToVisitLength++] = childNodeIndex;
}
}
if (nodesToVisitLength > nodeCount) {
throw new Error(
'BFS failed. Nodes to visit (' + nodesToVisitLength + ') is more than nodes count (' + nodeCount + ')');
}
}
/**
* @param {function(!HeapSnapshotNode):boolean=} filter
* @return {!{aggregatesByClassName: !Object<string, !AggregatedInfo>,
* aggregatesByClassIndex: !Object<number, !AggregatedInfo>}}
*/
_buildAggregates(filter) {
const aggregates = {};
const aggregatesByClassName = {};
const classIndexes = [];
const nodes = this.nodes;
const nodesLength = nodes.length;
const nodeNativeType = this._nodeNativeType;
const nodeFieldCount = this._nodeFieldCount;
const selfSizeOffset = this._nodeSelfSizeOffset;
const nodeTypeOffset = this._nodeTypeOffset;
const node = this.rootNode();
const nodeDistances = this._nodeDistances;
for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) {
node.nodeIndex = nodeIndex;
if (filter && !filter(node)) {
continue;
}
const selfSize = nodes[nodeIndex + selfSizeOffset];
if (!selfSize && nodes[nodeIndex + nodeTypeOffset] !== nodeNativeType) {
continue;
}
const classIndex = node.classIndex();
const nodeOrdinal = nodeIndex / nodeFieldCount;
const distance = nodeDistances[nodeOrdinal];
if (!(classIndex in aggregates)) {
const nodeType = node.type();
const nameMatters = nodeType === 'object' || nodeType === 'native';
const value = {
count: 1,
distance: distance,
self: selfSize,
maxRet: 0,
type: nodeType,
name: nameMatters ? node.name() : null,
idxs: [nodeIndex]
};
aggregates[classIndex] = value;
classIndexes.push(classIndex);
aggregatesByClassName[node.className()] = value;
} else {
const clss = aggregates[classIndex];
clss.distance = Math.min(clss.distance, distance);
++clss.count;
clss.self += selfSize;
clss.idxs.push(nodeIndex);
}
}
// Shave off provisionally allocated space.
for (let i = 0, l = classIndexes.length; i < l; ++i) {
const classIndex = classIndexes[i];
aggregates[classIndex].idxs = aggregates[classIndex].idxs.slice();
}
return {aggregatesByClassName: aggregatesByClassName, aggregatesByClassIndex: aggregates};
}
/**
* @param {!Object<number, !AggregatedInfo>} aggregates
* @param {function(!HeapSnapshotNode):boolean=} filter
*/
_calculateClassesRetainedSize(aggregates, filter) {
const rootNodeIndex = this._rootNodeIndex;
const node = this.createNode(rootNodeIndex);
const list = [rootNodeIndex];
const sizes = [-1];
const classes = [];
const seenClassNameIndexes = {};
const nodeFieldCount = this._nodeFieldCount;
const nodeTypeOffset = this._nodeTypeOffset;
const nodeNativeType = this._nodeNativeType;
const dominatedNodes = this._dominatedNodes;
const nodes = this.nodes;
const firstDominatedNodeIndex = this._firstDominatedNodeIndex;
while (list.length) {
const nodeIndex = list.pop();
node.nodeIndex = nodeIndex;
let classIndex = node.classIndex();
const seen = !!seenClassNameIndexes[classIndex];
const nodeOrdinal = nodeIndex / nodeFieldCount;
const dominatedIndexFrom = firstDominatedNodeIndex[nodeOrdinal];
const dominatedIndexTo = firstDominatedNodeIndex[nodeOrdinal + 1];
if (!seen && (!filter || filter(node)) &&
(node.selfSize() || nodes[nodeIndex + nodeTypeOffset] === nodeNativeType)) {
aggregates[classIndex].maxRet += node.retainedSize();
if (dominatedIndexFrom !== dominatedIndexTo) {
seenClassNameIndexes[classIndex] = true;
sizes.push(list.length);
classes.push(classIndex);
}
}
for (let i = dominatedIndexFrom; i < dominatedIndexTo; i++) {
list.push(dominatedNodes[i]);
}
const l = list.length;
while (sizes[sizes.length - 1] === l) {
sizes.pop();
classIndex = classes.pop();
seenClassNameIndexes[classIndex] = false;
}
}
}
/**
* @param {!{aggregatesByClassName: !Object<string, !AggregatedInfo>, aggregatesByClassIndex: !Object<number, !AggregatedInfo>}} aggregates
*/
_sortAggregateIndexes(aggregates) {
const nodeA = this.createNode();
const nodeB = this.createNode();
for (const clss in aggregates) {
aggregates[clss].idxs.sort((idxA, idxB) => {
nodeA.nodeIndex = idxA;
nodeB.nodeIndex = idxB;
return nodeA.id() < nodeB.id() ? -1 : 1;
});
}
}
/**
* The function checks is the edge should be considered during building
* postorder iterator and dominator tree.
*
* @param {number} nodeIndex
* @param {number} edgeType
* @return {boolean}
*/
_isEssentialEdge(nodeIndex, edgeType) {
// Shortcuts at the root node have special meaning of marking user global objects.
return edgeType !== this._edgeWeakType &&
(edgeType !== this._edgeShortcutType || nodeIndex === this._rootNodeIndex);
}
_buildPostOrderIndex() {
const nodeFieldCount = this._nodeFieldCount;
const nodeCount = this.nodeCount;
const rootNodeOrdinal = this._rootNodeIndex / nodeFieldCount;
const edgeFieldsCount = this._edgeFieldsCount;
const edgeTypeOffset = this._edgeTypeOffset;
const edgeToNodeOffset = this._edgeToNodeOffset;
const firstEdgeIndexes = this._firstEdgeIndexes;
const containmentEdges = this.containmentEdges;
const mapAndFlag = this.userObjectsMapAndFlag();
const flags = mapAndFlag ? mapAndFlag.map : null;
const flag = mapAndFlag ? mapAndFlag.flag : 0;
const stackNodes = new Uint32Array(nodeCount);
const stackCurrentEdge = new Uint32Array(nodeCount);
const postOrderIndex2NodeOrdinal = new Uint32Array(nodeCount);
const nodeOrdinal2PostOrderIndex = new Uint32Array(nodeCount);
const visited = new Uint8Array(nodeCount);
let postOrderIndex = 0;
let stackTop = 0;
stackNodes[0] = rootNodeOrdinal;
stackCurrentEdge[0] = firstEdgeIndexes[rootNodeOrdinal];
visited[rootNodeOrdinal] = 1;
let iteration = 0;
while (true) {
++iteration;
while (stackTop >= 0) {
const nodeOrdinal = stackNodes[stackTop];
const edgeIndex = stackCurrentEdge[stackTop];
const edgesEnd = firstEdgeIndexes[nodeOrdinal + 1];
if (edgeIndex < edgesEnd) {
stackCurrentEdge[stackTop] += edgeFieldsCount;
const edgeType = containmentEdges[edgeIndex + edgeTypeOffset];
if (!this._isEssentialEdge(nodeOrdinal * nodeFieldCount, edgeType)) {
continue;
}
const childNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
const childNodeOrdinal = childNodeIndex / nodeFieldCount;
if (visited[childNodeOrdinal]) {
continue;
}
const nodeFlag = !flags || (flags[nodeOrdinal] & flag);
const childNodeFlag = !flags || (flags[childNodeOrdinal] & flag);
// We are skipping the edges from non-page-owned nodes to page-owned nodes.
// Otherwise the dominators for the objects that also were retained by debugger would be affected.
if (nodeOrdinal !== rootNodeOrdinal && childNodeFlag && !nodeFlag) {
continue;
}
++stackTop;
stackNodes[stackTop] = childNodeOrdinal;
stackCurrentEdge[stackTop] = firstEdgeIndexes[childNodeOrdinal];
visited[childNodeOrdinal] = 1;
} else {
// Done with all the node children
nodeOrdinal2PostOrderIndex[nodeOrdinal] = postOrderIndex;
postOrderIndex2NodeOrdinal[postOrderIndex++] = nodeOrdinal;
--stackTop;
}
}
if (postOrderIndex === nodeCount || iteration > 1) {
break;
}
const errors = new HeapSnapshotProblemReport(`Heap snapshot: ${
nodeCount - postOrderIndex} nodes are unreachable from the root. Following nodes have only weak retainers:`);
const dumpNode = this.rootNode();
// Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is
// visited after all orphan nodes and their subgraphs.
--postOrderIndex;
stackTop = 0;
stackNodes[0] = rootNodeOrdinal;
stackCurrentEdge[0] = firstEdgeIndexes[rootNodeOrdinal + 1]; // no need to reiterate its edges
for (let i = 0; i < nodeCount; ++i) {
if (visited[i] || !this._hasOnlyWeakRetainers(i)) {
continue;
}
// Add all nodes that have only weak retainers to traverse their subgraphs.
stackNodes[++stackTop] = i;
stackCurrentEdge[stackTop] = firstEdgeIndexes[i];
visited[i] = 1;
dumpNode.nodeIndex = i * nodeFieldCount;
const retainers = [];
for (let it = dumpNode.retainers(); it.hasNext(); it.next()) {
retainers.push(`${it.item().node().name()}@${it.item().node().id()}.${it.item().name()}`);
}
errors.addError(`${dumpNode.name()} @${dumpNode.id()} weak retainers: ${retainers.join(', ')}`);
}
console.warn(errors.toString());
}
// If we already processed all orphan nodes that have only weak retainers and still have some orphans...
if (postOrderIndex !== nodeCount) {
const errors = new HeapSnapshotProblemReport(
'Still found ' + (nodeCount - postOrderIndex) + ' unreachable nodes in heap snapshot:');
const dumpNode = this.rootNode();
// Remove root from the result (last node in the array) and put it at the bottom of the stack so that it is
// visited after all orphan nodes and their subgraphs.
--postOrderIndex;
for (let i = 0; i < nodeCount; ++i) {
if (visited[i]) {
continue;
}
dumpNode.nodeIndex = i * nodeFieldCount;
errors.addError(dumpNode.name() + ' @' + dumpNode.id());
// Fix it by giving the node a postorder index anyway.
nodeOrdinal2PostOrderIndex[i] = postOrderIndex;
postOrderIndex2NodeOrdinal[postOrderIndex++] = i;
}
nodeOrdinal2PostOrderIndex[rootNodeOrdinal] = postOrderIndex;
postOrderIndex2NodeOrdinal[postOrderIndex++] = rootNodeOrdinal;
console.warn(errors.toString());
}
return {
postOrderIndex2NodeOrdinal: postOrderIndex2NodeOrdinal,
nodeOrdinal2PostOrderIndex: nodeOrdinal2PostOrderIndex
};
}
/**
* @param {number} nodeOrdinal
* @return {boolean}
*/
_hasOnlyWeakRetainers(nodeOrdinal) {
const edgeTypeOffset = this._edgeTypeOffset;
const edgeWeakType = this._edgeWeakType;
const edgeShortcutType = this._edgeShortcutType;
const containmentEdges = this.containmentEdges;
const retainingEdges = this._retainingEdges;
const beginRetainerIndex = this._firstRetainerIndex[nodeOrdinal];
const endRetainerIndex = this._firstRetainerIndex[nodeOrdinal + 1];
for (let retainerIndex = beginRetainerIndex; retainerIndex < endRetainerIndex; ++retainerIndex) {
const retainerEdgeIndex = retainingEdges[retainerIndex];
const retainerEdgeType = containmentEdges[retainerEdgeIndex + edgeTypeOffset];
if (retainerEdgeType !== edgeWeakType && retainerEdgeType !== edgeShortcutType) {
return false;
}
}
return true;
}
// The algorithm is based on the article:
// K. Cooper, T. Harvey and K. Kennedy "A Simple, Fast Dominance Algorithm"
// Softw. Pract. Exper. 4 (2001), pp. 1-10.
/**
* @param {!Array.<number>} postOrderIndex2NodeOrdinal
* @param {!Array.<number>} nodeOrdinal2PostOrderIndex
*/
_buildDominatorTree(postOrderIndex2NodeOrdinal, nodeOrdinal2PostOrderIndex) {
const nodeFieldCount = this._nodeFieldCount;
const firstRetainerIndex = this._firstRetainerIndex;
const retainingNodes = this._retainingNodes;
const retainingEdges = this._retainingEdges;
const edgeFieldsCount = this._edgeFieldsCount;
const edgeTypeOffset = this._edgeTypeOffset;
const edgeToNodeOffset = this._edgeToNodeOffset;
const firstEdgeIndexes = this._firstEdgeIndexes;
const containmentEdges = this.containmentEdges;
const rootNodeIndex = this._rootNodeIndex;
const mapAndFlag = this.userObjectsMapAndFlag();
const flags = mapAndFlag ? mapAndFlag.map : null;
const flag = mapAndFlag ? mapAndFlag.flag : 0;
const nodesCount = postOrderIndex2NodeOrdinal.length;
const rootPostOrderedIndex = nodesCount - 1;
const noEntry = nodesCount;
const dominators = new Uint32Array(nodesCount);
for (let i = 0; i < rootPostOrderedIndex; ++i) {
dominators[i] = noEntry;
}
dominators[rootPostOrderedIndex] = rootPostOrderedIndex;
// The affected array is used to mark entries which dominators
// have to be racalculated because of changes in their retainers.
const affected = new Uint8Array(nodesCount);
let nodeOrdinal;
{ // Mark the root direct children as affected.
nodeOrdinal = this._rootNodeIndex / nodeFieldCount;
const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1];
for (let edgeIndex = firstEdgeIndexes[nodeOrdinal]; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) {
const edgeType = containmentEdges[edgeIndex + edgeTypeOffset];
if (!this._isEssentialEdge(this._rootNodeIndex, edgeType)) {
continue;
}
const childNodeOrdinal = containmentEdges[edgeIndex + edgeToNodeOffset] / nodeFieldCount;
affected[nodeOrdinal2PostOrderIndex[childNodeOrdinal]] = 1;
}
}
let changed = true;
while (changed) {
changed = false;
for (let postOrderIndex = rootPostOrderedIndex - 1; postOrderIndex >= 0; --postOrderIndex) {
if (affected[postOrderIndex] === 0) {
continue;
}
affected[postOrderIndex] = 0;
// If dominator of the entry has already been set to root,
// then it can't propagate any further.
if (dominators[postOrderIndex] === rootPostOrderedIndex) {
continue;
}
nodeOrdinal = postOrderIndex2NodeOrdinal[postOrderIndex];
const nodeFlag = !flags || (flags[nodeOrdinal] & flag);
let newDominatorIndex = noEntry;
const beginRetainerIndex = firstRetainerIndex[nodeOrdinal];
const endRetainerIndex = firstRetainerIndex[nodeOrdinal + 1];
let orphanNode = true;
for (let retainerIndex = beginRetainerIndex; retainerIndex < endRetainerIndex; ++retainerIndex) {
const retainerEdgeIndex = retainingEdges[retainerIndex];
const retainerEdgeType = containmentEdges[retainerEdgeIndex + edgeTypeOffset];
const retainerNodeIndex = retainingNodes[retainerIndex];
if (!this._isEssentialEdge(retainerNodeIndex, retainerEdgeType)) {
continue;
}
orphanNode = false;
const retainerNodeOrdinal = retainerNodeIndex / nodeFieldCount;
const retainerNodeFlag = !flags || (flags[retainerNodeOrdinal] & flag);
// We are skipping the edges from non-page-owned nodes to page-owned nodes.
// Otherwise the dominators for the objects that also were retained by debugger would be affected.
if (retainerNodeIndex !== rootNodeIndex && nodeFlag && !retainerNodeFlag) {
continue;
}
let retanerPostOrderIndex = nodeOrdinal2PostOrderIndex[retainerNodeOrdinal];
if (dominators[retanerPostOrderIndex] !== noEntry) {
if (newDominatorIndex === noEntry) {
newDominatorIndex = retanerPostOrderIndex;
} else {
while (retanerPostOrderIndex !== newDominatorIndex) {
while (retanerPostOrderIndex < newDominatorIndex) {
retanerPostOrderIndex = dominators[retanerPostOrderIndex];
}
while (newDominatorIndex < retanerPostOrderIndex) {
newDominatorIndex = dominators[newDominatorIndex];
}
}
}
// If idom has already reached the root, it doesn't make sense
// to check other retainers.
if (newDominatorIndex === rootPostOrderedIndex) {
break;
}
}
}
// Make root dominator of orphans.
if (orphanNode) {
newDominatorIndex = rootPostOrderedIndex;
}
if (newDominatorIndex !== noEntry && dominators[postOrderIndex] !== newDominatorIndex) {
dominators[postOrderIndex] = newDominatorIndex;
changed = true;
nodeOrdinal = postOrderIndex2NodeOrdinal[postOrderIndex];
const beginEdgeToNodeFieldIndex = firstEdgeIndexes[nodeOrdinal] + edgeToNodeOffset;
const endEdgeToNodeFieldIndex = firstEdgeIndexes[nodeOrdinal + 1];
for (let toNodeFieldIndex = beginEdgeToNodeFieldIndex; toNodeFieldIndex < endEdgeToNodeFieldIndex;
toNodeFieldIndex += edgeFieldsCount) {
const childNodeOrdinal = containmentEdges[toNodeFieldIndex] / nodeFieldCount;
affected[nodeOrdinal2PostOrderIndex[childNodeOrdinal]] = 1;
}
}
}
}
const dominatorsTree = new Uint32Array(nodesCount);
for (let postOrderIndex = 0, l = dominators.length; postOrderIndex < l; ++postOrderIndex) {
nodeOrdinal = postOrderIndex2NodeOrdinal[postOrderIndex];
dominatorsTree[nodeOrdinal] = postOrderIndex2NodeOrdinal[dominators[postOrderIndex]];
}
return dominatorsTree;
}
/**
* @param {!Array<number>} postOrderIndex2NodeOrdinal
*/
_calculateRetainedSizes(postOrderIndex2NodeOrdinal) {
const nodeCount = this.nodeCount;
const nodes = this.nodes;
const nodeSelfSizeOffset = this._nodeSelfSizeOffset;
const nodeFieldCount = this._nodeFieldCount;
const dominatorsTree = this._dominatorsTree;
const retainedSizes = this._retainedSizes;
for (let nodeOrdinal = 0; nodeOrdinal < nodeCount; ++nodeOrdinal) {
retainedSizes[nodeOrdinal] = nodes[nodeOrdinal * nodeFieldCount + nodeSelfSizeOffset];
}
// Propagate retained sizes for each node excluding root.
for (let postOrderIndex = 0; postOrderIndex < nodeCount - 1; ++postOrderIndex) {
const nodeOrdinal = postOrderIndex2NodeOrdinal[postOrderIndex];
const dominatorOrdinal = dominatorsTree[nodeOrdinal];
retainedSizes[dominatorOrdinal] += retainedSizes[nodeOrdinal];
}
}
_buildDominatedNodes() {
// Builds up two arrays:
// - "dominatedNodes" is a continuous array, where each node owns an
// interval (can be empty) with corresponding dominated nodes.
// - "indexArray" is an array of indexes in the "dominatedNodes"
// with the same positions as in the _nodeIndex.
const indexArray = this._firstDominatedNodeIndex;
// All nodes except the root have dominators.
const dominatedNodes = this._dominatedNodes;
// Count the number of dominated nodes for each node. Skip the root (node at
// index 0) as it is the only node that dominates itself.
const nodeFieldCount = this._nodeFieldCount;
const dominatorsTree = this._dominatorsTree;
let fromNodeOrdinal = 0;
let toNodeOrdinal = this.nodeCount;
const rootNodeOrdinal = this._rootNodeIndex / nodeFieldCount;
if (rootNodeOrdinal === fromNodeOrdinal) {
fromNodeOrdinal = 1;
} else if (rootNodeOrdinal === toNodeOrdinal - 1) {
toNodeOrdinal = toNodeOrdinal - 1;
} else {
throw new Error('Root node is expected to be either first or last');
}
for (let nodeOrdinal = fromNodeOrdinal; nodeOrdinal < toNodeOrdinal; ++nodeOrdinal) {
++indexArray[dominatorsTree[nodeOrdinal]];
}
// Put in the first slot of each dominatedNodes slice the count of entries
// that will be filled.
let firstDominatedNodeIndex = 0;
for (let i = 0, l = this.nodeCount; i < l; ++i) {
const dominatedCount = dominatedNodes[firstDominatedNodeIndex] = indexArray[i];
indexArray[i] = firstDominatedNodeIndex;
firstDominatedNodeIndex += dominatedCount;
}
indexArray[this.nodeCount] = dominatedNodes.length;
// Fill up the dominatedNodes array with indexes of dominated nodes. Skip the root (node at
// index 0) as it is the only node that dominates itself.
for (let nodeOrdinal = fromNodeOrdinal; nodeOrdinal < toNodeOrdinal; ++nodeOrdinal) {
const dominatorOrdinal = dominatorsTree[nodeOrdinal];
let dominatedRefIndex = indexArray[dominatorOrdinal];
dominatedRefIndex += (--dominatedNodes[dominatedRefIndex]);
dominatedNodes[dominatedRefIndex] = nodeOrdinal * nodeFieldCount;
}
}
_buildSamples() {
const samples = this._rawSamples;
if (!samples || !samples.length) {
return;
}
const sampleCount = samples.length / 2;
const sizeForRange = new Array(sampleCount);
const timestamps = new Array(sampleCount);
const lastAssignedIds = new Array(sampleCount);
const timestampOffset = this._metaNode.sample_fields.indexOf('timestamp_us');
const lastAssignedIdOffset = this._metaNode.sample_fields.indexOf('last_assigned_id');
for (let i = 0; i < sampleCount; i++) {
sizeForRange[i] = 0;
timestamps[i] = (samples[2 * i + timestampOffset]) / 1000;
lastAssignedIds[i] = samples[2 * i + lastAssignedIdOffset];
}
const nodes = this.nodes;
const nodesLength = nodes.length;
const nodeFieldCount = this._nodeFieldCount;
const node = this.rootNode();
for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) {
node.nodeIndex = nodeIndex;
const nodeId = node.id();
// JS objects have odd ids, skip native objects.
if (nodeId % 2 === 0) {
continue;
}
const rangeIndex = lastAssignedIds.lowerBound(nodeId);
if (rangeIndex === sampleCount) {
// TODO: make heap profiler not allocate while taking snapshot
continue;
}
sizeForRange[rangeIndex] += node.selfSize();
}
this._samples = new Samples(timestamps, lastAssignedIds, sizeForRange);
}
_buildLocationMap() {
/** @type {!Map<number, !Location>} */
const map = new Map();
const locations = this._locations;
for (let i = 0; i < locations.length; i += this._locationFieldCount) {
const nodeIndex = locations[i + this._locationIndexOffset];
const scriptId = locations[i + this._locationScriptIdOffset];
const line = locations[i + this._locationLineOffset];
const col = locations[i + this._locationColumnOffset];
map.set(nodeIndex, new Location(scriptId, line, col));
}
this._locationMap = map;
}
/**
* @param {number} nodeIndex
* @return {?Location}
*/
getLocation(nodeIndex) {
return this._locationMap.get(nodeIndex) || null;
}
/**
* @return {?Samples}
*/
getSamples() {
return this._samples;
}
/**
* @protected
*/
calculateFlags() {
throw new Error('Not implemented');
}
/**
* @protected
*/
calculateStatistics() {
throw new Error('Not implemented');
}
userObjectsMapAndFlag() {
throw new Error('Not implemented');
}
/**
* @param {string} baseSnapshotId
* @param {!Object.<string, !AggregateForDiff>} baseSnapshotAggregates
* @return {!Object.<string, !Diff>}
*/
calculateSnapshotDiff(baseSnapshotId, baseSnapshotAggregates) {
let snapshotDiff = this._snapshotDiffs[baseSnapshotId];
if (snapshotDiff) {
return snapshotDiff;
}
snapshotDiff = {};
const aggregates = this.aggregates(true, 'allObjects');
for (const className in baseSnapshotAggregates) {
const baseAggregate = baseSnapshotAggregates[className];
const diff = this._calculateDiffForClass(baseAggregate, aggregates[className]);
if (diff) {
snapshotDiff[className] = diff;
}
}
const emptyBaseAggregate = new AggregateForDiff();
for (const className in aggregates) {
if (className in baseSnapshotAggregates) {
continue;
}
snapshotDiff[className] = this._calculateDiffForClass(emptyBaseAggregate, aggregates[className]);
}
this._snapshotDiffs[baseSnapshotId] = snapshotDiff;
return snapshotDiff;
}
/**
* @param {!AggregateForDiff} baseAggregate
* @param {!Aggregate} aggregate
* @return {?Diff}
*/
_calculateDiffForClass(baseAggregate, aggregate) {
const baseIds = baseAggregate.ids;
const baseIndexes = baseAggregate.indexes;
const baseSelfSizes = baseAggregate.selfSizes;
const indexes = aggregate ? aggregate.idxs : [];
let i = 0;
let j = 0;
const l = baseIds.length;
const m = indexes.length;
const diff = new Diff();
const nodeB = this.createNode(indexes[j]);
while (i < l && j < m) {
const nodeAId = baseIds[i];
if (nodeAId < nodeB.id()) {
diff.deletedIndexes.push(baseIndexes[i]);
diff.removedCount++;
diff.removedSize += baseSelfSizes[i];
++i;
} else if (
nodeAId >
nodeB.id()) { // Native nodes(e.g. dom groups) may have ids less than max JS object id in the base snapshot
diff.addedIndexes.push(indexes[j]);
diff.addedCount++;
diff.addedSize += nodeB.selfSize();
nodeB.nodeIndex = indexes[++j];
} else { // nodeAId === nodeB.id()
++i;
nodeB.nodeIndex = indexes[++j];
}
}
while (i < l) {
diff.deletedIndexes.push(baseIndexes[i]);
diff.removedCount++;
diff.removedSize += baseSelfSizes[i];
++i;
}
while (j < m) {
diff.addedIndexes.push(indexes[j]);
diff.addedCount++;
diff.addedSize += nodeB.selfSize();
nodeB.nodeIndex = indexes[++j];
}
diff.countDelta = diff.addedCount - diff.removedCount;
diff.sizeDelta = diff.addedSize - diff.removedSize;
if (!diff.addedCount && !diff.removedCount) {
return null;
}
return diff;
}
_nodeForSnapshotObjectId(snapshotObjectId) {
for (let it = this._allNodes(); it.hasNext(); it.next()) {
if (it.node.id() === snapshotObjectId) {
return it.node;
}
}
return null;
}
/**
* @param {string} snapshotObjectId
* @return {?string}
*/
nodeClassName(snapshotObjectId) {
const node = this._nodeForSnapshotObjectId(snapshotObjectId);
if (node) {
return node.className();
}
return null;
}
/**
* @param {string} name
* @return {!Array.<number>}
*/
idsOfObjectsWithName(name) {
const ids = [];
for (let it = this._allNodes(); it.hasNext(); it.next()) {
if (it.item().name() === name) {
ids.push(it.item().id());
}
}
return ids;
}
/**
* @param {number} nodeIndex
* @return {!HeapSnapshotEdgesProvider}
*/
createEdgesProvider(nodeIndex) {
const node = this.createNode(nodeIndex);
const filter = this.containmentEdgesFilter();
const indexProvider = new HeapSnapshotEdgeIndexProvider(this);
return new HeapSnapshotEdgesProvider(this, filter, node.edges(), indexProvider);
}
/**
* @param {number} nodeIndex
* @param {?function(!HeapSnapshotEdge):boolean} filter
* @return {!HeapSnapshotEdgesProvider}
*/
createEdgesProviderForTest(nodeIndex, filter) {
const node = this.createNode(nodeIndex);
const indexProvider = new HeapSnapshotEdgeIndexProvider(this);
return new HeapSnapshotEdgesProvider(this, filter, node.edges(), indexProvider);
}
/**
* @return {?function(!HeapSnapshotEdge):boolean}
*/
retainingEdgesFilter() {
return null;
}
/**
* @return {?function(!HeapSnapshotEdge):boolean}
*/
containmentEdgesFilter() {
return null;
}
/**
* @param {number} nodeIndex
* @return {!HeapSnapshotEdgesProvider}
*/
createRetainingEdgesProvider(nodeIndex) {
const node = this.createNode(nodeIndex);
const filter = this.retainingEdgesFilter();
const indexProvider = new HeapSnapshotRetainerEdgeIndexProvider(this);
return new HeapSnapshotEdgesProvider(this, filter, node.retainers(), indexProvider);
}
/**
* @param {string} baseSnapshotId
* @param {string} className
* @return {!HeapSnapshotNodesProvider}
*/
createAddedNodesProvider(baseSnapshotId, className) {
const snapshotDiff = this._snapshotDiffs[baseSnapshotId];
const diffForClass = snapshotDiff[className];
return new HeapSnapshotNodesProvider(this, diffForClass.addedIndexes);
}
/**
* @param {!Array.<number>} nodeIndexes
* @return {!HeapSnapshotNodesProvider}
*/
createDeletedNodesProvider(nodeIndexes) {
return new HeapSnapshotNodesProvider(this, nodeIndexes);
}
/**
* @param {string} className
* @param {!NodeFilter} nodeFilter
* @return {!HeapSnapshotNodesProvider}
*/
createNodesProviderForClass(className, nodeFilter) {
return new HeapSnapshotNodesProvider(this, this.aggregatesWithFilter(nodeFilter)[className].idxs);
}
/**
* @return {number}
*/
_maxJsNodeId() {
const nodeFieldCount = this._nodeFieldCount;
const nodes = this.nodes;
const nodesLength = nodes.length;
let id = 0;
for (let nodeIndex = this._nodeIdOffset; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) {
const nextId = nodes[nodeIndex];
// JS objects have odd ids, skip native objects.
if (nextId % 2 === 0) {
continue;
}
if (id < nextId) {
id = nextId;
}
}
return id;
}
/**
* @return {!StaticData}
*/
updateStaticData() {
return new StaticData(
this.nodeCount, this._rootNodeIndex, this.totalSize, this._maxJsNodeId());
}
}
/**
* @unrestricted
*/
class HeapSnapshotItemProvider {
/**
* @param {!HeapSnapshotItemIterator} iterator
* @param {!HeapSnapshotItemIndexProvider} indexProvider
*/
constructor(iterator, indexProvider) {
this._iterator = iterator;
this._indexProvider = indexProvider;
this._isEmpty = !iterator.hasNext();
/** @type {?Array.<number>} */
this._iterationOrder = null;
this._currentComparator = null;
this._sortedPrefixLength = 0;
this._sortedSuffixLength = 0;
}
_createIterationOrder() {
if (this._iterationOrder) {
return;
}
this._iterationOrder = [];
for (let iterator = this._iterator; iterator.hasNext(); iterator.next()) {
this._iterationOrder.push(iterator.item().itemIndex());
}
}
/**
* @return {boolean}
*/
isEmpty() {
return this._isEmpty;
}
/**
* @param {number} begin
* @param {number} end
* @return {!ItemsRange}
*/
serializeItemsRange(begin, end) {
this._createIterationOrder();
if (begin > end) {
throw new Error('Start position > end position: ' + begin + ' > ' + end);
}
if (end > this._iterationOrder.length) {
end = this._iterationOrder.length;
}
if (this._sortedPrefixLength < end && begin < this._iterationOrder.length - this._sortedSuffixLength) {
this.sort(
this._currentComparator, this._sortedPrefixLength, this._iterationOrder.length - 1 - this._sortedSuffixLength,
begin, end - 1);
if (begin <= this._sortedPrefixLength) {
this._sortedPrefixLength = end;
}
if (end >= this._iterationOrder.length - this._sortedSuffixLength) {
this._sortedSuffixLength = this._iterationOrder.length - begin;
}
}
let position = begin;
const count = end - begin;
const result = new Array(count);
for (let i = 0; i < count; ++i) {
const itemIndex = this._iterationOrder[position++];
const item = this._indexProvider.itemForIndex(itemIndex);
result[i] = item.serialize();
}
return new ItemsRange(begin, end, this._iterationOrder.length, result);
}
sortAndRewind(comparator) {
this._currentComparator = comparator;
this._sortedPrefixLength = 0;
this._sortedSuffixLength = 0;
}
}
/**
* @unrestricted
*/
class HeapSnapshotEdgesProvider extends HeapSnapshotItemProvider {
/**
* @param {!HeapSnapshot} snapshot
* @param {?function(!HeapSnapshotEdge):boolean} filter
* @param {!HeapSnapshotEdgeIterator} edgesIter
* @param {!HeapSnapshotItemIndexProvider} indexProvider
*/
constructor(snapshot, filter, edgesIter, indexProvider) {
const iter = filter ?
new HeapSnapshotFilteredIterator(edgesIter, /** @type {function(!HeapSnapshotItem):boolean} */ (filter)) :
edgesIter;
super(iter, indexProvider);
this.snapshot = snapshot;
}
/**
* @param {!ComparatorConfig} comparator
* @param {number} leftBound
* @param {number} rightBound
* @param {number} windowLeft
* @param {number} windowRight
*/
sort(comparator, leftBound, rightBound, windowLeft, windowRight) {
const fieldName1 = comparator.fieldName1;
const fieldName2 = comparator.fieldName2;
const ascending1 = comparator.ascending1;
const ascending2 = comparator.ascending2;
const edgeA = /** @type {!HeapSnapshotEdge} */ (this._iterator.item()).clone();
const edgeB = edgeA.clone();
const nodeA = this.snapshot.createNode();
const nodeB = this.snapshot.createNode();
function compareEdgeFieldName(ascending, indexA, indexB) {
edgeA.edgeIndex = indexA;
edgeB.edgeIndex = indexB;
if (edgeB.name() === '__proto__') {
return -1;
}
if (edgeA.name() === '__proto__') {
return 1;
}
const result = edgeA.hasStringName() === edgeB.hasStringName() ?
(edgeA.name() < edgeB.name() ? -1 : (edgeA.name() > edgeB.name() ? 1 : 0)) :
(edgeA.hasStringName() ? -1 : 1);
return ascending ? result : -result;
}
function compareNodeField(fieldName, ascending, indexA, indexB) {
edgeA.edgeIndex = indexA;
nodeA.nodeIndex = edgeA.nodeIndex();
const valueA = nodeA[fieldName]();
edgeB.edgeIndex = indexB;
nodeB.nodeIndex = edgeB.nodeIndex();
const valueB = nodeB[fieldName]();
const result = valueA < valueB ? -1 : (valueA > valueB ? 1 : 0);
return ascending ? result : -result;
}
function compareEdgeAndNode(indexA, indexB) {
let result = compareEdgeFieldName(ascending1, indexA, indexB);
if (result === 0) {
result = compareNodeField(fieldName2, ascending2, indexA, indexB);
}
if (result === 0) {
return indexA - indexB;
}
return result;
}
function compareNodeAndEdge(indexA, indexB) {
let result = compareNodeField(fieldName1, ascending1, indexA, indexB);
if (result === 0) {
result = compareEdgeFieldName(ascending2, indexA, indexB);
}
if (result === 0) {
return indexA - indexB;
}
return result;
}
function compareNodeAndNode(indexA, indexB) {
let result = compareNodeField(fieldName1, ascending1, indexA, indexB);
if (result === 0) {
result = compareNodeField(fieldName2, ascending2, indexA, indexB);
}
if (result === 0) {
return indexA - indexB;
}
return result;
}
if (fieldName1 === '!edgeName') {
this._iterationOrder.sortRange(compareEdgeAndNode, leftBound, rightBound, windowLeft, windowRight);
} else if (fieldName2 === '!edgeName') {
this._iterationOrder.sortRange(compareNodeAndEdge, leftBound, rightBound, windowLeft, windowRight);
} else {
this._iterationOrder.sortRange(compareNodeAndNode, leftBound, rightBound, windowLeft, windowRight);
}
}
}
/**
* @unrestricted
*/
class HeapSnapshotNodesProvider extends HeapSnapshotItemProvider {
/**
* @param {!HeapSnapshot} snapshot
* @param {!Array<number>|!Uint32Array} nodeIndexes
*/
constructor(snapshot, nodeIndexes) {
const indexProvider = new HeapSnapshotNodeIndexProvider(snapshot);
const it = new HeapSnapshotIndexRangeIterator(indexProvider, nodeIndexes);
super(it, indexProvider);
this.snapshot = snapshot;
}
/**
* @param {string} snapshotObjectId
* @return {number}
*/
nodePosition(snapshotObjectId) {
this._createIterationOrder();
const node = this.snapshot.createNode();
let i = 0;
for (; i < this._iterationOrder.length; i++) {
node.nodeIndex = this._iterationOrder[i];
if (node.id() === snapshotObjectId) {
break;
}
}
if (i === this._iterationOrder.length) {
return -1;
}
const targetNodeIndex = this._iterationOrder[i];
let smallerCount = 0;
const compare = this._buildCompareFunction(this._currentComparator);
for (let i = 0; i < this._iterationOrder.length; i++) {
if (compare(this._iterationOrder[i], targetNodeIndex) < 0) {
++smallerCount;
}
}
return smallerCount;
}
/**
* @return {function(number,number):number}
*/
_buildCompareFunction(comparator) {
const nodeA = this.snapshot.createNode();
const nodeB = this.snapshot.createNode();
const fieldAccessor1 = nodeA[comparator.fieldName1];
const fieldAccessor2 = nodeA[comparator.fieldName2];
const ascending1 = comparator.ascending1 ? 1 : -1;
const ascending2 = comparator.ascending2 ? 1 : -1;
/**
* @param {function():*} fieldAccessor
* @param {number} ascending
* @return {number}
*/
function sortByNodeField(fieldAccessor, ascending) {
const valueA = fieldAccessor.call(nodeA);
const valueB = fieldAccessor.call(nodeB);
return valueA < valueB ? -ascending : (valueA > valueB ? ascending : 0);
}
/**
* @param {number} indexA
* @param {number} indexB
* @return {number}
*/
function sortByComparator(indexA, indexB) {
nodeA.nodeIndex = indexA;
nodeB.nodeIndex = indexB;
let result = sortByNodeField(fieldAccessor1, ascending1);
if (result === 0) {
result = sortByNodeField(fieldAccessor2, ascending2);
}
return result || indexA - indexB;
}
return sortByComparator;
}
/**
* @param {!ComparatorConfig} comparator
* @param {number} leftBound
* @param {number} rightBound
* @param {number} windowLeft
* @param {number} windowRight
*/
sort(comparator, leftBound, rightBound, windowLeft, windowRight) {
this._iterationOrder.sortRange(
this._buildCompareFunction(comparator), leftBound, rightBound, windowLeft, windowRight);
}
}
/**
* @unrestricted
*/
class JSHeapSnapshot extends HeapSnapshot {
/**
* @param {!Object} profile
* @param {!HeapSnapshotProgress} progress
*/
constructor(profile, progress) {
super(profile, progress);
this._nodeFlags = {
// bit flags
canBeQueried: 1,
detachedDOMTreeNode: 2,
pageObject: 4 // The idea is to track separately the objects owned by the page and the objects owned by debugger.
};
this._lazyStringCache = {};
this.initialize();
}
/**
* @override
* @param {number=} nodeIndex
* @return {!JSHeapSnapshotNode}
*/
createNode(nodeIndex) {
return new JSHeapSnapshotNode(this, nodeIndex === undefined ? -1 : nodeIndex);
}
/**
* @override
* @param {number} edgeIndex
* @return {!JSHeapSnapshotEdge}
*/
createEdge(edgeIndex) {
return new JSHeapSnapshotEdge(this, edgeIndex);
}
/**
* @override
* @param {number} retainerIndex
* @return {!JSHeapSnapshotRetainerEdge}
*/
createRetainingEdge(retainerIndex) {
return new JSHeapSnapshotRetainerEdge(this, retainerIndex);
}
/**
* @override
* @return {function(!HeapSnapshotEdge):boolean}
*/
containmentEdgesFilter() {
return edge => !edge.isInvisible();
}
/**
* @override
* @return {function(!HeapSnapshotEdge):boolean}
*/
retainingEdgesFilter() {
const containmentEdgesFilter = this.containmentEdgesFilter();
function filter(edge) {
return containmentEdgesFilter(edge) && !edge.node().isRoot() && !edge.isWeak();
}
return filter;
}
/**
* @override
*/
calculateFlags() {
this._flags = new Uint32Array(this.nodeCount);
this._markDetachedDOMTreeNodes();
this._markQueriableHeapObjects();
this._markPageOwnedNodes();
}
/**
* @override
*/
calculateDistances() {
/**
* @param {!HeapSnapshotNode} node
* @param {!HeapSnapshotEdge} edge
* @return {boolean}
*/
function filter(node, edge) {
if (node.isHidden()) {
return edge.name() !== 'sloppy_function_map' || node.rawName() !== 'system / NativeContext';
}
if (node.isArray()) {
// DescriptorArrays are fixed arrays used to hold instance descriptors.
// The format of the these objects is:
// [0]: Number of descriptors
// [1]: Either Smi(0) if uninitialized, or a pointer to small fixed array:
// [0]: pointer to fixed array with enum cache
// [1]: either Smi(0) or pointer to fixed array with indices
// [i*3+2]: i-th key
// [i*3+3]: i-th type
// [i*3+4]: i-th descriptor
// As long as maps may share descriptor arrays some of the descriptor
// links may not be valid for all the maps. We just skip
// all the descriptor links when calculating distances.
// For more details see http://crbug.com/413608
if (node.rawName() !== '(map descriptors)') {
return true;
}
const index = edge.name();
return index < 2 || (index % 3) !== 1;
}
return true;
}
super.calculateDistances(filter);
}
/**
* @override
* @protected
* @param {!HeapSnapshotNode} node
* @return {boolean}
*/
isUserRoot(node) {
return node.isUserRoot() || node.isDocumentDOMTreesRoot();
}
/**
* @override
* @return {?{map: !Uint32Array, flag: number}}
*/
userObjectsMapAndFlag() {
return {map: this._flags, flag: this._nodeFlags.pageObject};
}
/**
* @param {!HeapSnapshotNode} node
* @return {number}
*/
_flagsOfNode(node) {
return this._flags[node.nodeIndex / this._nodeFieldCount];
}
_markDetachedDOMTreeNodes() {
const nodes = this.nodes;
const nodesLength = nodes.length;
const nodeFieldCount = this._nodeFieldCount;
const nodeNativeType = this._nodeNativeType;
const nodeTypeOffset = this._nodeTypeOffset;
const flag = this._nodeFlags.detachedDOMTreeNode;
const node = this.rootNode();
for (let nodeIndex = 0, ordinal = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount, ordinal++) {
const nodeType = nodes[nodeIndex + nodeTypeOffset];
if (nodeType !== nodeNativeType) {
continue;
}
node.nodeIndex = nodeIndex;
if (node.name().startsWith('Detached ')) {
this._flags[ordinal] |= flag;
}
}
}
_markQueriableHeapObjects() {
// Allow runtime properties query for objects accessible from Window objects
// via regular properties, and for DOM wrappers. Trying to access random objects
// can cause a crash due to insonsistent state of internal properties of wrappers.
const flag = this._nodeFlags.canBeQueried;
const hiddenEdgeType = this._edgeHiddenType;
const internalEdgeType = this._edgeInternalType;
const invisibleEdgeType = this._edgeInvisibleType;
const weakEdgeType = this._edgeWeakType;
const edgeToNodeOffset = this._edgeToNodeOffset;
const edgeTypeOffset = this._edgeTypeOffset;
const edgeFieldsCount = this._edgeFieldsCount;
const containmentEdges = this.containmentEdges;
const nodeFieldCount = this._nodeFieldCount;
const firstEdgeIndexes = this._firstEdgeIndexes;
const flags = this._flags;
const list = [];
for (let iter = this.rootNode().edges(); iter.hasNext(); iter.next()) {
if (iter.edge.node().isUserRoot()) {
list.push(iter.edge.node().nodeIndex / nodeFieldCount);
}
}
while (list.length) {
const nodeOrdinal = list.pop();
if (flags[nodeOrdinal] & flag) {
continue;
}
flags[nodeOrdinal] |= flag;
const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal];
const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1];
for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) {
const childNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
const childNodeOrdinal = childNodeIndex / nodeFieldCount;
if (flags[childNodeOrdinal] & flag) {
continue;
}
const type = containmentEdges[edgeIndex + edgeTypeOffset];
if (type === hiddenEdgeType || type === invisibleEdgeType || type === internalEdgeType ||
type === weakEdgeType) {
continue;
}
list.push(childNodeOrdinal);
}
}
}
_markPageOwnedNodes() {
const edgeShortcutType = this._edgeShortcutType;
const edgeElementType = this._edgeElementType;
const edgeToNodeOffset = this._edgeToNodeOffset;
const edgeTypeOffset = this._edgeTypeOffset;
const edgeFieldsCount = this._edgeFieldsCount;
const edgeWeakType = this._edgeWeakType;
const firstEdgeIndexes = this._firstEdgeIndexes;
const containmentEdges = this.containmentEdges;
const nodeFieldCount = this._nodeFieldCount;
const nodesCount = this.nodeCount;
const flags = this._flags;
const pageObjectFlag = this._nodeFlags.pageObject;
const nodesToVisit = new Uint32Array(nodesCount);
let nodesToVisitLength = 0;
const rootNodeOrdinal = this._rootNodeIndex / nodeFieldCount;
const node = this.rootNode();
// Populate the entry points. They are Window objects and DOM Tree Roots.
for (let edgeIndex = firstEdgeIndexes[rootNodeOrdinal], endEdgeIndex = firstEdgeIndexes[rootNodeOrdinal + 1];
edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) {
const edgeType = containmentEdges[edgeIndex + edgeTypeOffset];
const nodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
if (edgeType === edgeElementType) {
node.nodeIndex = nodeIndex;
if (!node.isDocumentDOMTreesRoot()) {
continue;
}
} else if (edgeType !== edgeShortcutType) {
continue;
}
const nodeOrdinal = nodeIndex / nodeFieldCount;
nodesToVisit[nodesToVisitLength++] = nodeOrdinal;
flags[nodeOrdinal] |= pageObjectFlag;
}
// Mark everything reachable with the pageObject flag.
while (nodesToVisitLength) {
const nodeOrdinal = nodesToVisit[--nodesToVisitLength];
const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal];
const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1];
for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) {
const childNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
const childNodeOrdinal = childNodeIndex / nodeFieldCount;
if (flags[childNodeOrdinal] & pageObjectFlag) {
continue;
}
const type = containmentEdges[edgeIndex + edgeTypeOffset];
if (type === edgeWeakType) {
continue;
}
nodesToVisit[nodesToVisitLength++] = childNodeOrdinal;
flags[childNodeOrdinal] |= pageObjectFlag;
}
}
}
/**
* @override
*/
calculateStatistics() {
const nodeFieldCount = this._nodeFieldCount;
const nodes = this.nodes;
const nodesLength = nodes.length;
const nodeTypeOffset = this._nodeTypeOffset;
const nodeSizeOffset = this._nodeSelfSizeOffset;
const nodeNativeType = this._nodeNativeType;
const nodeCodeType = this._nodeCodeType;
const nodeConsStringType = this._nodeConsStringType;
const nodeSlicedStringType = this._nodeSlicedStringType;
const distances = this._nodeDistances;
let sizeNative = 0;
let sizeCode = 0;
let sizeStrings = 0;
let sizeJSArrays = 0;
let sizeSystem = 0;
const node = this.rootNode();
for (let nodeIndex = 0; nodeIndex < nodesLength; nodeIndex += nodeFieldCount) {
const nodeSize = nodes[nodeIndex + nodeSizeOffset];
const ordinal = nodeIndex / nodeFieldCount;
if (distances[ordinal] >= baseSystemDistance) {
sizeSystem += nodeSize;
continue;
}
const nodeType = nodes[nodeIndex + nodeTypeOffset];
node.nodeIndex = nodeIndex;
if (nodeType === nodeNativeType) {
sizeNative += nodeSize;
} else if (nodeType === nodeCodeType) {
sizeCode += nodeSize;
} else if (nodeType === nodeConsStringType || nodeType === nodeSlicedStringType || node.type() === 'string') {
sizeStrings += nodeSize;
} else if (node.name() === 'Array') {
sizeJSArrays += this._calculateArraySize(node);
}
}
this._statistics = new Statistics();
this._statistics.total = this.totalSize;
this._statistics.v8heap = this.totalSize - sizeNative;
this._statistics.native = sizeNative;
this._statistics.code = sizeCode;
this._statistics.jsArrays = sizeJSArrays;
this._statistics.strings = sizeStrings;
this._statistics.system = sizeSystem;
}
/**
* @param {!HeapSnapshotNode} node
* @return {number}
*/
_calculateArraySize(node) {
let size = node.selfSize();
const beginEdgeIndex = node.edgeIndexesStart();
const endEdgeIndex = node.edgeIndexesEnd();
const containmentEdges = this.containmentEdges;
const strings = this.strings;
const edgeToNodeOffset = this._edgeToNodeOffset;
const edgeTypeOffset = this._edgeTypeOffset;
const edgeNameOffset = this._edgeNameOffset;
const edgeFieldsCount = this._edgeFieldsCount;
const edgeInternalType = this._edgeInternalType;
for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex; edgeIndex += edgeFieldsCount) {
const edgeType = containmentEdges[edgeIndex + edgeTypeOffset];
if (edgeType !== edgeInternalType) {
continue;
}
const edgeName = strings[containmentEdges[edgeIndex + edgeNameOffset]];
if (edgeName !== 'elements') {
continue;
}
const elementsNodeIndex = containmentEdges[edgeIndex + edgeToNodeOffset];
node.nodeIndex = elementsNodeIndex;
if (node.retainersCount() === 1) {
size += node.selfSize();
}
break;
}
return size;
}
/**
* @return {!Statistics}
*/
getStatistics() {
return this._statistics;
}
}
/**
* @unrestricted
*/
class JSHeapSnapshotNode extends HeapSnapshotNode {
/**
* @param {!JSHeapSnapshot} snapshot
* @param {number=} nodeIndex
*/
constructor(snapshot, nodeIndex) {
super(snapshot, nodeIndex);
}
/**
* @return {boolean}
*/
canBeQueried() {
const flags = this._snapshot._flagsOfNode(this);
return !!(flags & this._snapshot._nodeFlags.canBeQueried);
}
/**
* @return {string}
*/
rawName() {
return super.name();
}
/**
* @override
* @return {string}
*/
name() {
const snapshot = this._snapshot;
if (this.rawType() === snapshot._nodeConsStringType) {
let string = snapshot._lazyStringCache[this.nodeIndex];
if (typeof string === 'undefined') {
string = this._consStringName();
snapshot._lazyStringCache[this.nodeIndex] = string;
}
return string;
}
return this.rawName();
}
/**
* @return {string}
*/
_consStringName() {
const snapshot = this._snapshot;
const consStringType = snapshot._nodeConsStringType;
const edgeInternalType = snapshot._edgeInternalType;
const edgeFieldsCount = snapshot._edgeFieldsCount;
const edgeToNodeOffset = snapshot._edgeToNodeOffset;
const edgeTypeOffset = snapshot._edgeTypeOffset;
const edgeNameOffset = snapshot._edgeNameOffset;
const strings = snapshot.strings;
const edges = snapshot.containmentEdges;
const firstEdgeIndexes = snapshot._firstEdgeIndexes;
const nodeFieldCount = snapshot._nodeFieldCount;
const nodeTypeOffset = snapshot._nodeTypeOffset;
const nodeNameOffset = snapshot._nodeNameOffset;
const nodes = snapshot.nodes;
const nodesStack = [];
nodesStack.push(this.nodeIndex);
let name = '';
while (nodesStack.length && name.length < 1024) {
const nodeIndex = nodesStack.pop();
if (nodes[nodeIndex + nodeTypeOffset] !== consStringType) {
name += strings[nodes[nodeIndex + nodeNameOffset]];
continue;
}
const nodeOrdinal = nodeIndex / nodeFieldCount;
const beginEdgeIndex = firstEdgeIndexes[nodeOrdinal];
const endEdgeIndex = firstEdgeIndexes[nodeOrdinal + 1];
let firstNodeIndex = 0;
let secondNodeIndex = 0;
for (let edgeIndex = beginEdgeIndex; edgeIndex < endEdgeIndex && (!firstNodeIndex || !secondNodeIndex);
edgeIndex += edgeFieldsCount) {
const edgeType = edges[edgeIndex + edgeTypeOffset];
if (edgeType === edgeInternalType) {
const edgeName = strings[edges[edgeIndex + edgeNameOffset]];
if (edgeName === 'first') {
firstNodeIndex = edges[edgeIndex + edgeToNodeOffset];
} else if (edgeName === 'second') {
secondNodeIndex = edges[edgeIndex + edgeToNodeOffset];
}
}
}
nodesStack.push(secondNodeIndex);
nodesStack.push(firstNodeIndex);
}
return name;
}
/**
* @override
* @return {string}
*/
className() {
const type = this.type();
switch (type) {
case 'hidden':
return '(system)';
case 'object':
case 'native':
return this.name();
case 'code':
return '(compiled code)';
default:
return '(' + type + ')';
}
}
/**
* @override
* @return {number}
*/
classIndex() {
const snapshot = this._snapshot;
const nodes = snapshot.nodes;
const type = nodes[this.nodeIndex + snapshot._nodeTypeOffset];
if (type === snapshot._nodeObjectType || type === snapshot._nodeNativeType) {
return nodes[this.nodeIndex + snapshot._nodeNameOffset];
}
return -1 - type;
}
/**
* @override
* @return {number}
*/
id() {
const snapshot = this._snapshot;
return snapshot.nodes[this.nodeIndex + snapshot._nodeIdOffset];
}
/**
* @return {boolean}
*/
isHidden() {
return this.rawType() === this._snapshot._nodeHiddenType;
}
/**
* @return {boolean}
*/
isArray() {
return this.rawType() === this._snapshot._nodeArrayType;
}
/**
* @return {boolean}
*/
isSynthetic() {
return this.rawType() === this._snapshot._nodeSyntheticType;
}
/**
* @return {boolean}
*/
isUserRoot() {
return !this.isSynthetic();
}
/**
* @return {boolean}
*/
isDocumentDOMTreesRoot() {
return this.isSynthetic() && this.name() === '(Document DOM trees)';
}
/**
* @override
* @return {!Node}
*/
serialize() {
const result = super.serialize();
const flags = this._snapshot._flagsOfNode(this);
if (flags & this._snapshot._nodeFlags.canBeQueried) {
result.canBeQueried = true;
}
if (flags & this._snapshot._nodeFlags.detachedDOMTreeNode) {
result.detachedDOMTreeNode = true;
}
return result;
}
}
/**
* @unrestricted
*/
class JSHeapSnapshotEdge extends HeapSnapshotEdge {
/**
* @param {!JSHeapSnapshot} snapshot
* @param {number=} edgeIndex
*/
constructor(snapshot, edgeIndex) {
super(snapshot, edgeIndex);
}
/**
* @override
* @return {!JSHeapSnapshotEdge}
*/
clone() {
const snapshot = /** @type {!JSHeapSnapshot} */ (this._snapshot);
return new JSHeapSnapshotEdge(snapshot, this.edgeIndex);
}
/**
* @override
* @return {boolean}
*/
hasStringName() {
if (!this.isShortcut()) {
return this._hasStringName();
}
return isNaN(parseInt(this._name(), 10));
}
/**
* @return {boolean}
*/
isElement() {
return this.rawType() === this._snapshot._edgeElementType;
}
/**
* @return {boolean}
*/
isHidden() {
return this.rawType() === this._snapshot._edgeHiddenType;
}
/**
* @return {boolean}
*/
isWeak() {
return this.rawType() === this._snapshot._edgeWeakType;
}
/**
* @return {boolean}
*/
isInternal() {
return this.rawType() === this._snapshot._edgeInternalType;
}
/**
* @return {boolean}
*/
isInvisible() {
return this.rawType() === this._snapshot._edgeInvisibleType;
}
/**
* @return {boolean}
*/
isShortcut() {
return this.rawType() === this._snapshot._edgeShortcutType;
}
/**
* @override
* @return {string}
*/
name() {
const name = this._name();
if (!this.isShortcut()) {
return String(name);
}
const numName = parseInt(name, 10);
return String(isNaN(numName) ? name : numName);
}
/**
* @override
* @return {string}
*/
toString() {
const name = this.name();
switch (this.type()) {
case 'context':
return '->' + name;
case 'element':
return '[' + name + ']';
case 'weak':
return '[[' + name + ']]';
case 'property':
return name.indexOf(' ') === -1 ? '.' + name : '["' + name + '"]';
case 'shortcut':
if (typeof name === 'string') {
return name.indexOf(' ') === -1 ? '.' + name : '["' + name + '"]';
}
return '[' + name + ']';
case 'internal':
case 'hidden':
case 'invisible':
return '{' + name + '}';
}
return '?' + name + '?';
}
/**
* @return {boolean}
*/
_hasStringName() {
const type = this.rawType();
const snapshot = this._snapshot;
return type !== snapshot._edgeElementType && type !== snapshot._edgeHiddenType;
}
/**
* @return {string|number}
*/
_name() {
return this._hasStringName() ? this._snapshot.strings[this._nameOrIndex()] : this._nameOrIndex();
}
/**
* @return {number}
*/
_nameOrIndex() {
return this._edges[this.edgeIndex + this._snapshot._edgeNameOffset];
}
/**
* @override
* @return {number}
*/
rawType() {
return this._edges[this.edgeIndex + this._snapshot._edgeTypeOffset];
}
}
/**
* @unrestricted
*/
class JSHeapSnapshotRetainerEdge extends HeapSnapshotRetainerEdge {
/**
* @param {!JSHeapSnapshot} snapshot
* @param {number} retainerIndex
*/
constructor(snapshot, retainerIndex) {
super(snapshot, retainerIndex);
}
/**
* @override
* @return {!JSHeapSnapshotRetainerEdge}
*/
clone() {
const snapshot = /** @type {!JSHeapSnapshot} */ (this._snapshot);
return new JSHeapSnapshotRetainerEdge(snapshot, this.retainerIndex());
}
/**
* @return {boolean}
*/
isHidden() {
return this._edge().isHidden();
}
/**
* @return {boolean}
*/
isInternal() {
return this._edge().isInternal();
}
/**
* @return {boolean}
*/
isInvisible() {
return this._edge().isInvisible();
}
/**
* @return {boolean}
*/
isShortcut() {
return this._edge().isShortcut();
}
/**
* @return {boolean}
*/
isWeak() {
return this._edge().isWeak();
}
}
(function disableLoggingForTest() {
// Runtime doesn't exist because this file is loaded as a one-off
// file in some inspector-protocol tests.
if (self.Root && self.Root.Runtime && Root.Runtime.queryParam('test')) {
console.warn = () => undefined;
}
})();
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
class HeapSnapshotLoader {
/**
* @param {!HeapSnapshotWorkerDispatcher} dispatcher
*/
constructor(dispatcher) {
this._reset();
this._progress = new HeapSnapshotProgress(dispatcher);
this._buffer = '';
this._dataCallback = null;
this._done = false;
this._parseInput();
}
dispose() {
this._reset();
}
_reset() {
this._json = '';
this._snapshot = {};
}
close() {
this._done = true;
if (this._dataCallback) {
this._dataCallback('');
}
}
/**
* @return {!JSHeapSnapshot}
*/
buildSnapshot() {
this._progress.updateStatus(ls`Processing snapshot…`);
const result = new JSHeapSnapshot(this._snapshot, this._progress);
this._reset();
return result;
}
_parseUintArray() {
let index = 0;
const char0 = '0'.charCodeAt(0);
const char9 = '9'.charCodeAt(0);
const closingBracket = ']'.charCodeAt(0);
const length = this._json.length;
while (true) {
while (index < length) {
const code = this._json.charCodeAt(index);
if (char0 <= code && code <= char9) {
break;
} else if (code === closingBracket) {
this._json = this._json.slice(index + 1);
return false;
}
++index;
}
if (index === length) {
this._json = '';
return true;
}
let nextNumber = 0;
const startIndex = index;
while (index < length) {
const code = this._json.charCodeAt(index);
if (char0 > code || code > char9) {
break;
}
nextNumber *= 10;
nextNumber += (code - char0);
++index;
}
if (index === length) {
this._json = this._json.slice(startIndex);
return true;
}
this._array[this._arrayIndex++] = nextNumber;
}
}
_parseStringsArray() {
this._progress.updateStatus(ls`Parsing strings…`);
const closingBracketIndex = this._json.lastIndexOf(']');
if (closingBracketIndex === -1) {
throw new Error('Incomplete JSON');
}
this._json = this._json.slice(0, closingBracketIndex + 1);
this._snapshot.strings = JSON.parse(this._json);
}
/**
* @param {string} chunk
*/
write(chunk) {
this._buffer += chunk;
if (!this._dataCallback) {
return;
}
this._dataCallback(this._buffer);
this._dataCallback = null;
this._buffer = '';
}
/**
* @return {!Promise<string>}
*/
_fetchChunk() {
return this._done ? Promise.resolve(this._buffer) : new Promise(r => {
this._dataCallback = r;
});
}
/**
* @param {string} token
* @param {number=} startIndex
* @return {!Promise<number>}
*/
async _findToken(token, startIndex) {
while (true) {
const pos = this._json.indexOf(token, startIndex || 0);
if (pos !== -1) {
return pos;
}
startIndex = this._json.length - token.length + 1;
this._json += await this._fetchChunk();
}
}
/**
* @param {string} name
* @param {string} title
* @param {number=} length
* @return {!Promise<!Uint32Array|!Array<number>>}
*/
async _parseArray(name, title, length) {
const nameIndex = await this._findToken(name);
const bracketIndex = await this._findToken('[', nameIndex);
this._json = this._json.slice(bracketIndex + 1);
this._array = length ? new Uint32Array(length) : [];
this._arrayIndex = 0;
while (this._parseUintArray()) {
this._progress.updateProgress(title, this._arrayIndex, this._array.length);
this._json += await this._fetchChunk();
}
const result = this._array;
this._array = null;
return result;
}
async _parseInput() {
const snapshotToken = '"snapshot"';
const snapshotTokenIndex = await this._findToken(snapshotToken);
if (snapshotTokenIndex === -1) {
throw new Error('Snapshot token not found');
}
this._progress.updateStatus(ls`Loading snapshot info…`);
const json = this._json.slice(snapshotTokenIndex + snapshotToken.length + 1);
this._jsonTokenizer = new BalancedJSONTokenizer(metaJSON => {
this._json = this._jsonTokenizer.remainder();
this._jsonTokenizer = null;
this._snapshot.snapshot = /** @type {!HeapSnapshotHeader} */ (JSON.parse(metaJSON));
});
this._jsonTokenizer.write(json);
while (this._jsonTokenizer) {
this._jsonTokenizer.write(await this._fetchChunk());
}
this._snapshot.nodes = await this._parseArray(
'"nodes"', ls`Loading nodes %d%%`,
this._snapshot.snapshot.meta.node_fields.length * this._snapshot.snapshot.node_count);
this._snapshot.edges = await this._parseArray(
'"edges"', ls`Loading edges %d%%`,
this._snapshot.snapshot.meta.edge_fields.length * this._snapshot.snapshot.edge_count);
if (this._snapshot.snapshot.trace_function_count) {
this._snapshot.trace_function_infos = await this._parseArray(
'"trace_function_infos"', ls`Loading allocation traces %d%%`,
this._snapshot.snapshot.meta.trace_function_info_fields.length *
this._snapshot.snapshot.trace_function_count);
const thisTokenEndIndex = await this._findToken(':');
const nextTokenIndex = await this._findToken('"', thisTokenEndIndex);
const openBracketIndex = this._json.indexOf('[');
const closeBracketIndex = this._json.lastIndexOf(']', nextTokenIndex);
this._snapshot.trace_tree = JSON.parse(this._json.substring(openBracketIndex, closeBracketIndex + 1));
this._json = this._json.slice(closeBracketIndex + 1);
}
if (this._snapshot.snapshot.meta.sample_fields) {
this._snapshot.samples = await this._parseArray('"samples"', ls`Loading samples…`);
}
if (this._snapshot.snapshot.meta['location_fields']) {
this._snapshot.locations = await this._parseArray('"locations"', ls`Loading locations…`);
} else {
this._snapshot.locations = [];
}
this._progress.updateStatus(ls`Loading strings…`);
const stringsTokenIndex = await this._findToken('"strings"');
const bracketIndex = await this._findToken('[', stringsTokenIndex);
this._json = this._json.slice(bracketIndex);
while (!this._done) {
this._json += await this._fetchChunk();
}
this._parseStringsArray();
}
}
exports.HeapSnapshotLoader = HeapSnapshotLoader;
return exports;
}({}));
self.HeapSnapshotWorker = Object.assign(self.HeapSnapshotWorker || {}, HeapSnapshotLoader);