// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview using private properties isn't a Closure violation in tests.
 * @suppress {accessControls}
 */
self.HeapProfilerTestRunner = self.HeapProfilerTestRunner || {};

HeapProfilerTestRunner.createHeapSnapshotMockFactories = function() {
  HeapProfilerTestRunner.createJSHeapSnapshotMockObject = function() {
    return {
      _rootNodeIndex: 0,
      _nodeTypeOffset: 0,
      _nodeNameOffset: 1,
      _nodeEdgeCountOffset: 2,
      _nodeFieldCount: 3,
      _edgeFieldsCount: 3,
      _edgeTypeOffset: 0,
      _edgeNameOffset: 1,
      _edgeToNodeOffset: 2,
      _nodeTypes: ['hidden', 'object'],
      _edgeTypes: ['element', 'property', 'shortcut'],
      _edgeShortcutType: -1,
      _edgeHiddenType: -1,
      _edgeElementType: 0,
      _realNodesLength: 18,
      nodes: new Uint32Array([0, 0, 2, 1, 1, 2, 1, 2, 2, 1, 3, 1, 1, 4, 0, 1, 5, 0]),
      containmentEdges: new Uint32Array([2, 6, 3, 1, 7, 6, 0, 1, 6, 1, 8, 9, 1, 9, 9, 1, 10, 12, 1, 11, 15]),
      strings: ['', 'A', 'B', 'C', 'D', 'E', 'a', 'b', 'ac', 'bc', 'bd', 'ce'],
      _firstEdgeIndexes: new Uint32Array([0, 6, 12, 18, 21, 21, 21]),
      createNode: HeapSnapshotWorker.JSHeapSnapshot.prototype.createNode,
      createEdge: HeapSnapshotWorker.JSHeapSnapshot.prototype.createEdge,
      createRetainingEdge: HeapSnapshotWorker.JSHeapSnapshot.prototype.createRetainingEdge
    };
  };

  HeapProfilerTestRunner.createHeapSnapshotMockRaw = function() {
    return {
      snapshot: {
        meta: {
          node_fields: ['type', 'name', 'id', 'self_size', 'retained_size', 'dominator', 'edge_count'],
          node_types: [['hidden', 'object'], '', '', '', '', '', ''],
          edge_fields: ['type', 'name_or_index', 'to_node'],
          edge_types: [['element', 'property', 'shortcut'], '', ''],
          location_fields: ['object_index', 'script_id', 'line', 'column'],
          trace_function_info_fields: ['function_id', 'name', 'script_name', 'script_id', 'line', 'column'],
          trace_node_fields: ['id', 'function_info_index', 'count', 'size', 'children']
        },
        node_count: 6,
        edge_count: 7
      },

      nodes: [
        0, 0, 1, 0, 20, 0, 2, 1, 1, 2, 2, 2, 0,  2, 1, 2, 3, 3, 8, 0,  2,
        1, 3, 4, 4, 10, 0, 1, 1, 4, 5, 5, 5, 14, 0, 1, 5, 6, 6, 6, 21, 0
      ],

      edges: [1, 6, 7, 1, 7, 14, 0, 1, 14, 1, 8, 21, 1, 9, 21, 1, 10, 28, 1, 11, 35],

      trace_function_infos: [0, 2, 1, 0, 0, 0],

      trace_tree: [1, 0, 0, 0, []],

      locations: [0, 1, 2, 3, 18, 2, 3, 4],

      strings: ['', 'A', 'B', 'C', 'D', 'E', 'a', 'b', 'ac', 'bc', 'bd', 'ce']
    };
  };

  HeapProfilerTestRunner._postprocessHeapSnapshotMock = function(mock) {
    mock.nodes = new Uint32Array(mock.nodes);
    mock.edges = new Uint32Array(mock.edges);
    return mock;
  };

  HeapProfilerTestRunner.createHeapSnapshotMock = function() {
    return HeapProfilerTestRunner._postprocessHeapSnapshotMock(HeapProfilerTestRunner.createHeapSnapshotMockRaw());
  };

  HeapProfilerTestRunner.createHeapSnapshotMockWithDOM = function() {
    return HeapProfilerTestRunner._postprocessHeapSnapshotMock({
      snapshot: {
        meta: {
          node_fields: ['type', 'name', 'id', 'edge_count'],
          node_types: [['hidden', 'object', 'synthetic'], '', '', ''],
          edge_fields: ['type', 'name_or_index', 'to_node'],
          edge_types: [['element', 'hidden', 'internal'], '', ''],
          location_fields: ['object_index', 'script_id', 'line', 'column']
        },

        node_count: 13,
        edge_count: 13
      },

      nodes: [
        2, 0, 1, 4, 1, 11, 2, 2, 1, 11, 3, 3, 2,  5, 4, 0, 2,  6, 5, 1,  1,  1, 6, 0, 1,  2,
        7, 1, 1, 4, 8, 2,  1, 8, 9, 0,  1, 7, 10, 0, 1, 3, 11, 0, 1, 10, 12, 0, 1, 9, 13, 0
      ],

      edges: [
        0,  1, 4, 0,  2, 8, 0,  3, 12, 0,  4, 16, 0,  1, 20, 0,  2, 24, 0, 1,
        24, 0, 2, 28, 1, 3, 32, 0, 1,  36, 0, 1,  40, 2, 12, 44, 2, 1,  48
      ],

      locations: [0, 2, 1, 1, 6, 2, 2, 2],

      strings: ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'M', 'N', 'Window', 'native']
    });
  };

  HeapProfilerTestRunner.HeapNode = function(name, selfSize, type, id) {
    this._type = type || HeapProfilerTestRunner.HeapNode.Type.object;
    this._name = name;
    this._selfSize = selfSize || 0;
    this._builder = null;
    this._edges = {};
    this._edgesCount = 0;
    this._id = id;
  };

  HeapProfilerTestRunner.HeapNode.Type = {
    'hidden': 'hidden',
    'array': 'array',
    'string': 'string',
    'object': 'object',
    'code': 'code',
    'closure': 'closure',
    'regexp': 'regexp',
    'number': 'number',
    'native': 'native',
    'synthetic': 'synthetic',
    'bigint': 'bigint'
  };

  HeapProfilerTestRunner.HeapNode.prototype = {
    linkNode: function(node, type, nameOrIndex) {
      if (!this._builder) {
        throw new Error('parent node is not connected to a snapshot');
      }

      if (!node._builder) {
        node._setBuilder(this._builder);
      }

      if (nameOrIndex === undefined) {
        nameOrIndex = this._edgesCount;
      }

      ++this._edgesCount;

      if (nameOrIndex in this._edges) {
        throw new Error(
            'Can\'t add edge with the same nameOrIndex. nameOrIndex: ' + nameOrIndex +
            ' oldNodeName: ' + this._edges[nameOrIndex]._name + ' newNodeName: ' + node._name);
      }

      this._edges[nameOrIndex] = new HeapProfilerTestRunner.HeapEdge(node, type, nameOrIndex);
    },

    _setBuilder: function(builder) {
      if (this._builder) {
        throw new Error('node reusing is prohibited');
      }

      this._builder = builder;
      this._ordinal = this._builder._registerNode(this);
    },

    _serialize: function(rawSnapshot) {
      rawSnapshot.nodes.push(this._builder.lookupNodeType(this._type));
      rawSnapshot.nodes.push(this._builder.lookupOrAddString(this._name));
      rawSnapshot.nodes.push(this._id || this._ordinal * 2 + 1);
      rawSnapshot.nodes.push(this._selfSize);
      rawSnapshot.nodes.push(0);
      rawSnapshot.nodes.push(0);
      rawSnapshot.nodes.push(Object.keys(this._edges).length);

      for (const i in this._edges) {
        this._edges[i]._serialize(rawSnapshot);
      }
    }
  };

  HeapProfilerTestRunner.HeapEdge = function(targetNode, type, nameOrIndex) {
    this._targetNode = targetNode;
    this._type = type;
    this._nameOrIndex = nameOrIndex;
  };

  HeapProfilerTestRunner.HeapEdge.prototype = {
    _serialize: function(rawSnapshot) {
      if (!this._targetNode._builder) {
        throw new Error('Inconsistent state of node: ' + this._name + ' no builder assigned');
      }

      const builder = this._targetNode._builder;
      rawSnapshot.edges.push(builder.lookupEdgeType(this._type));
      rawSnapshot.edges.push(
          (typeof this._nameOrIndex === 'string' ? builder.lookupOrAddString(this._nameOrIndex) : this._nameOrIndex));
      rawSnapshot.edges.push(this._targetNode._ordinal * builder.nodeFieldsCount);
    }
  };

  HeapProfilerTestRunner.HeapEdge.Type = {
    'context': 'context',
    'element': 'element',
    'property': 'property',
    'internal': 'internal',
    'hidden': 'hidden',
    'shortcut': 'shortcut',
    'weak': 'weak'
  };

  HeapProfilerTestRunner.HeapSnapshotBuilder = function() {
    this._nodes = [];
    this._string2id = {};
    this._strings = [];
    this.nodeFieldsCount = 7;
    this._nodeTypesMap = {};
    this._nodeTypesArray = [];

    for (const nodeType in HeapProfilerTestRunner.HeapNode.Type) {
      this._nodeTypesMap[nodeType] = this._nodeTypesArray.length;
      this._nodeTypesArray.push(nodeType);
    }

    this._edgeTypesMap = {};
    this._edgeTypesArray = [];

    for (const edgeType in HeapProfilerTestRunner.HeapEdge.Type) {
      this._edgeTypesMap[edgeType] = this._edgeTypesArray.length;
      this._edgeTypesArray.push(edgeType);
    }

    this.rootNode = new HeapProfilerTestRunner.HeapNode('root', 0, 'object');
    this.rootNode._setBuilder(this);
  };

  HeapProfilerTestRunner.HeapSnapshotBuilder.prototype = {
    generateSnapshot: function() {
      const rawSnapshot = {
        'snapshot': {
          'meta': {
            'node_fields': ['type', 'name', 'id', 'self_size', 'retained_size', 'dominator', 'edge_count'],
            'node_types': [this._nodeTypesArray, 'string', 'number', 'number', 'number', 'number', 'number'],
            'edge_fields': ['type', 'name_or_index', 'to_node'],
            'edge_types': [this._edgeTypesArray, 'string_or_number', 'node']
          }
        },

        'nodes': [],
        'edges': [],
        'locations': [],
        'strings': []
      };

      for (let i = 0; i < this._nodes.length; ++i) {
        this._nodes[i]._serialize(rawSnapshot);
      }

      rawSnapshot.strings = this._strings.slice();
      const meta = rawSnapshot.snapshot.meta;
      rawSnapshot.snapshot.edge_count = rawSnapshot.edges.length / meta.edge_fields.length;
      rawSnapshot.snapshot.node_count = rawSnapshot.nodes.length / meta.node_fields.length;
      return rawSnapshot;
    },

    createJSHeapSnapshot: function() {
      const parsedSnapshot = HeapProfilerTestRunner._postprocessHeapSnapshotMock(this.generateSnapshot());
      return new HeapSnapshotWorker.JSHeapSnapshot(parsedSnapshot, new HeapSnapshotWorker.HeapSnapshotProgress());
    },

    _registerNode: function(node) {
      this._nodes.push(node);
      return this._nodes.length - 1;
    },

    lookupNodeType: function(typeName) {
      if (typeName === undefined) {
        throw new Error('wrong node type: ' + typeName);
      }

      if (!(typeName in this._nodeTypesMap)) {
        throw new Error('wrong node type name: ' + typeName);
      }

      return this._nodeTypesMap[typeName];
    },

    lookupEdgeType: function(typeName) {
      if (!(typeName in this._edgeTypesMap)) {
        throw new Error('wrong edge type name: ' + typeName);
      }

      return this._edgeTypesMap[typeName];
    },

    lookupOrAddString: function(string) {
      if (string in this._string2id) {
        return this._string2id[string];
      }

      this._string2id[string] = this._strings.length;
      this._strings.push(string);
      return this._strings.length - 1;
    }
  };

  HeapProfilerTestRunner.createHeapSnapshot = function(instanceCount, firstId) {
    let seed = 881669;

    function pseudoRandom(limit) {
      seed = seed * 355109 + 153763 >> 2 & 65535;
      return seed % limit;
    }

    const builder = new HeapProfilerTestRunner.HeapSnapshotBuilder();
    const rootNode = builder.rootNode;
    const gcRootsNode =
        new HeapProfilerTestRunner.HeapNode('(GC roots)', 0, HeapProfilerTestRunner.HeapNode.Type.synthetic);
    rootNode.linkNode(gcRootsNode, HeapProfilerTestRunner.HeapEdge.Type.element);
    const windowNode = new HeapProfilerTestRunner.HeapNode('Window', 20);
    rootNode.linkNode(windowNode, HeapProfilerTestRunner.HeapEdge.Type.shortcut);
    gcRootsNode.linkNode(windowNode, HeapProfilerTestRunner.HeapEdge.Type.element);

    for (let i = 0; i < instanceCount; ++i) {
      const sizeOfB = pseudoRandom(20) + 1;
      const nodeB = new HeapProfilerTestRunner.HeapNode('B', sizeOfB, undefined, firstId++);
      windowNode.linkNode(nodeB, HeapProfilerTestRunner.HeapEdge.Type.element);
      const sizeOfA = pseudoRandom(50) + 1;
      const nodeA = new HeapProfilerTestRunner.HeapNode('A', sizeOfA, undefined, firstId++);
      nodeB.linkNode(nodeA, HeapProfilerTestRunner.HeapEdge.Type.property, 'a');
      nodeA.linkNode(nodeA, HeapProfilerTestRunner.HeapEdge.Type.property, 'a');
    }

    return builder.generateSnapshot();
  };
};

HeapProfilerTestRunner.createHeapSnapshotMockFactories();

HeapProfilerTestRunner.startProfilerTest = function(callback) {
  TestRunner.addResult('Profiler was enabled.');
  HeapProfilerTestRunner._panelReset = TestRunner.override(UI.panels.heap_profiler, '_reset', function() {}, true);
  TestRunner.addSniffer(UI.panels.heap_profiler, '_addProfileHeader', HeapProfilerTestRunner._profileHeaderAdded, true);
  TestRunner.addSniffer(Profiler.ProfileView.prototype, 'refresh', HeapProfilerTestRunner._profileViewRefresh, true);
  TestRunner.addSniffer(Profiler.HeapSnapshotView.prototype, 'show', HeapProfilerTestRunner._snapshotViewShown, true);

  Profiler.HeapSnapshotContainmentDataGrid.prototype.defaultPopulateCount = function() {
    return 10;
  };

  Profiler.HeapSnapshotConstructorsDataGrid.prototype.defaultPopulateCount = function() {
    return 10;
  };

  Profiler.HeapSnapshotDiffDataGrid.prototype.defaultPopulateCount = function() {
    return 5;
  };

  TestRunner.addResult('Detailed heap profiles were enabled.');
  TestRunner.safeWrap(callback)();
};

HeapProfilerTestRunner.completeProfilerTest = function() {
  TestRunner.addResult('');
  TestRunner.addResult('Profiler was disabled.');
  TestRunner.completeTest();
};

HeapProfilerTestRunner.runHeapSnapshotTestSuite = function(testSuite) {
  const testSuiteTests = testSuite.slice();
  let completeTestStack;

  function runner() {
    if (!testSuiteTests.length) {
      if (completeTestStack) {
        TestRunner.addResult('FAIL: test already completed at ' + completeTestStack);
      }

      HeapProfilerTestRunner.completeProfilerTest();
      completeTestStack = new Error().stack;
      return;
    }

    const nextTest = testSuiteTests.shift();
    TestRunner.addResult('');
    TestRunner.addResult(
        'Running: ' +
        /function\s([^(]*)/.exec(nextTest)[1]);
    HeapProfilerTestRunner._panelReset.call(UI.panels.heap_profiler);
    TestRunner.safeWrap(nextTest)(runner, runner);
  }

  HeapProfilerTestRunner.startProfilerTest(runner);
};

HeapProfilerTestRunner.assertColumnContentsEqual = function(reference, actual) {
  const length = Math.min(reference.length, actual.length);

  for (let i = 0; i < length; ++i) {
    TestRunner.assertEquals(reference[i], actual[i], 'row ' + i);
  }

  if (reference.length > length) {
    TestRunner.addResult('extra rows in reference array:\n' + reference.slice(length).join('\n'));
  } else if (actual.length > length) {
    TestRunner.addResult('extra rows in actual array:\n' + actual.slice(length).join('\n'));
  }
};

HeapProfilerTestRunner.checkArrayIsSorted = function(contents, sortType, sortOrder) {
  function simpleComparator(a, b) {
    return (a < b ? -1 : (a > b ? 1 : 0));
  }

  function parseSize(size) {
    return parseInt(size.replace(/[\xa0,]/g, ''), 10);
  }

  const extractor = {
    text: function(data) {
      data;
    },

    number: function(data) {
      return parseInt(data, 10);
    },

    size: parseSize,

    name: function(data) {
      return data;
    },

    id: function(data) {
      return parseInt(data, 10);
    }
  }[sortType];

  if (!extractor) {
    TestRunner.addResult('Invalid sort type: ' + sortType);
    return;
  }

  let acceptableComparisonResult;

  if (sortOrder === DataGrid.DataGrid.Order.Ascending) {
    acceptableComparisonResult = -1;
  } else if (sortOrder === DataGrid.DataGrid.Order.Descending) {
    acceptableComparisonResult = 1;
  } else {
    TestRunner.addResult('Invalid sort order: ' + sortOrder);
    return;
  }

  for (let i = 0; i < contents.length - 1; ++i) {
    const a = extractor(contents[i]);
    const b = extractor(contents[i + 1]);
    const result = simpleComparator(a, b);

    if (result !== 0 && result !== acceptableComparisonResult) {
      TestRunner.addResult(
          'Elements ' + i + ' and ' + (i + 1) + ' are out of order: ' + a + ' ' + b + ' (' + sortOrder + ')');
    }
  }
};

HeapProfilerTestRunner.clickColumn = function(column, callback) {
  callback = TestRunner.safeWrap(callback);
  const cell = this._currentGrid()._headerTableHeaders[column.id];

  const event = {target: cell};

  function sortingComplete() {
    HeapProfilerTestRunner._currentGrid().removeEventListener(
        Profiler.HeapSnapshotSortableDataGrid.Events.SortingComplete, sortingComplete, this);
    TestRunner.assertEquals(column.id, this._currentGrid().sortColumnId(), 'unexpected sorting');
    column.sort = this._currentGrid().sortOrder();

    function callCallback() {
      callback(column);
    }

    setTimeout(callCallback, 0);
  }

  HeapProfilerTestRunner._currentGrid().addEventListener(
      Profiler.HeapSnapshotSortableDataGrid.Events.SortingComplete, sortingComplete, this);
  this._currentGrid()._clickInHeaderCell(event);
};

HeapProfilerTestRunner.clickRowAndGetRetainers = function(row, callback) {
  callback = TestRunner.safeWrap(callback);

  const event = {target: row._element};

  this._currentGrid()._mouseDownInDataTable(event);
  const rootNode = HeapProfilerTestRunner.currentProfileView()._retainmentDataGrid.rootNode();
  rootNode.once(Profiler.HeapSnapshotGridNode.Events.PopulateComplete).then(() => callback(rootNode));
};

HeapProfilerTestRunner.clickShowMoreButton = function(buttonName, row, callback) {
  callback = TestRunner.safeWrap(callback);
  const parent = row.parent;
  parent.once(Profiler.HeapSnapshotGridNode.Events.PopulateComplete).then(() => setTimeout(() => callback(parent), 0));
  row[buttonName].click();
};

HeapProfilerTestRunner.columnContents = function(column, row) {
  this._currentGrid().updateVisibleNodes();
  const columnOrdinal = HeapProfilerTestRunner.viewColumns().indexOf(column);
  const result = [];
  const parent = row || this._currentGrid().rootNode();

  for (let node = parent.children[0]; node; node = node.traverseNextNode(true, parent, true)) {
    if (!node.selectable) {
      continue;
    }

    let content = node.element().children[columnOrdinal];

    if (content.firstElementChild) {
      content = content.firstElementChild;
    }

    result.push(content.textContent);
  }

  return result;
};

HeapProfilerTestRunner.countDataRows = function(row, filter) {
  let result = 0;

  filter = filter || function(node) {
    return node.selectable;
  };

  for (let node = row.children[0]; node; node = node.traverseNextNode(true, row, true)) {
    if (filter(node)) {
      ++result;
    }
  }

  return result;
};

HeapProfilerTestRunner.expandRow = function(row, callback) {
  callback = TestRunner.safeWrap(callback);
  row.once(Profiler.HeapSnapshotGridNode.Events.PopulateComplete).then(() => setTimeout(() => callback(row), 0));

  (function expand() {
    if (row.hasChildren()) {
      row.expand();
    } else {
      setTimeout(expand, 0);
    }
  })();
};

HeapProfilerTestRunner.expandRowPromise = function(row) {
  return new Promise(resolve => HeapProfilerTestRunner.expandRow(row, resolve));
};

HeapProfilerTestRunner.findAndExpandGCRoots = function(callback) {
  HeapProfilerTestRunner.findAndExpandRow('(GC roots)', callback);
};

HeapProfilerTestRunner.findAndExpandWindow = function(callback) {
  HeapProfilerTestRunner.findAndExpandRow('Window', callback);
};

HeapProfilerTestRunner.findAndExpandRow = async function(name, callback) {
  const row = HeapProfilerTestRunner.findRow(name);
  TestRunner.assertEquals(true, !!row, `"${name}" row`);
  await HeapProfilerTestRunner.expandRowPromise(row);
  TestRunner.safeWrap(callback)(row);
  return row;
};

HeapProfilerTestRunner.findButtonsNode = function(row, startNode) {
  for (let node = startNode || row.children[0]; node; node = node.traverseNextNode(true, row, true)) {
    if (!node.selectable && node.showNext) {
      return node;
    }
  }
  return null;
};

HeapProfilerTestRunner.findRow = function(name, parent) {
  return HeapProfilerTestRunner.findMatchingRow(node => node._name === name, parent);
};

HeapProfilerTestRunner.findMatchingRow = function(matcher, parent) {
  parent = parent || this._currentGrid().rootNode();

  for (let node = parent.children[0]; node; node = node.traverseNextNode(true, parent, true)) {
    if (matcher(node)) {
      return node;
    }
  }

  return null;
};

HeapProfilerTestRunner.switchToView = function(title, callback) {
  return new Promise(resolve => {
    callback = TestRunner.safeWrap(callback);
    const view = UI.panels.heap_profiler.visibleView;
    view._changePerspectiveAndWait(title).then(callback).then(resolve);
    HeapProfilerTestRunner._currentGrid().scrollContainer.style.height = '10000px';
  });
};

HeapProfilerTestRunner.takeAndOpenSnapshot = async function(generator, callback) {
  callback = TestRunner.safeWrap(callback);
  const snapshot = generator();
  const profileType = Profiler.ProfileTypeRegistry.instance.heapSnapshotProfileType;

  function pushGeneratedSnapshot(reportProgress) {
    if (reportProgress) {
      profileType._reportHeapSnapshotProgress({data: {done: 50, total: 100, finished: false}});
      profileType._reportHeapSnapshotProgress({data: {done: 100, total: 100, finished: true}});
    }
    snapshot.snapshot.typeId = 'HEAP';
    profileType._addHeapSnapshotChunk({data: JSON.stringify(snapshot)});
    return Promise.resolve();
  }

  HeapProfilerTestRunner._takeAndOpenSnapshotCallback = callback;
  TestRunner.override(TestRunner.HeapProfilerAgent, 'invoke_takeHeapSnapshot', pushGeneratedSnapshot);
  if (!self.UI.context.flavor(SDK.HeapProfilerModel)) {
    await new Promise(resolve => self.UI.context.addFlavorChangeListener(SDK.HeapProfilerModel, resolve));
  }
  profileType._takeHeapSnapshot();
};

/**
 * @return {!Promise<!Profiler.HeapProfileHeader>}
 */
HeapProfilerTestRunner.takeSnapshotPromise = function() {
  return new Promise(resolve => {
    const heapProfileType = Profiler.ProfileTypeRegistry.instance.heapSnapshotProfileType;
    heapProfileType.addEventListener(Profiler.HeapSnapshotProfileType.SnapshotReceived, finishHeapSnapshot);
    heapProfileType._takeHeapSnapshot();

    function finishHeapSnapshot() {
      const profiles = heapProfileType.getProfiles();
      if (!profiles.length) {
        throw 'FAILED: no profiles found.';
      }
      if (profiles.length > 1) {
        throw `FAILED: wrong number of recorded profiles was found. profiles.length = ${profiles.length}`;
      }
      const profile = profiles[0];
      UI.panels.heap_profiler.showProfile(profile);

      const dataGrid = HeapProfilerTestRunner.currentProfileView()._dataGrid;
      dataGrid.addEventListener(Profiler.HeapSnapshotSortableDataGrid.Events.SortingComplete, sortingComplete, null);

      function sortingComplete() {
        dataGrid.removeEventListener(
            Profiler.HeapSnapshotSortableDataGrid.Events.SortingComplete, sortingComplete, null);
        resolve(profile);
      }
    }
  });
};

HeapProfilerTestRunner.viewColumns = function() {
  return HeapProfilerTestRunner._currentGrid()._columnsArray;
};

HeapProfilerTestRunner.currentProfileView = function() {
  return UI.panels.heap_profiler.visibleView;
};

HeapProfilerTestRunner._currentGrid = function() {
  return this.currentProfileView()._dataGrid;
};

HeapProfilerTestRunner._snapshotViewShown = function() {
  if (HeapProfilerTestRunner._takeAndOpenSnapshotCallback) {
    const callback = HeapProfilerTestRunner._takeAndOpenSnapshotCallback;
    HeapProfilerTestRunner._takeAndOpenSnapshotCallback = null;
    const dataGrid = this._dataGrid;

    function sortingComplete() {
      dataGrid.removeEventListener(Profiler.HeapSnapshotSortableDataGrid.Events.SortingComplete, sortingComplete, null);
      callback();
    }

    dataGrid.addEventListener(Profiler.HeapSnapshotSortableDataGrid.Events.SortingComplete, sortingComplete, null);
  }
};

HeapProfilerTestRunner.showProfileWhenAdded = function(title) {
  HeapProfilerTestRunner._showProfileWhenAdded = title;
  return new Promise(resolve => HeapProfilerTestRunner._waitUntilProfileViewIsShown(title, resolve));
};

HeapProfilerTestRunner._profileHeaderAdded = function(profile) {
  if (HeapProfilerTestRunner._showProfileWhenAdded === profile.title) {
    UI.panels.heap_profiler.showProfile(profile);
  }
};

HeapProfilerTestRunner._waitUntilProfileViewIsShown = function(title, callback) {
  callback = TestRunner.safeWrap(callback);
  const profilesPanel = UI.panels.heap_profiler;

  if (profilesPanel.visibleView && profilesPanel.visibleView.profile &&
      profilesPanel.visibleView.profileHeader.title === title) {
    callback(profilesPanel.visibleView);
  } else {
    HeapProfilerTestRunner._waitUntilProfileViewIsShownCallback = {title: title, callback: callback};
  }
};

HeapProfilerTestRunner._profileViewRefresh = function() {
  if (HeapProfilerTestRunner._waitUntilProfileViewIsShownCallback &&
      HeapProfilerTestRunner._waitUntilProfileViewIsShownCallback.title === this.profileHeader.title) {
    const callback = HeapProfilerTestRunner._waitUntilProfileViewIsShownCallback;
    delete HeapProfilerTestRunner._waitUntilProfileViewIsShownCallback;
    callback.callback(this);
  }
};

HeapProfilerTestRunner.startSamplingHeapProfiler = async function() {
  if (!self.UI.context.flavor(SDK.HeapProfilerModel)) {
    await new Promise(resolve => self.UI.context.addFlavorChangeListener(SDK.HeapProfilerModel, resolve));
  }
  Profiler.SamplingHeapProfileType.instance._startRecordingProfile();
};

HeapProfilerTestRunner.stopSamplingHeapProfiler = function() {
  Profiler.SamplingHeapProfileType.instance._stopRecordingProfile();
};
