chromium/third_party/blink/web_tests/inspector-protocol/heap-profiler/resources/HeapSnapshotLoader.js

const HeapSnapshotLoader = (function (exports) {
  'use strict';

  // Copyright 2020 The Chromium Authors
  // 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 ([email protected]).
   * 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
  // 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
  // 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);