[go: nahoru, domu]

blob: 7ddb037192be7ed300bacabf53ad50659494292f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import type * as puppeteer from 'puppeteer';
import {$, platform, waitForElementWithTextContent} from '../../shared/helper.js';
import {
$$,
click,
clickElement,
getBrowserAndPages,
pasteText,
waitFor,
waitForFunction,
waitForNone,
} from '../../shared/helper.js';
const NEW_HEAP_SNAPSHOT_BUTTON = 'button[aria-label="Take heap snapshot"]';
const MEMORY_PANEL_CONTENT = 'div[aria-label="Memory panel"]';
const PROFILE_TREE_SIDEBAR = 'div.profiles-tree-sidebar';
export const MEMORY_TAB_ID = '#tab-heap_profiler';
const CLASS_FILTER_INPUT = 'div[aria-placeholder="Class filter"]';
export async function navigateToMemoryTab() {
await click(MEMORY_TAB_ID);
await waitFor(MEMORY_PANEL_CONTENT);
await waitFor(PROFILE_TREE_SIDEBAR);
}
export async function takeAllocationProfile() {
const radioButton = await $('//label[text()="Allocation sampling"]', undefined, 'xpath');
await clickElement(radioButton);
await click('button[aria-label="Start heap profiling"]');
await new Promise(r => setTimeout(r, 200));
await click('button[aria-label="Stop heap profiling"]');
await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
await waitFor('.heap-snapshot-sidebar-tree-item.selected');
}
export async function takeAllocationTimelineProfile({recordStacks}: {recordStacks: boolean} = {
recordStacks: false,
}) {
const radioButton = await $('//label[text()="Allocation instrumentation on timeline"]', undefined, 'xpath');
await clickElement(radioButton);
if (recordStacks) {
await click('[title="Record stack traces of allocations (extra performance overhead)"]');
}
await click('button[aria-label="Start recording heap profile"]');
await new Promise(r => setTimeout(r, 200));
await click('button[aria-label="Stop recording heap profile"]');
await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
await waitFor('.heap-snapshot-sidebar-tree-item.selected');
}
export async function takeHeapSnapshot() {
await click(NEW_HEAP_SNAPSHOT_BUTTON);
await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
await waitFor('.heap-snapshot-sidebar-tree-item.selected');
}
export async function waitForHeapSnapshotData() {
await waitFor('#profile-views');
await waitFor('#profile-views .data-grid');
const rowCountMatches = async () => {
const rows = await getDataGridRows('#profile-views table.data');
if (rows.length > 0) {
return rows;
}
return undefined;
};
return await waitForFunction(rowCountMatches);
}
export async function waitForNonEmptyHeapSnapshotData() {
const rows = await waitForHeapSnapshotData();
assert.isTrue(rows.length > 0);
}
export async function getDataGridRows(selector: string) {
// The grid in Memory Tab contains a tree
const grid = await waitFor(selector);
return await $$('.data-grid-data-grid-node', grid);
}
export async function setClassFilter(text: string) {
const classFilter = await waitFor(CLASS_FILTER_INPUT);
await classFilter.focus();
void pasteText(text);
}
export async function triggerLocalFindDialog(frontend: puppeteer.Page) {
switch (platform) {
case 'mac':
await frontend.keyboard.down('Meta');
break;
default:
await frontend.keyboard.down('Control');
}
await frontend.keyboard.press('f');
switch (platform) {
case 'mac':
await frontend.keyboard.up('Meta');
break;
default:
await frontend.keyboard.up('Control');
}
}
export async function setSearchFilter(text: string) {
const {frontend} = getBrowserAndPages();
const grid = await waitFor('#profile-views table.data');
await grid.focus();
await triggerLocalFindDialog(frontend);
const SEARCH_QUERY = '[aria-label="Find"]';
const inputElement = await waitFor(SEARCH_QUERY);
if (!inputElement) {
assert.fail('Unable to find search input field');
}
await inputElement.focus();
await inputElement.type(text);
}
export async function waitForSearchResultNumber(results: number) {
const findMatch = async () => {
const currentMatch = await waitFor('label[for=\'search-input-field\']');
const currentTextContent = currentMatch && await currentMatch.evaluate(el => el.textContent);
if (currentTextContent && currentTextContent.endsWith(` ${results}`)) {
return currentMatch;
}
return undefined;
};
return await waitForFunction(findMatch);
}
export async function findSearchResult(searchResult: string, pollIntrerval: number = 500) {
const match = await waitFor('#profile-views table.data');
await waitForFunction(async () => {
await click('[aria-label="Search next"]');
const result = Promise.race([
waitForElementWithTextContent(searchResult, match),
new Promise(resolve => {
setTimeout(resolve, pollIntrerval, false);
}),
]);
return result;
});
}
const normalizRetainerName = (retainerName: string) => {
// Retainers starting with `Window /` might have host information in their
// name, including the port, so we need to strip that. We can't distinguish
// Window from Window / either, because on Mac it is often just Window.
if (retainerName.startsWith('Window /')) {
return 'Window';
}
// Retainers including double-colons :: are names from the C++ implementation
// exposed through V8's gn arg `cppgc_enable_object_names`; these should be
// considered implementation details, so we normalize them.
if (retainerName.includes('::')) {
if (retainerName.startsWith('Detached')) {
return 'Detached InternalNode';
}
return 'InternalNode';
}
return retainerName;
};
interface RetainerChainEntry {
propertyName: string;
retainerClassName: string;
}
export async function assertRetainerChainSatisfies(p: (retainerChain: Array<RetainerChainEntry>) => boolean) {
// Give some time for the expansion to finish.
const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data');
const retainerChain = [];
for (let i = 0; i < retainerGridElements.length; ++i) {
const retainer = retainerGridElements[i];
const propertyName = await retainer.$eval('span.property-name', el => el.textContent);
const rawRetainerClassName = await retainer.$eval('span.value', el => el.textContent);
if (!propertyName) {
assert.fail('Could not get retainer name');
}
if (!rawRetainerClassName) {
assert.fail('Could not get retainer value');
}
const retainerClassName = normalizRetainerName(rawRetainerClassName);
retainerChain.push({propertyName, retainerClassName});
if (await retainer.evaluate(el => !el.classList.contains('expanded'))) {
// Only follow the shortest retainer chain to the end. This relies on
// the retainer view behavior that auto-expands the shortest retaining
// chain.
break;
}
}
return p(retainerChain);
}
export async function waitUntilRetainerChainSatisfies(p: (retainerChain: Array<RetainerChainEntry>) => boolean) {
await waitForFunction(assertRetainerChainSatisfies.bind(null, p));
}
export function appearsInOrder(targetArray: string[], inputArray: string[]) {
let i = 0;
let j = 0;
if (inputArray.length > targetArray.length) {
return false;
}
if (inputArray === targetArray) {
return true;
}
while (i < targetArray.length && j < inputArray.length) {
if (inputArray[j] === targetArray[i]) {
j++;
}
i++;
}
if (j === inputArray.length) {
return true;
}
return false;
}
export async function waitForRetainerChain(expectedRetainers: Array<string>) {
await waitForFunction(assertRetainerChainSatisfies.bind(null, retainerChain => {
const actual = retainerChain.map(e => e.retainerClassName);
return appearsInOrder(actual, expectedRetainers);
}));
}
export async function changeViewViaDropdown(newPerspective: string) {
const perspectiveDropdownSelector = 'select[aria-label="Perspective"]';
const dropdown = await waitFor(perspectiveDropdownSelector) as puppeteer.ElementHandle<HTMLSelectElement>;
const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
const optionValue = await optionToSelect.evaluate(opt => opt.getAttribute('value'));
if (!optionValue) {
throw new Error(`Could not find heap snapshot perspective option: ${newPerspective}`);
}
await dropdown.select(optionValue);
}
export async function changeAllocationSampleViewViaDropdown(newPerspective: string) {
const perspectiveDropdownSelector = 'select[aria-label="Profile view mode"]';
const dropdown = await waitFor(perspectiveDropdownSelector) as puppeteer.ElementHandle<HTMLSelectElement>;
const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
const optionValue = await optionToSelect.evaluate(opt => opt.getAttribute('value'));
if (!optionValue) {
throw new Error(`Could not find heap snapshot perspective option: ${newPerspective}`);
}
await dropdown.select(optionValue);
}