Move Memory test to E2E non hosted
Fixed: 416405547
Change-Id: I7d84ed6a64f4c3a2d2c1bed1c96d1b7134f61bb1
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6759939
Auto-Submit: Nikolay Vitkov <[email protected]>
Reviewed-by: Simon Zünd <[email protected]>
Commit-Queue: Simon Zünd <[email protected]>
diff --git a/test/e2e/BUILD.gn b/test/e2e/BUILD.gn
index c8ceed5..ef050bb 100644
--- a/test/e2e/BUILD.gn
+++ b/test/e2e/BUILD.gn
@@ -24,7 +24,6 @@
"host",
"issues",
"lighthouse",
- "memory",
"network",
"panels",
"performance",
diff --git a/test/e2e/helpers/memory-helpers.ts b/test/e2e/helpers/memory-helpers.ts
index 1ae6f6c..6e0fdd8 100644
--- a/test/e2e/helpers/memory-helpers.ts
+++ b/test/e2e/helpers/memory-helpers.ts
@@ -6,19 +6,9 @@
import type * as puppeteer from 'puppeteer-core';
import {
- $,
- $$,
- click,
- clickElement,
- getBrowserAndPages,
- pasteText,
platform,
- waitFor,
- waitForAria,
- waitForElementWithTextContent,
- waitForFunction,
- waitForNone,
} from '../../shared/helper.js';
+import {getBrowserAndPagesWrappers} from '../../shared/non_hosted_wrappers.js';
const NEW_HEAP_SNAPSHOT_BUTTON = 'devtools-button[aria-label="Take heap snapshot"]';
const MEMORY_PANEL_CONTENT = 'div[aria-label="Memory panel"]';
@@ -27,114 +17,115 @@
const CLASS_FILTER_INPUT = 'div[aria-placeholder="Filter by class"]';
const SELECTED_RESULT = '#profile-views table.data tr.data-grid-data-grid-node.revealed.parent.selected';
-export async function navigateToMemoryTab() {
- await click(MEMORY_TAB_ID);
- await waitFor(MEMORY_PANEL_CONTENT);
- await waitFor(PROFILE_TREE_SIDEBAR);
+export async function navigateToMemoryTab(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ await devToolsPage.click(MEMORY_TAB_ID);
+ await devToolsPage.waitFor(MEMORY_PANEL_CONTENT);
+ await devToolsPage.waitFor(PROFILE_TREE_SIDEBAR);
}
-export async function takeDetachedElementsProfile() {
- const radioButton = await $('//label[text()="Detached elements"]', undefined, 'xpath');
- await clickElement(radioButton);
- await click('devtools-button[aria-label="Obtain detached elements"]');
- await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
- await waitFor('.heap-snapshot-sidebar-tree-item.selected');
+export async function takeDetachedElementsProfile(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const radioButton = await devToolsPage.$('//label[text()="Detached elements"]', undefined, 'xpath');
+ await devToolsPage.clickElement(radioButton);
+ await devToolsPage.click('devtools-button[aria-label="Obtain detached elements"]');
+ await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+ await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
}
-export async function takeAllocationProfile() {
- const radioButton = await $('//label[text()="Allocation sampling"]', undefined, 'xpath');
- await clickElement(radioButton);
- await click('devtools-button[aria-label="Start heap profiling"]');
+export async function takeAllocationProfile(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const radioButton = await devToolsPage.$('//label[text()="Allocation sampling"]', undefined, 'xpath');
+ await devToolsPage.clickElement(radioButton);
+ await devToolsPage.click('devtools-button[aria-label="Start heap profiling"]');
await new Promise(r => setTimeout(r, 200));
- await click('devtools-button[aria-label="Stop heap profiling"]');
- await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
- await waitFor('.heap-snapshot-sidebar-tree-item.selected');
+ await devToolsPage.click('devtools-button[aria-label="Stop heap profiling"]');
+ await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+ await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
}
-export async function takeAllocationTimelineProfile({recordStacks}: {recordStacks: boolean} = {
- recordStacks: false,
-}) {
- const radioButton = await $('//label[text()="Allocations on timeline"]', undefined, 'xpath');
- await clickElement(radioButton);
+export async function takeAllocationTimelineProfile(
+ {recordStacks}: {recordStacks: boolean} = {
+ recordStacks: false,
+ },
+ devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const radioButton = await devToolsPage.$('//label[text()="Allocations on timeline"]', undefined, 'xpath');
+ await devToolsPage.clickElement(radioButton);
if (recordStacks) {
- await click('[title="Allocation stack traces (more overhead)"]');
+ await devToolsPage.click('[title="Allocation stack traces (more overhead)"]');
}
- await click('devtools-button[aria-label="Start recording heap profile"]');
+ await devToolsPage.click('devtools-button[aria-label="Start recording heap profile"]');
await new Promise(r => setTimeout(r, 200));
- await click('devtools-button[aria-label="Stop recording heap profile"]');
- await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
- await waitFor('.heap-snapshot-sidebar-tree-item.selected');
+ await devToolsPage.click('devtools-button[aria-label="Stop recording heap profile"]');
+ await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+ await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
}
-export async function takeHeapSnapshot(name = 'Snapshot 1') {
- await click(NEW_HEAP_SNAPSHOT_BUTTON);
- await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
- await waitForFunction(async () => {
- const selected = await waitFor('.heap-snapshot-sidebar-tree-item.selected');
- const title = await waitFor('span.title', selected);
+export async function takeHeapSnapshot(name = 'Snapshot 1', devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ await devToolsPage.click(NEW_HEAP_SNAPSHOT_BUTTON);
+ await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+ await devToolsPage.waitForFunction(async () => {
+ const selected = await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
+ const title = await devToolsPage.waitFor('span.title', selected);
return (await title.evaluate(e => e.textContent)) === name ? title : undefined;
});
}
-export async function waitForHeapSnapshotData() {
- await waitFor('#profile-views');
- await waitFor('#profile-views .data-grid');
+export async function waitForHeapSnapshotData(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ await devToolsPage.waitFor('#profile-views');
+ await devToolsPage.waitFor('#profile-views .data-grid');
const rowCountMatches = async () => {
- const rows = await getDataGridRows('#profile-views table.data');
+ const rows = await getDataGridRows('#profile-views table.data', devToolsPage);
if (rows.length > 0) {
return rows;
}
return undefined;
};
- return await waitForFunction(rowCountMatches);
+ return await devToolsPage.waitForFunction(rowCountMatches);
}
-export async function waitForNonEmptyHeapSnapshotData() {
- const rows = await waitForHeapSnapshotData();
+export async function waitForNonEmptyHeapSnapshotData(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const rows = await waitForHeapSnapshotData(devToolsPage);
assert.isTrue(rows.length > 0);
}
-export async function getDataGridRows(selector: string) {
+export async function getDataGridRows(selector: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
// The grid in Memory Tab contains a tree
- const grid = await waitFor(selector);
- return await $$('.data-grid-data-grid-node', grid);
+ const grid = await devToolsPage.waitFor(selector);
+ return await devToolsPage.$$('.data-grid-data-grid-node', grid);
}
-export async function setClassFilter(text: string) {
- const classFilter = await waitFor(CLASS_FILTER_INPUT);
+export async function setClassFilter(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const classFilter = await devToolsPage.waitFor(CLASS_FILTER_INPUT);
await classFilter.focus();
- void pasteText(text);
+ void devToolsPage.pasteText(text);
}
-export async function triggerLocalFindDialog(frontend: puppeteer.Page) {
+export async function triggerLocalFindDialog(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
switch (platform) {
case 'mac':
- await frontend.keyboard.down('Meta');
+ await devToolsPage.page.keyboard.down('Meta');
break;
default:
- await frontend.keyboard.down('Control');
+ await devToolsPage.page.keyboard.down('Control');
}
- await frontend.keyboard.press('f');
+ await devToolsPage.page.keyboard.press('f');
switch (platform) {
case 'mac':
- await frontend.keyboard.up('Meta');
+ await devToolsPage.page.keyboard.up('Meta');
break;
default:
- await frontend.keyboard.up('Control');
+ await devToolsPage.page.keyboard.up('Control');
}
}
-export async function setSearchFilter(text: string) {
- const {frontend} = getBrowserAndPages();
- const grid = await waitFor('#profile-views table.data');
+export async function setSearchFilter(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const grid = await devToolsPage.waitFor('#profile-views table.data');
await grid.focus();
- await triggerLocalFindDialog(frontend);
+ await triggerLocalFindDialog(devToolsPage);
const SEARCH_QUERY = '[aria-label="Find"]';
- const inputElement = await waitFor(SEARCH_QUERY);
+ const inputElement = await devToolsPage.waitFor(SEARCH_QUERY);
if (!inputElement) {
assert.fail('Unable to find search input field');
}
@@ -142,42 +133,43 @@
await inputElement.type(text);
}
-export async function waitForSearchResultNumber(results: number) {
+export async function waitForSearchResultNumber(
+ results: number, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
const findMatch = async () => {
- const currentMatch = await waitFor('.search-results-matches');
+ const currentMatch = await devToolsPage.waitFor('.search-results-matches');
const currentTextContent = currentMatch && await currentMatch.evaluate(el => el.textContent);
if (currentTextContent?.endsWith(` ${results}`)) {
return currentMatch;
}
return undefined;
};
- return await waitForFunction(findMatch);
+ return await devToolsPage.waitForFunction(findMatch);
}
-export async function findSearchResult(searchResult: string, pollIntrerval = 500) {
- const {frontend} = getBrowserAndPages();
- const match = await waitFor('#profile-views table.data');
- const matches = await waitFor(' .search-results-matches');
+export async function findSearchResult(
+ searchResult: string, pollIntrerval = 500, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const match = await devToolsPage.waitFor('#profile-views table.data');
+ const matches = await devToolsPage.waitFor(' .search-results-matches');
const matchesText = await matches.evaluate(async element => {
return element.textContent;
});
if (matchesText === '1 of 1') {
- await waitForElementWithTextContent(searchResult, match);
+ await devToolsPage.waitForElementWithTextContent(searchResult, match);
} else {
- await waitForFunction(async () => {
- const selectedBefore = await waitFor(SELECTED_RESULT);
- await click('[aria-label="Show next result"]');
+ await devToolsPage.waitForFunction(async () => {
+ const selectedBefore = await devToolsPage.waitFor(SELECTED_RESULT);
+ await devToolsPage.click('[aria-label="Show next result"]');
// Wait until the click has taken effect by checking that the selected
// result has changed. This is done to prevent the assertion afterwards
// from happening before the next result is fully loaded.
- await waitForFunction(async () => {
- const selectedAfter = await waitFor(SELECTED_RESULT);
- return await frontend.evaluate((b, a) => {
+ await devToolsPage.waitForFunction(async () => {
+ const selectedAfter = await devToolsPage.waitFor(SELECTED_RESULT);
+ return await devToolsPage.page.evaluate((b, a) => {
return b !== a;
}, selectedBefore, selectedAfter);
});
const result = Promise.race([
- waitForElementWithTextContent(searchResult, match),
+ devToolsPage.waitForElementWithTextContent(searchResult, match),
new Promise(resolve => {
setTimeout(resolve, pollIntrerval, false);
}),
@@ -211,9 +203,10 @@
retainerClassName: string;
}
-export async function assertRetainerChainSatisfies(p: (retainerChain: RetainerChainEntry[]) => boolean) {
+export async function assertRetainerChainSatisfies(
+ p: (retainerChain: RetainerChainEntry[]) => boolean, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
// Give some time for the expansion to finish.
- const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data');
+ const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
const retainerChain = [];
for (let i = 0; i < retainerGridElements.length; ++i) {
const retainer = retainerGridElements[i];
@@ -237,8 +230,9 @@
return p(retainerChain);
}
-export async function waitUntilRetainerChainSatisfies(p: (retainerChain: RetainerChainEntry[]) => boolean) {
- await waitForFunction(assertRetainerChainSatisfies.bind(null, p));
+export async function waitUntilRetainerChainSatisfies(
+ p: (retainerChain: RetainerChainEntry[]) => boolean, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ await devToolsPage.waitForFunction(assertRetainerChainSatisfies.bind(null, p, devToolsPage));
}
export function appearsInOrder(targetArray: string[], inputArray: string[]) {
@@ -266,18 +260,20 @@
return false;
}
-export async function waitForRetainerChain(expectedRetainers: string[]) {
- await waitForFunction(assertRetainerChainSatisfies.bind(null, retainerChain => {
+export async function waitForRetainerChain(
+ expectedRetainers: string[], devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ await devToolsPage.waitForFunction(assertRetainerChainSatisfies.bind(null, retainerChain => {
const actual = retainerChain.map(e => e.retainerClassName);
return appearsInOrder(actual, expectedRetainers);
- }));
+ }, devToolsPage));
}
-export async function changeViewViaDropdown(newPerspective: string) {
+export async function changeViewViaDropdown(
+ newPerspective: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
const perspectiveDropdownSelector = 'select[aria-label="Perspective"]';
- const dropdown = await waitFor(perspectiveDropdownSelector);
+ const dropdown = await devToolsPage.waitFor(perspectiveDropdownSelector);
- const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
+ const optionToSelect = await devToolsPage.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}`);
@@ -285,10 +281,13 @@
await dropdown.select(optionValue);
}
-export async function changeAllocationSampleViewViaDropdown(newPerspective: string) {
+export async function changeAllocationSampleViewViaDropdown(
+ newPerspective: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
const perspectiveDropdownSelector = 'select[aria-label="Profile view mode"]';
- const dropdown = await waitFor(perspectiveDropdownSelector);
- const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
+ const dropdown = await devToolsPage.waitFor(
+ perspectiveDropdownSelector,
+ );
+ const optionToSelect = await devToolsPage.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}`);
@@ -296,21 +295,21 @@
await dropdown.select(optionValue);
}
-export async function focusTableRowWithName(text: string) {
- const row = await waitFor(`//span[text()="${text}"]/ancestor::tr`, undefined, undefined, 'xpath');
- await focusTableRow(row);
+export async function focusTableRowWithName(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const row = await devToolsPage.waitFor(`//span[text()="${text}"]/ancestor::tr`, undefined, undefined, 'xpath');
+ await focusTableRow(row, devToolsPage);
}
-export async function focusTableRow(row: puppeteer.ElementHandle<Element>) {
+export async function focusTableRow(
+ row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
// Click in a numeric cell, to avoid accidentally clicking a link.
- const cell = await waitFor('.numeric-column', row);
- await clickElement(cell);
+ const cell = await devToolsPage.waitFor('.numeric-column', row);
+ await devToolsPage.clickElement(cell);
}
-export async function expandFocusedRow() {
- const {frontend} = getBrowserAndPages();
- await frontend.keyboard.press('ArrowRight');
- await waitFor('.selected.data-grid-data-grid-node.expanded');
+export async function expandFocusedRow(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ await devToolsPage.page.keyboard.press('ArrowRight');
+ await devToolsPage.waitFor('.selected.data-grid-data-grid-node.expanded');
}
function parseByteString(str: string): number {
@@ -327,8 +326,9 @@
return number;
}
-async function getSizesFromRow(row: puppeteer.ElementHandle<Element>) {
- const numericData = await $$('.numeric-column>.profile-multiple-values>span', row);
+async function getSizesFromRow(
+ row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const numericData = await devToolsPage.$$('.numeric-column>.profile-multiple-values>span', row);
assert.lengthOf(numericData, 4);
function readNumber(e: Element): string {
return e.textContent as string;
@@ -339,74 +339,83 @@
return {shallowSize, retainedSize};
}
-export async function getSizesFromSelectedRow() {
- const row = await waitFor('.selected.data-grid-data-grid-node');
- return await getSizesFromRow(row);
+export async function getSizesFromSelectedRow(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const row = await devToolsPage.waitFor('.selected.data-grid-data-grid-node');
+ return await getSizesFromRow(row, devToolsPage);
}
-export async function getCategoryRow(text: string, wait = true) {
+export async function getCategoryRow(
+ text: string, wait = true, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
const selector = `//td[text()="${text}"]/ancestor::tr`;
- return await (wait ? waitFor(selector, undefined, undefined, 'xpath') : $(selector, undefined, 'xpath'));
+ return await (wait ? devToolsPage.waitFor(selector, undefined, undefined, 'xpath') :
+ devToolsPage.$(selector, undefined, 'xpath'));
}
-export async function getSizesFromCategoryRow(text: string) {
- const row = await getCategoryRow(text);
- return await getSizesFromRow(row);
+export async function getSizesFromCategoryRow(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const row = await getCategoryRow(text, undefined, devToolsPage);
+ return await getSizesFromRow(row, devToolsPage);
}
-export async function getDistanceFromCategoryRow(text: string) {
- const row = await getCategoryRow(text);
- const numericColumns = await $$('.numeric-column', row);
+export async function getDistanceFromCategoryRow(
+ text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const row = await getCategoryRow(text, undefined, devToolsPage);
+ const numericColumns = await devToolsPage.$$('.numeric-column', row);
return await numericColumns[0].evaluate(e => parseInt(e.textContent as string, 10));
}
-export async function getCountFromCategoryRowWithName(text: string) {
- const row = await getCategoryRow(text);
- return await getCountFromCategoryRow(row);
+export async function getCountFromCategoryRowWithName(
+ text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const row = await getCategoryRow(text, undefined, devToolsPage);
+ return await getCountFromCategoryRow(row, devToolsPage);
}
-export async function getCountFromCategoryRow(row: puppeteer.ElementHandle<Element>) {
- const countSpan = await waitFor('.objects-count', row);
+export async function getCountFromCategoryRow(
+ row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const countSpan = await devToolsPage.waitFor('.objects-count', row);
return await countSpan.evaluate(e => parseInt((e.textContent ?? '').substring(1), 10));
}
-export async function getAddedCountFromComparisonRowWithName(text: string) {
- const row = await getCategoryRow(text);
- return await getAddedCountFromComparisonRow(row);
+export async function getAddedCountFromComparisonRowWithName(
+ text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const row = await getCategoryRow(text, undefined, devToolsPage);
+ return await getAddedCountFromComparisonRow(row, devToolsPage);
}
-export async function getAddedCountFromComparisonRow(row: puppeteer.ElementHandle<Element>) {
- const addedCountCell = await waitFor('.addedCount-column', row);
+export async function getAddedCountFromComparisonRow(
+ row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const addedCountCell = await devToolsPage.waitFor('.addedCount-column', row);
const countText = await addedCountCell.evaluate(e => e.textContent ?? '');
return parseByteString(countText);
}
-export async function getRemovedCountFromComparisonRow(row: puppeteer.ElementHandle<Element>) {
- const addedCountCell = await waitFor('.removedCount-column', row);
+export async function getRemovedCountFromComparisonRow(
+ row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const addedCountCell = await devToolsPage.waitFor('.removedCount-column', row);
const countText = await addedCountCell.evaluate(e => e.textContent ?? '');
return parseByteString(countText);
}
-export async function clickOnContextMenuForRetainer(retainerName: string, menuItem: string) {
- const retainersPane = await waitFor('.retaining-paths-view');
- const element = await waitFor(`//span[text()="${retainerName}"]`, retainersPane, undefined, 'xpath');
+export async function clickOnContextMenuForRetainer(
+ retainerName: string, menuItem: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const retainersPane = await devToolsPage.waitFor('.retaining-paths-view');
+ const element = await devToolsPage.waitFor(`//span[text()="${retainerName}"]`, retainersPane, undefined, 'xpath');
// Push the click right a bit further to avoid the disclosure triangle.
- await clickElement(element, {clickOptions: {button: 'right', offset: {x: 35, y: 0}}});
- const button = await waitForAria(menuItem);
- await clickElement(button);
+ await devToolsPage.clickElement(element, {clickOptions: {button: 'right', offset: {x: 35, y: 0}}});
+ const button = await devToolsPage.waitForAria(menuItem);
+ await devToolsPage.clickElement(button);
}
-export async function restoreIgnoredRetainers() {
- const element = await waitFor('devtools-button[aria-label="Restore ignored retainers"]');
- await clickElement(element);
+export async function restoreIgnoredRetainers(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const element = await devToolsPage.waitFor('devtools-button[aria-label="Restore ignored retainers"]');
+ await devToolsPage.clickElement(element);
}
-export async function setFilterDropdown(filter: string) {
- const select = await waitFor('devtools-toolbar select[aria-label="Filter"]');
+export async function setFilterDropdown(filter: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const select = await devToolsPage.waitFor('devtools-toolbar select[aria-label="Filter"]');
await select.select(filter);
}
-export async function checkExposeInternals() {
- const element = await waitForElementWithTextContent('Internals with implementation details');
- await clickElement(element);
+export async function checkExposeInternals(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+ const element = await devToolsPage.waitForElementWithTextContent('Internals with implementation details');
+ await devToolsPage.clickElement(element);
}
diff --git a/test/e2e/memory/memory_test.ts b/test/e2e/memory/memory_test.ts
deleted file mode 100644
index f9dd276..0000000
--- a/test/e2e/memory/memory_test.ts
+++ /dev/null
@@ -1,615 +0,0 @@
-// 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-core';
-
-import {
- $,
- $$,
- assertNotNullOrUndefined,
- clickElement,
- drainFrontendTaskQueue,
- enableExperiment,
- getBrowserAndPages,
- goToResource,
- step,
- waitFor,
- waitForElementsWithTextContent,
- waitForElementWithTextContent,
- waitForFunction,
- waitForMany,
- waitForNoElementsWithTextContent,
-} from '../../shared/helper.js';
-import {
- changeAllocationSampleViewViaDropdown,
- changeViewViaDropdown,
- checkExposeInternals,
- clickOnContextMenuForRetainer,
- expandFocusedRow,
- findSearchResult,
- focusTableRow,
- focusTableRowWithName,
- getAddedCountFromComparisonRow,
- getAddedCountFromComparisonRowWithName,
- getCategoryRow,
- getCountFromCategoryRow,
- getCountFromCategoryRowWithName,
- getDataGridRows,
- getDistanceFromCategoryRow,
- getRemovedCountFromComparisonRow,
- getSizesFromCategoryRow,
- getSizesFromSelectedRow,
- navigateToMemoryTab,
- restoreIgnoredRetainers,
- setClassFilter,
- setFilterDropdown,
- setSearchFilter,
- takeAllocationProfile,
- takeAllocationTimelineProfile,
- takeDetachedElementsProfile,
- takeHeapSnapshot,
- waitForNonEmptyHeapSnapshotData,
- waitForRetainerChain,
- waitForSearchResultNumber,
- waitUntilRetainerChainSatisfies,
-} from '../helpers/memory-helpers.js';
-
-describe('The Memory Panel', function() {
- // These tests render large chunks of data into DevTools and filter/search
- // through it. On bots with less CPU power, these can fail because the
- // rendering takes a long time, so we allow a much larger timeout.
- if (this.timeout() !== 0) {
- this.timeout(100000);
- }
-
- it('Loads content', async () => {
- await goToResource('memory/default.html');
- await navigateToMemoryTab();
- });
-
- it('Can take several heap snapshots ', async () => {
- await goToResource('memory/default.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await takeHeapSnapshot('Snapshot 2');
- await waitForNonEmptyHeapSnapshotData();
- const heapSnapShots = await $$('.heap-snapshot-sidebar-tree-item');
- assert.lengthOf(heapSnapShots, 2);
- });
-
- it('Shows a DOM node and its JS wrapper as a single node', async () => {
- await goToResource('memory/detached-node.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('leaking');
- await waitForSearchResultNumber(4);
- await findSearchResult('leaking()');
- await waitForRetainerChain([
- 'Detached V8EventListener',
- 'Detached EventListener',
- 'Detached InternalNode',
- 'Detached InternalNode',
- 'Detached <div>',
- 'Retainer',
- 'Window',
- ]);
- });
-
- it('Correctly retains the path for event listeners', async () => {
- await goToResource('memory/event-listeners.html');
- await step('taking a heap snapshot', async () => {
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- });
- await step('searching for the event listener', async () => {
- await setSearchFilter('myEventListener');
- await waitForSearchResultNumber(4);
- });
-
- await step('selecting the search result that we need', async () => {
- await findSearchResult('myEventListener()');
- });
-
- await step('waiting for retainer chain', async () => {
- await waitForRetainerChain([
- 'V8EventListener',
- 'EventListener',
- 'InternalNode',
- 'InternalNode',
- '<body>',
- ]);
- });
- });
-
- it('Puts all ActiveDOMObjects with pending activities into one group', async () => {
- const {frontend} = getBrowserAndPages();
- await goToResource('memory/dom-objects.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- // The test ensures that the following structure is present:
- // Pending activities
- // -> Pending activities
- // -> InternalNode
- // -> MediaQueryList
- // -> MediaQueryList
- await setSearchFilter('Pending activities');
- // Here and below we have to wait until the elements are actually created
- // and visible.
- await waitForFunction(async () => {
- const pendingActivitiesSpan = await waitFor('//span[text()="Pending activities"]', undefined, undefined, 'xpath');
- const pendingActiviesRow = await waitFor('ancestor-or-self::tr', pendingActivitiesSpan, undefined, 'xpath');
- try {
- await clickElement(pendingActivitiesSpan);
- } catch {
- return false;
- }
- const res = await pendingActiviesRow.evaluate(x => x.classList.toString());
- return res.includes('selected');
- });
- await frontend.keyboard.press('ArrowRight');
- const internalNodeSpan = await waitFor(
- '//span[text()="InternalNode"][ancestor-or-self::tr[preceding-sibling::*[1][//span[text()="Pending activities"]]]]',
- undefined, undefined, 'xpath');
- const internalNodeRow = await $('ancestor-or-self::tr', internalNodeSpan, 'xpath');
- await waitForFunction(async () => {
- await clickElement(internalNodeSpan);
- const res = await internalNodeRow.evaluate(x => x.classList.toString());
- return res.includes('selected');
- });
- await frontend.keyboard.press('ArrowRight');
- await waitForFunction(async () => {
- const pendingActiviesChildren = await waitForElementsWithTextContent('MediaQueryList');
- return pendingActiviesChildren.length === 2;
- });
- });
-
- it('Shows the correct number of divs for a detached DOM tree correctly', async () => {
- await goToResource('memory/detached-dom-tree.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('Detached <div>');
- await waitForSearchResultNumber(3);
- });
-
- it('Shows the correct output for an attached iframe', async () => {
- await goToResource('memory/attached-iframe.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('searchable string');
- await waitForSearchResultNumber(1);
- // The following line checks two things: That the property 'aUniqueName'
- // in the iframe is retaining the Retainer class object, and that the
- // iframe window is not detached.
- await waitUntilRetainerChainSatisfies(
- retainerChain => retainerChain.some(
- ({propertyName, retainerClassName}) => propertyName === 'aUniqueName' && retainerClassName === 'Window'));
- });
-
- // Flaky on win and linux
- it.skip('[crbug.com/40238574] Correctly shows multiple retainer paths for an object', async () => {
- await goToResource('memory/multiple-retainers.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('leaking');
- await waitForSearchResultNumber(4);
- await findSearchResult('\"leaking\"');
-
- await waitForFunction(async () => {
- // Wait for all the rows of the data-grid to load.
- const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data');
- return retainerGridElements.length === 9;
- });
-
- const sharedInLeakingElementRow = await waitForFunction(async () => {
- const results = await getDataGridRows('.retaining-paths-view table.data');
- const findPromises = await Promise.all(results.map(async e => {
- const textContent = await e.evaluate(el => el.textContent);
- // Can't search for "shared in leaking()" because the different parts are spaced with CSS.
- return textContent?.startsWith('sharedinleaking()') ? e : null;
- }));
- return findPromises.find(result => result !== null);
- });
-
- if (!sharedInLeakingElementRow) {
- assert.fail('Could not find data-grid row with "shared in leaking()" text.');
- }
-
- const textOfEl = await sharedInLeakingElementRow.evaluate(e => e.textContent || '');
- // Double check we got the right element to avoid a confusing text failure
- // later down the line.
- assert.isTrue(textOfEl.startsWith('sharedinleaking()'));
-
- // Have to click it not in the middle as the middle can hold the link to the
- // file in the sources pane and we want to avoid clicking that.
- await clickElement(sharedInLeakingElementRow /* TODO(crbug.com/1363150): {maxPixelsFromLeft: 10} */);
- const {frontend} = getBrowserAndPages();
- // Expand the data-grid for the shared list
- await frontend.keyboard.press('ArrowRight');
-
- // check that we found two V8EventListener objects
- await waitForFunction(async () => {
- const pendingActiviesChildren = await waitForElementsWithTextContent('V8EventListener');
- return pendingActiviesChildren.length === 2;
- });
-
- // Now we want to get the two rows below the "shared in leaking()" row and assert on them.
- // Unfortunately they are not structured in the data-grid as children, despite being children in the UI
- // So the best way to get at them is to grab the two subsequent siblings of the "shared in leaking()" row.
- const nextRow = (await sharedInLeakingElementRow.evaluateHandle(e => e.nextSibling)).asElement() as
- puppeteer.ElementHandle<HTMLElement>;
- if (!nextRow) {
- assert.fail('Could not find row below "shared in leaking()" row');
- }
- const nextNextRow =
- (await nextRow.evaluateHandle(e => e.nextSibling)).asElement() as puppeteer.ElementHandle<HTMLElement>;
- if (!nextNextRow) {
- assert.fail('Could not find 2nd row below "shared in leaking()" row');
- }
-
- const childText = await Promise.all([nextRow, nextNextRow].map(async row => await row.evaluate(r => r.innerText)));
-
- assert.isTrue(childText[0].includes('inV8EventListener'));
- assert.isTrue(childText[1].includes('inEventListener'));
- });
-
- // Flaky test causing build failures
- it.skip('[crbug.com/40193901] Shows the correct output for a detached iframe', async () => {
- await goToResource('memory/detached-iframe.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('Leak');
- await waitForSearchResultNumber(8);
- await waitUntilRetainerChainSatisfies(
- retainerChain => retainerChain.some(({retainerClassName}) => retainerClassName === 'Detached Window'));
- });
-
- it('Shows a tooltip', async () => {
- await goToResource('memory/detached-dom-tree.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('Detached <div>');
- await waitForSearchResultNumber(3);
- await waitUntilRetainerChainSatisfies(retainerChain => {
- return retainerChain.length > 0 && retainerChain[0].propertyName === 'retaining_wrapper';
- });
- const rows = await getDataGridRows('.retaining-paths-view table.data');
- const propertyNameElement = await rows[0].$('span.property-name');
- await propertyNameElement!.hover();
- const el = await waitFor('div.vbox.flex-auto.no-pointer-events');
- await waitFor('.source-code', el);
-
- await setSearchFilter('system / descriptorarray');
- await findSearchResult('system / DescriptorArray');
- const searchResultElement = await waitFor('.selected.data-grid-data-grid-node span.object-value-null');
- await searchResultElement!.hover();
- await waitFor('.widget .object-popover-footer');
- });
-
- it('shows the list of a detached node', async () => {
- await goToResource('memory/detached-node.html');
- await navigateToMemoryTab();
- void takeDetachedElementsProfile();
- await waitFor('.detached-elements-view');
- });
-
- it('shows the flamechart for an allocation sample', async () => {
- await goToResource('memory/allocations.html');
- await navigateToMemoryTab();
- void takeAllocationProfile();
- void changeAllocationSampleViewViaDropdown('Chart');
- await waitFor('canvas.flame-chart-canvas');
- });
-
- it('shows allocations for an allocation timeline', async () => {
- await goToResource('memory/allocations.html');
- await navigateToMemoryTab();
- void takeAllocationTimelineProfile({recordStacks: true});
- await changeViewViaDropdown('Allocation');
-
- const header = await waitForElementWithTextContent('Live Count');
- const table = await header.evaluateHandle(node => {
- return node.closest('.data-grid')!;
- });
- await waitFor('.data-grid-data-grid-node', table);
- });
-
- it('does not show allocations perspective when stacks not recorded', async () => {
- await goToResource('memory/allocations.html');
- await navigateToMemoryTab();
- void takeAllocationTimelineProfile({recordStacks: false});
- const dropdown = await waitFor('select[aria-label="Perspective"]');
- await waitForNoElementsWithTextContent('Allocation', dropdown);
- });
-
- it('shows object source links in snapshot', async () => {
- const {target, frontend} = getBrowserAndPages();
- await target.evaluate(`
- class MyTestClass {
- constructor() {
- this.z = new Uint32Array(1e6); // Pull the class to top.
- this.myFunction = () => 42;
- }
- };
- function* MyTestGenerator() {
- yield 1;
- }
- class MyTestClass2 {}
- window.myTestClass = new MyTestClass();
- window.myTestGenerator = MyTestGenerator();
- window.myTestClass2 = new MyTestClass2();
- //# sourceURL=my-test-script.js`);
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await setClassFilter('MyTest');
- await waitForNonEmptyHeapSnapshotData();
-
- const expectedEntries = [
- {constructor: 'MyTestClass', link: 'my-test-script.js:3'},
- {constructor: 'MyTestClass', prop: 'myFunction', link: 'my-test-script.js:5'},
- {constructor: 'MyTestGenerator', link: 'my-test-script.js:8'},
- {constructor: 'MyTestClass2', link: 'my-test-script.js:11'},
- ];
-
- const rows = await getDataGridRows('.data-grid');
- for (const entry of expectedEntries) {
- let row: puppeteer.ElementHandle<Element>|null = null;
- // Find the row with the desired constructor.
- for (const r of rows) {
- const constructorName = await waitForFunction(() => r.evaluate(e => e.firstChild?.textContent));
- if (entry.constructor === constructorName) {
- row = r;
- break;
- }
- }
- assertNotNullOrUndefined(row);
- // Expand the constructor sub-tree.
- await clickElement(row);
- await frontend.keyboard.press('ArrowRight');
- // Get the object subtree/child.
- const {objectElement, objectName} = await waitForFunction(async () => {
- const objectElement =
- await row?.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
- const objectName = await objectElement?.evaluate(e => e.querySelector('.object-value-object')?.textContent);
- if (!objectName) {
- return undefined;
- }
- return {objectElement, objectName};
- });
- let element = objectElement;
- assertNotNullOrUndefined(element);
- // Verify we have the object with the matching name.
- assert.strictEqual(objectName, entry.constructor);
- // Get the right property of the object if required.
- if (entry.prop) {
- // Expand the object.
- await clickElement(element);
- await frontend.keyboard.press('ArrowRight');
- // Try to find the property.
- element = await waitForFunction(async () => {
- let row = element;
- while (row) {
- const nextRow = await row.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
- if (!nextRow) {
- return undefined;
- }
- row = nextRow;
- const text = await row.evaluate(e => e.querySelector('.property-name')?.textContent);
- // If we did not find any text at all, then we saw all properties. Let us fail/retry here.
- if (!text) {
- return undefined;
- }
- // If we found the property, we are done.
- if (text === entry.prop) {
- return row;
- }
- // Continue looking for the property on the next row.
- }
- return undefined;
- });
- assertNotNullOrUndefined(element);
- }
-
- // Verify the link to the source code.
- const linkText = await waitForFunction(
- async () => await element?.evaluate(e => e.querySelector('.devtools-link')?.textContent));
- assert.strictEqual(linkText, entry.link);
- }
- });
-
- async function runJSSetTest() {
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('Retainer');
- await waitForSearchResultNumber(4);
- await findSearchResult('Retainer()');
- await focusTableRowWithName('Retainer()');
- await expandFocusedRow();
- await focusTableRowWithName('customProperty');
- const sizesForSet = await getSizesFromSelectedRow();
- await expandFocusedRow();
- await focusTableRowWithName('(internal array)[]');
- const sizesForBackingStorage = await getSizesFromSelectedRow();
- return {sizesForSet, sizesForBackingStorage};
- }
-
- it('Does not include backing store size in the shallow size of a JS Set', async () => {
- await goToResource('memory/set.html');
- await enableExperiment('show-option-tp-expose-internals-in-heap-snapshot');
- await navigateToMemoryTab();
- await checkExposeInternals();
- const sizes = await runJSSetTest();
-
- // The Set object is small, regardless of the contained content.
- assert.isTrue(sizes.sizesForSet.shallowSize <= 100);
- // The Set retains its backing storage.
- // Note: 16 bytes is added to retainedSize to account for rounding present in the UI layer.
- assert.isTrue(
- sizes.sizesForSet.retainedSize + 16 >=
- sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
- // The backing storage contains 100 items, which occupy at least one pointer per item.
- assert.isTrue(sizes.sizesForBackingStorage.shallowSize >= 400);
- // The backing storage retains 100 strings, which occupy at least 16 bytes each.
- assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= sizes.sizesForBackingStorage.shallowSize + 1600);
- });
-
- it('Includes backing store size in the shallow size of a JS Set', async () => {
- await goToResource('memory/set.html');
- const sizes = await runJSSetTest();
-
- // The Set is reported as containing at least 100 pointers.
- assert.isTrue(sizes.sizesForSet.shallowSize >= 400);
- // The Set retains its backing storage.
- assert.isTrue(
- sizes.sizesForSet.retainedSize >= sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
- // The backing storage is reported as zero size.
- assert.strictEqual(sizes.sizesForBackingStorage.shallowSize, 0);
- // The backing storage retains 100 strings, which occupy at least 16 bytes each.
- assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= 1600);
- });
-
- it('Computes distances and sizes for WeakMap values correctly', async () => {
- await goToResource('memory/weakmap.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setClassFilter('CustomClass');
- assert.strictEqual(5, await getDistanceFromCategoryRow('CustomClass1'));
- assert.strictEqual(6, await getDistanceFromCategoryRow('CustomClass2'));
- assert.strictEqual(2, await getDistanceFromCategoryRow('CustomClass3'));
- assert.strictEqual(8, await getDistanceFromCategoryRow('CustomClass4'));
- assert.isTrue((await getSizesFromCategoryRow('CustomClass1Key')).retainedSize >= 2 ** 15);
- assert.isTrue((await getSizesFromCategoryRow('CustomClass2Key')).retainedSize >= 2 ** 15);
- assert.isTrue((await getSizesFromCategoryRow('CustomClass3Key')).retainedSize < 2 ** 15);
- assert.isTrue((await getSizesFromCategoryRow('CustomClass4Key')).retainedSize < 2 ** 15);
- assert.isTrue((await getSizesFromCategoryRow('CustomClass4Retainer')).retainedSize >= 2 ** 15);
- });
-
- it('Allows ignoring retainers', async () => {
- await goToResource('memory/ignoring-retainers.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setSearchFilter('searchable_string');
- await waitForSearchResultNumber(2);
- await findSearchResult('"searchable_string"');
- await waitForRetainerChain(['{y}', 'KeyType', 'Window']);
- await clickOnContextMenuForRetainer('KeyType', 'Ignore this retainer');
- await waitForRetainerChain(['{y}', '{x}', 'Window']);
- await clickOnContextMenuForRetainer('x', 'Ignore this retainer');
- await waitForRetainerChain(['{y}', '(internal array)[]', 'WeakMap', 'Window']);
- await clickOnContextMenuForRetainer('(internal array)[]', 'Ignore this retainer');
- await waitForRetainerChain([
- '{y}',
- '{d}',
- `{${'#'.repeat(130)}, …}`,
- '{b, irrelevantProperty, <symbol also irrelevant>, "}"}',
- '{a, extraProp0, extraProp1, extraProp2, extraProp3, …, extraProp6, extraProp7, extraProp8, extraProp9}',
- 'Window',
- ]);
- await clickOnContextMenuForRetainer('b', 'Ignore this retainer');
- await waitForRetainerChain(['(Internalized strings)', '(GC roots)']);
- await restoreIgnoredRetainers();
- await waitForRetainerChain(['{y}', 'KeyType', 'Window']);
- });
-
- it('Can filter the summary view', async () => {
- await goToResource('memory/filtering.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setFilterDropdown('Duplicated strings');
- await setSearchFilter('"duplicatedKey":"duplicatedValue"');
- await waitForSearchResultNumber(2);
- await setFilterDropdown('Objects retained by detached DOM nodes');
- await getCategoryRow('ObjectRetainedByDetachedDom');
- assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false));
- await setFilterDropdown('Objects retained by DevTools Console');
- await getCategoryRow('ObjectRetainedByConsole');
- assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false));
- });
-
- it('Groups HTML elements by tag name', async () => {
- await goToResource('memory/dom-details.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setClassFilter('<div>');
- assert.strictEqual(3, await getCountFromCategoryRowWithName('<div>'));
- assert.strictEqual(3, await getCountFromCategoryRowWithName('Detached <div>'));
- await setSearchFilter('Detached <div data-x="p" data-y="q">');
- await waitForSearchResultNumber(1);
- });
-
- it('Groups plain JS objects by interface', async () => {
- await goToResource('memory/diff.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- await setClassFilter('{a, b, c, d, ');
- // Objects should be grouped by interface if there are at least two matching instances.
- assert.strictEqual(2, await getCountFromCategoryRowWithName('{a, b, c, d, p, q, r}'));
- assert.isNotOk(await getCategoryRow('{a, b, c, d, e}', /* wait:*/ false));
- const {frontend, target} = getBrowserAndPages();
- await target.bringToFront();
- await target.click('button#update');
- await frontend.bringToFront();
- await takeHeapSnapshot('Snapshot 2');
- await waitForNonEmptyHeapSnapshotData();
- await changeViewViaDropdown('Comparison');
- await setClassFilter('{a, b, c, d, ');
- // When comparing, the old snapshot is categorized according to the new one's interfaces,
- // so the comparison should report only one new object of the following type, not two.
- assert.strictEqual(1, await getAddedCountFromComparisonRowWithName('{a, b, c, d, e}'));
- // Only one of these objects remains, so it's no longer a category.
- assert.isNotOk(await getCategoryRow('{a, b, c, d, p, q, r}', /* wait:*/ false));
- });
-
- it('Groups objects by constructor location', async () => {
- await goToResource('memory/duplicated-names.html');
- await navigateToMemoryTab();
- await takeHeapSnapshot();
- await waitForNonEmptyHeapSnapshotData();
- // TODO: filtering does not work while UI is rendering snapshot.
- await drainFrontendTaskQueue();
- await setClassFilter('DuplicatedClassName');
- let rows = await waitForMany('tr.data-grid-data-grid-node', 3);
- assert.strictEqual(30, await getCountFromCategoryRow(rows[0]));
- assert.strictEqual(3, await getCountFromCategoryRow(rows[1]));
- assert.strictEqual(2, await getCountFromCategoryRow(rows[2]));
- await focusTableRow(rows[0]);
- await expandFocusedRow();
- // TODO: pressing arrowDown does not work while UI is rendering.
- await drainFrontendTaskQueue();
- const {frontend, target} = getBrowserAndPages();
- await frontend.keyboard.press('ArrowDown');
- await clickOnContextMenuForRetainer('x', 'Reveal in Summary view');
- await waitUntilRetainerChainSatisfies(
- retainerChain => retainerChain.length > 0 && retainerChain[0].propertyName === 'a');
- await target.bringToFront();
- await target.click('button#update');
- await frontend.bringToFront();
- await takeHeapSnapshot('Snapshot 2');
- await waitForNonEmptyHeapSnapshotData();
- await changeViewViaDropdown('Comparison');
- await setClassFilter('DuplicatedClassName');
- rows = await waitForMany('tr.data-grid-data-grid-node', 3);
- assert.strictEqual(5, await getAddedCountFromComparisonRow(rows[0]));
- assert.strictEqual(1, await getRemovedCountFromComparisonRow(rows[0]));
- assert.strictEqual(1, await getAddedCountFromComparisonRow(rows[1]));
- assert.strictEqual(10, await getRemovedCountFromComparisonRow(rows[1]));
- assert.strictEqual(0, await getAddedCountFromComparisonRow(rows[2]));
- assert.strictEqual(2, await getRemovedCountFromComparisonRow(rows[2]));
- });
-});
diff --git a/test/e2e/network/network-request-view_test.ts b/test/e2e/network/network-request-view_test.ts
index 22b545c..85b7dcb 100644
--- a/test/e2e/network/network-request-view_test.ts
+++ b/test/e2e/network/network-request-view_test.ts
@@ -660,7 +660,7 @@
const SEARCH_RESULT = '.search-result';
const {frontend} = getBrowserAndPages();
- await triggerLocalFindDialog(frontend);
+ await triggerLocalFindDialog();
await waitFor(SEARCH_QUERY);
const inputElement = await $(SEARCH_QUERY);
if (!inputElement) {
diff --git a/test/e2e_non_hosted/BUILD.gn b/test/e2e_non_hosted/BUILD.gn
index 97ff69c..82269be 100644
--- a/test/e2e_non_hosted/BUILD.gn
+++ b/test/e2e_non_hosted/BUILD.gn
@@ -30,6 +30,7 @@
"layers",
"lighthouse",
"media",
+ "memory",
"network",
"performance",
"puppeteer",
diff --git a/test/e2e/memory/BUILD.gn b/test/e2e_non_hosted/memory/BUILD.gn
similarity index 79%
rename from test/e2e/memory/BUILD.gn
rename to test/e2e_non_hosted/memory/BUILD.gn
index d798524..3917f25 100644
--- a/test/e2e/memory/BUILD.gn
+++ b/test/e2e_non_hosted/memory/BUILD.gn
@@ -4,11 +4,11 @@
import("../../../scripts/build/typescript/typescript.gni")
-node_ts_library("memory") {
+ts_e2e_library("memory") {
sources = [ "memory_test.ts" ]
deps = [
- "../../shared",
- "../helpers",
+ "../../e2e/helpers",
+ "../shared",
]
}
diff --git a/test/e2e/memory/DIR_METADATA b/test/e2e_non_hosted/memory/DIR_METADATA
similarity index 100%
rename from test/e2e/memory/DIR_METADATA
rename to test/e2e_non_hosted/memory/DIR_METADATA
diff --git a/test/e2e_non_hosted/memory/memory_test.ts b/test/e2e_non_hosted/memory/memory_test.ts
new file mode 100644
index 0000000..cea0d64
--- /dev/null
+++ b/test/e2e_non_hosted/memory/memory_test.ts
@@ -0,0 +1,619 @@
+// 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-core';
+
+import {
+ changeAllocationSampleViewViaDropdown,
+ changeViewViaDropdown,
+ checkExposeInternals,
+ clickOnContextMenuForRetainer,
+ expandFocusedRow,
+ findSearchResult,
+ focusTableRow,
+ focusTableRowWithName,
+ getAddedCountFromComparisonRow,
+ getAddedCountFromComparisonRowWithName,
+ getCategoryRow,
+ getCountFromCategoryRow,
+ getCountFromCategoryRowWithName,
+ getDataGridRows,
+ getDistanceFromCategoryRow,
+ getRemovedCountFromComparisonRow,
+ getSizesFromCategoryRow,
+ getSizesFromSelectedRow,
+ navigateToMemoryTab,
+ restoreIgnoredRetainers,
+ setClassFilter,
+ setFilterDropdown,
+ setSearchFilter,
+ takeAllocationProfile,
+ takeAllocationTimelineProfile,
+ takeDetachedElementsProfile,
+ takeHeapSnapshot,
+ waitForNonEmptyHeapSnapshotData,
+ waitForRetainerChain,
+ waitForSearchResultNumber,
+ waitUntilRetainerChainSatisfies,
+} from '../../e2e/helpers/memory-helpers.js';
+import {
+ assertNotNullOrUndefined,
+ step,
+} from '../../shared/helper.js';
+import type {DevToolsPage} from '../shared/frontend-helper.js';
+
+async function runJSSetTest(devToolsPage: DevToolsPage) {
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('Retainer', devToolsPage);
+ await waitForSearchResultNumber(4, devToolsPage);
+ await findSearchResult('Retainer()', undefined, devToolsPage);
+ await focusTableRowWithName('Retainer()', devToolsPage);
+ await expandFocusedRow(devToolsPage);
+ await focusTableRowWithName('customProperty', devToolsPage);
+ const sizesForSet = await getSizesFromSelectedRow(devToolsPage);
+ await expandFocusedRow(devToolsPage);
+ await focusTableRowWithName('(internal array)[]', devToolsPage);
+ const sizesForBackingStorage = await getSizesFromSelectedRow(devToolsPage);
+ return {sizesForSet, sizesForBackingStorage};
+}
+
+describe('The Memory Panel', function() {
+ // These tests render large chunks of data into DevTools and filter/search
+ // through it. On bots with less CPU power, these can fail because the
+ // rendering takes a long time, so we allow a much larger timeout.
+ if (this.timeout() !== 0) {
+ this.timeout(20_000);
+ }
+
+ setup({dockingMode: 'undocked'});
+
+ it('Loads content', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/default.html');
+ await navigateToMemoryTab(devToolsPage);
+ });
+
+ it('Can take several heap snapshots ', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/default.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await takeHeapSnapshot('Snapshot 2', devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ const heapSnapShots = await devToolsPage.$$('.heap-snapshot-sidebar-tree-item');
+ assert.lengthOf(heapSnapShots, 2);
+ });
+
+ it('Shows a DOM node and its JS wrapper as a single node', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/detached-node.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('leaking', devToolsPage);
+ await waitForSearchResultNumber(4, devToolsPage);
+ await findSearchResult('leaking()', undefined, devToolsPage);
+ await waitForRetainerChain(
+ [
+ 'Detached V8EventListener',
+ 'Detached EventListener',
+ 'Detached InternalNode',
+ 'Detached InternalNode',
+ 'Detached <div>',
+ 'Retainer',
+ 'Window',
+ ],
+ devToolsPage);
+ });
+
+ it('Correctly retains the path for event listeners', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/event-listeners.html');
+ await step('taking a heap snapshot', async () => {
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ });
+ await step('searching for the event listener', async () => {
+ await setSearchFilter('myEventListener', devToolsPage);
+ await waitForSearchResultNumber(4, devToolsPage);
+ });
+
+ await step('selecting the search result that we need', async () => {
+ await findSearchResult('myEventListener()', undefined, devToolsPage);
+ });
+
+ await step('waiting for retainer chain', async () => {
+ await waitForRetainerChain(
+ [
+ 'V8EventListener',
+ 'EventListener',
+ 'InternalNode',
+ 'InternalNode',
+ '<body>',
+ ],
+ devToolsPage);
+ });
+ });
+
+ it('Puts all ActiveDOMObjects with pending activities into one group', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/dom-objects.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ // The test ensures that the following structure is present:
+ // Pending activities
+ // -> Pending activities
+ // -> InternalNode
+ // -> MediaQueryList
+ // -> MediaQueryList
+ await setSearchFilter('Pending activities', devToolsPage);
+ // Here and below we have to wait until the elements are actually created
+ // and visible.
+ await devToolsPage.waitForFunction(async () => {
+ const pendingActivitiesSpan =
+ await devToolsPage.waitFor('//span[text()="Pending activities"]', undefined, undefined, 'xpath');
+ const pendingActiviesRow =
+ await devToolsPage.waitFor('ancestor-or-self::tr', pendingActivitiesSpan, undefined, 'xpath');
+ try {
+ await devToolsPage.clickElement(pendingActivitiesSpan);
+ } catch {
+ return false;
+ }
+ const res = await pendingActiviesRow.evaluate(x => x.classList.toString());
+ return res.includes('selected');
+ });
+ await devToolsPage.page.keyboard.press('ArrowRight');
+ const internalNodeSpan = await devToolsPage.waitFor(
+ '//span[text()="InternalNode"][ancestor-or-self::tr[preceding-sibling::*[1][//span[text()="Pending activities"]]]]',
+ undefined, undefined, 'xpath');
+ const internalNodeRow = await devToolsPage.$('ancestor-or-self::tr', internalNodeSpan, 'xpath');
+ await devToolsPage.waitForFunction(async () => {
+ await devToolsPage.clickElement(internalNodeSpan);
+ const res = await internalNodeRow.evaluate(x => x.classList.toString());
+ return res.includes('selected');
+ });
+ await devToolsPage.page.keyboard.press('ArrowRight');
+ await devToolsPage.waitForFunction(async () => {
+ const pendingActiviesChildren = await devToolsPage.waitForElementsWithTextContent('MediaQueryList');
+ return pendingActiviesChildren.length === 2;
+ });
+ });
+
+ it('Shows the correct number of divs for a detached DOM tree correctly', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/detached-dom-tree.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('Detached <div>', devToolsPage);
+ await waitForSearchResultNumber(3, devToolsPage);
+ });
+
+ it('Shows the correct output for an attached iframe', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/attached-iframe.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('searchable string', devToolsPage);
+ await waitForSearchResultNumber(1, devToolsPage);
+ // The following line checks two things: That the property 'aUniqueName'
+ // in the iframe is retaining the Retainer class object, and that the
+ // iframe window is not detached.
+ await waitUntilRetainerChainSatisfies(
+ retainerChain => retainerChain.some(
+ ({propertyName, retainerClassName}) => propertyName === 'aUniqueName' && retainerClassName === 'Window'),
+ devToolsPage);
+ });
+
+ // Flaky on win and linux
+ it.skip(
+ '[crbug.com/40238574] Correctly shows multiple retainer paths for an object',
+ async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/multiple-retainers.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('leaking', devToolsPage);
+ await waitForSearchResultNumber(4, devToolsPage);
+ await findSearchResult('\"leaking\"', undefined, devToolsPage);
+
+ await devToolsPage.waitForFunction(async () => {
+ // Wait for all the rows of the data-grid to load.
+ const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
+ return retainerGridElements.length === 9;
+ });
+
+ const sharedInLeakingElementRow = await devToolsPage.waitForFunction(async () => {
+ const results = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
+ const findPromises = await Promise.all(results.map(async e => {
+ const textContent = await e.evaluate(el => el.textContent);
+ // Can't search for "shared in leaking()" because the different parts are spaced with CSS.
+ return textContent?.startsWith('sharedinleaking()') ? e : null;
+ }));
+ return findPromises.find(result => result !== null);
+ });
+
+ if (!sharedInLeakingElementRow) {
+ assert.fail('Could not find data-grid row with "shared in leaking()" text.');
+ }
+
+ const textOfEl = await sharedInLeakingElementRow.evaluate(e => e.textContent || '');
+ // Double check we got the right element to avoid a confusing text failure
+ // later down the line.
+ assert.isTrue(textOfEl.startsWith('sharedinleaking()'));
+
+ // Have to click it not in the middle as the middle can hold the link to the
+ // file in the sources pane and we want to avoid clicking that.
+ await devToolsPage.clickElement(
+ sharedInLeakingElementRow /* TODO(crbug.com/1363150): {maxPixelsFromLeft: 10} */);
+ // Expand the data-grid for the shared list
+ await devToolsPage.page.keyboard.press('ArrowRight');
+
+ // check that we found two V8EventListener objects
+ await devToolsPage.waitForFunction(async () => {
+ const pendingActiviesChildren = await devToolsPage.waitForElementsWithTextContent('V8EventListener');
+ return pendingActiviesChildren.length === 2;
+ });
+
+ // Now we want to get the two rows below the "shared in leaking()" row and assert on them.
+ // Unfortunately they are not structured in the data-grid as children, despite being children in the UI
+ // So the best way to get at them is to grab the two subsequent siblings of the "shared in leaking()" row.
+ const nextRow = (await sharedInLeakingElementRow.evaluateHandle(e => e.nextSibling)).asElement() as
+ puppeteer.ElementHandle<HTMLElement>;
+ if (!nextRow) {
+ assert.fail('Could not find row below "shared in leaking()" row');
+ }
+ const nextNextRow =
+ (await nextRow.evaluateHandle(e => e.nextSibling)).asElement() as puppeteer.ElementHandle<HTMLElement>;
+ if (!nextNextRow) {
+ assert.fail('Could not find 2nd row below "shared in leaking()" row');
+ }
+
+ const childText =
+ await Promise.all([nextRow, nextNextRow].map(async row => await row.evaluate(r => r.innerText)));
+
+ assert.isTrue(childText[0].includes('inV8EventListener'));
+ assert.isTrue(childText[1].includes('inEventListener'));
+ });
+
+ // Flaky test causing build failures
+ it.skip(
+ '[crbug.com/40193901] Shows the correct output for a detached iframe', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/detached-iframe.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('Leak', devToolsPage);
+ await waitForSearchResultNumber(8, devToolsPage);
+ await waitUntilRetainerChainSatisfies(
+ retainerChain => retainerChain.some(({retainerClassName}) => retainerClassName === 'Detached Window'),
+ devToolsPage,
+ );
+ });
+
+ it('Shows a tooltip', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/detached-dom-tree.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('Detached <div>', devToolsPage);
+ await waitForSearchResultNumber(3, devToolsPage);
+ await waitUntilRetainerChainSatisfies(retainerChain => {
+ return retainerChain.length > 0 && retainerChain[0].propertyName === 'retaining_wrapper';
+ }, devToolsPage);
+ const rows = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
+ const propertyNameElement = await rows[0].$('span.property-name');
+ await propertyNameElement!.hover();
+ const el = await devToolsPage.waitFor('div.vbox.flex-auto.no-pointer-events');
+ await devToolsPage.waitFor('.source-code', el);
+
+ await setSearchFilter('system / descriptorarray', devToolsPage);
+ await findSearchResult('system / DescriptorArray', undefined, devToolsPage);
+ const searchResultElement = await devToolsPage.waitFor('.selected.data-grid-data-grid-node span.object-value-null');
+ await searchResultElement!.hover();
+ await devToolsPage.waitFor('.widget .object-popover-footer');
+ });
+
+ it('shows the list of a detached node', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/detached-node.html');
+ await navigateToMemoryTab(devToolsPage);
+ void takeDetachedElementsProfile(devToolsPage);
+ await devToolsPage.waitFor('.detached-elements-view');
+ });
+
+ it('shows the flamechart for an allocation sample', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/allocations.html');
+ await navigateToMemoryTab(devToolsPage);
+ void takeAllocationProfile(devToolsPage);
+ void changeAllocationSampleViewViaDropdown('Chart', devToolsPage);
+ await devToolsPage.waitFor('canvas.flame-chart-canvas');
+ });
+
+ it('shows allocations for an allocation timeline', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/allocations.html');
+ await navigateToMemoryTab(devToolsPage);
+ void takeAllocationTimelineProfile({recordStacks: true}, devToolsPage);
+ await changeViewViaDropdown('Allocation', devToolsPage);
+
+ const header = await devToolsPage.waitForElementWithTextContent('Live Count');
+ const table = await header.evaluateHandle(node => {
+ return node.closest('.data-grid')!;
+ });
+ await devToolsPage.waitFor('.data-grid-data-grid-node', table);
+ });
+
+ it('does not show allocations perspective when stacks not recorded', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/allocations.html');
+ await navigateToMemoryTab(devToolsPage);
+ void takeAllocationTimelineProfile({recordStacks: false}, devToolsPage);
+ const dropdown = await devToolsPage.waitFor('select[aria-label="Perspective"]');
+ await devToolsPage.waitForNoElementsWithTextContent('Allocation', dropdown);
+ });
+
+ it('shows object source links in snapshot', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.evaluate(`
+ class MyTestClass {
+ constructor() {
+ this.z = new Uint32Array(1e6); // Pull the class to top.
+ this.myFunction = () => 42;
+ }
+ };
+ function* MyTestGenerator() {
+ yield 1;
+ }
+ class MyTestClass2 {}
+ window.myTestClass = new MyTestClass();
+ window.myTestGenerator = MyTestGenerator();
+ window.myTestClass2 = new MyTestClass2();
+ //# sourceURL=my-test-script.js`);
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await setClassFilter('MyTest', devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+
+ const expectedEntries = [
+ {constructor: 'MyTestClass', link: 'my-test-script.js:3'},
+ {constructor: 'MyTestClass', prop: 'myFunction', link: 'my-test-script.js:5'},
+ {constructor: 'MyTestGenerator', link: 'my-test-script.js:8'},
+ {constructor: 'MyTestClass2', link: 'my-test-script.js:11'},
+ ];
+
+ const rows = await getDataGridRows('.data-grid', devToolsPage);
+ for (const entry of expectedEntries) {
+ let row: puppeteer.ElementHandle<Element>|null = null;
+ // Find the row with the desired constructor.
+ for (const r of rows) {
+ const constructorName = await devToolsPage.waitForFunction(() => r.evaluate(e => e.firstChild?.textContent));
+ if (entry.constructor === constructorName) {
+ row = r;
+ break;
+ }
+ }
+ assertNotNullOrUndefined(row);
+ // Expand the constructor sub-tree.
+ await devToolsPage.clickElement(row);
+ await devToolsPage.page.keyboard.press('ArrowRight');
+ // Get the object subtree/child.
+ const {objectElement, objectName} = await devToolsPage.waitForFunction(async () => {
+ const objectElement =
+ await row?.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
+ const objectName = await objectElement?.evaluate(e => e.querySelector('.object-value-object')?.textContent);
+ if (!objectName) {
+ return undefined;
+ }
+ return {objectElement, objectName};
+ });
+ let element = objectElement;
+ assertNotNullOrUndefined(element);
+ // Verify we have the object with the matching name.
+ assert.strictEqual(objectName, entry.constructor);
+ // Get the right property of the object if required.
+ if (entry.prop) {
+ // Expand the object.
+ await devToolsPage.clickElement(element);
+ await devToolsPage.page.keyboard.press('ArrowRight');
+ // Try to find the property.
+ element = await devToolsPage.waitForFunction(async () => {
+ let row = element;
+ while (row) {
+ const nextRow = await row.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
+ if (!nextRow) {
+ return undefined;
+ }
+ row = nextRow;
+ const text = await row.evaluate(e => e.querySelector('.property-name')?.textContent);
+ // If we did not find any text at all, then we saw all properties. Let us fail/retry here.
+ if (!text) {
+ return undefined;
+ }
+ // If we found the property, we are done.
+ if (text === entry.prop) {
+ return row;
+ }
+ // Continue looking for the property on the next row.
+ }
+ return undefined;
+ });
+ assertNotNullOrUndefined(element);
+ }
+
+ // Verify the link to the source code.
+ const linkText = await devToolsPage.waitForFunction(
+ async () => await element?.evaluate(e => e.querySelector('.devtools-link')?.textContent));
+ assert.strictEqual(linkText, entry.link);
+ }
+ });
+
+ it('Includes backing store size in the shallow size of a JS Set', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/set.html');
+ const sizes = await runJSSetTest(devToolsPage);
+
+ // The Set is reported as containing at least 100 pointers.
+ assert.isTrue(sizes.sizesForSet.shallowSize >= 400);
+ // The Set retains its backing storage.
+ assert.isTrue(
+ sizes.sizesForSet.retainedSize >= sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
+ // The backing storage is reported as zero size.
+ assert.strictEqual(sizes.sizesForBackingStorage.shallowSize, 0);
+ // The backing storage retains 100 strings, which occupy at least 16 bytes each.
+ assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= 1600);
+ });
+
+ it('Computes distances and sizes for WeakMap values correctly', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/weakmap.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setClassFilter('CustomClass', devToolsPage);
+ assert.strictEqual(5, await getDistanceFromCategoryRow('CustomClass1', devToolsPage));
+ assert.strictEqual(6, await getDistanceFromCategoryRow('CustomClass2', devToolsPage));
+ assert.strictEqual(2, await getDistanceFromCategoryRow('CustomClass3', devToolsPage));
+ assert.strictEqual(8, await getDistanceFromCategoryRow('CustomClass4', devToolsPage));
+ assert.isTrue((await getSizesFromCategoryRow('CustomClass1Key', devToolsPage)).retainedSize >= 2 ** 15);
+ assert.isTrue((await getSizesFromCategoryRow('CustomClass2Key', devToolsPage)).retainedSize >= 2 ** 15);
+ assert.isTrue((await getSizesFromCategoryRow('CustomClass3Key', devToolsPage)).retainedSize < 2 ** 15);
+ assert.isTrue((await getSizesFromCategoryRow('CustomClass4Key', devToolsPage)).retainedSize < 2 ** 15);
+ assert.isTrue((await getSizesFromCategoryRow('CustomClass4Retainer', devToolsPage)).retainedSize >= 2 ** 15);
+ });
+
+ it('Allows ignoring retainers', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/ignoring-retainers.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setSearchFilter('searchable_string', devToolsPage);
+ await waitForSearchResultNumber(2, devToolsPage);
+ await findSearchResult('"searchable_string"', undefined, devToolsPage);
+ await waitForRetainerChain(['{y}', 'KeyType', 'Window'], devToolsPage);
+ await clickOnContextMenuForRetainer('KeyType', 'Ignore this retainer', devToolsPage);
+ await waitForRetainerChain(['{y}', '{x}', 'Window'], devToolsPage);
+ await clickOnContextMenuForRetainer('x', 'Ignore this retainer', devToolsPage);
+ await waitForRetainerChain(['{y}', '(internal array)[]', 'WeakMap', 'Window'], devToolsPage);
+ await clickOnContextMenuForRetainer('(internal array)[]', 'Ignore this retainer', devToolsPage);
+ await waitForRetainerChain(
+ [
+ '{y}',
+ '{d}',
+ `{${'#'.repeat(130)}, …}`,
+ '{b, irrelevantProperty, <symbol also irrelevant>, "}"}',
+ '{a, extraProp0, extraProp1, extraProp2, extraProp3, …, extraProp6, extraProp7, extraProp8, extraProp9}',
+ 'Window',
+ ],
+ devToolsPage);
+ await clickOnContextMenuForRetainer('b', 'Ignore this retainer', devToolsPage);
+ await waitForRetainerChain(['(Internalized strings)', '(GC roots)'], devToolsPage);
+ await restoreIgnoredRetainers(devToolsPage);
+ await waitForRetainerChain(['{y}', 'KeyType', 'Window'], devToolsPage);
+ });
+
+ it('Can filter the summary view', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/filtering.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setFilterDropdown('Duplicated strings', devToolsPage);
+ await setSearchFilter('"duplicatedKey":"duplicatedValue"', devToolsPage);
+ await waitForSearchResultNumber(2, devToolsPage);
+ await setFilterDropdown('Objects retained by detached DOM nodes', devToolsPage);
+ await getCategoryRow('ObjectRetainedByDetachedDom', undefined, devToolsPage);
+ assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false, devToolsPage));
+ await setFilterDropdown('Objects retained by DevTools Console', devToolsPage);
+ await getCategoryRow('ObjectRetainedByConsole', undefined, devToolsPage);
+ assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false, devToolsPage));
+ });
+
+ it('Groups HTML elements by tag name', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/dom-details.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setClassFilter('<div>', devToolsPage);
+ assert.strictEqual(3, await getCountFromCategoryRowWithName('<div>', devToolsPage));
+ assert.strictEqual(3, await getCountFromCategoryRowWithName('Detached <div>', devToolsPage));
+ await setSearchFilter('Detached <div data-x="p" data-y="q">', devToolsPage);
+ await waitForSearchResultNumber(1, devToolsPage);
+ });
+
+ it('Groups plain JS objects by interface', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/diff.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await setClassFilter('{a, b, c, d, ', devToolsPage);
+ // Objects should be grouped by interface if there are at least two matching instances.
+ assert.strictEqual(2, await getCountFromCategoryRowWithName('{a, b, c, d, p, q, r}', devToolsPage));
+ assert.isNotOk(await getCategoryRow('{a, b, c, d, e}', /* wait:*/ false, devToolsPage));
+ await inspectedPage.bringToFront();
+ await inspectedPage.page.click('button#update');
+ await devToolsPage.bringToFront();
+ await takeHeapSnapshot('Snapshot 2', devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await changeViewViaDropdown('Comparison', devToolsPage);
+ await setClassFilter('{a, b, c, d, ', devToolsPage);
+ // When comparing, the old snapshot is categorized according to the new one's interfaces,
+ // so the comparison should report only one new object of the following type, not two.
+ assert.strictEqual(1, await getAddedCountFromComparisonRowWithName('{a, b, c, d, e}', devToolsPage));
+ // Only one of these objects remains, so it's no longer a category.
+ assert.isNotOk(await getCategoryRow('{a, b, c, d, p, q, r}', /* wait:*/ false, devToolsPage));
+ });
+
+ it('Groups objects by constructor location', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/duplicated-names.html');
+ await navigateToMemoryTab(devToolsPage);
+ await takeHeapSnapshot(undefined, devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ // TODO: filtering does not work while UI is rendering snapshot.
+ await devToolsPage.drainTaskQueue();
+ await setClassFilter('DuplicatedClassName', devToolsPage);
+ let rows = await devToolsPage.waitForMany('tr.data-grid-data-grid-node', 3);
+ assert.strictEqual(30, await getCountFromCategoryRow(rows[0], devToolsPage));
+ assert.strictEqual(3, await getCountFromCategoryRow(rows[1], devToolsPage));
+ assert.strictEqual(2, await getCountFromCategoryRow(rows[2], devToolsPage));
+ await focusTableRow(rows[0], devToolsPage);
+ await expandFocusedRow(devToolsPage);
+ // TODO: pressing arrowDown does not work while UI is rendering.
+ await devToolsPage.drainTaskQueue();
+ await devToolsPage.page.keyboard.press('ArrowDown');
+ await clickOnContextMenuForRetainer('x', 'Reveal in Summary view', devToolsPage);
+ await waitUntilRetainerChainSatisfies(
+ retainerChain => retainerChain.length > 0 && retainerChain[0].propertyName === 'a', devToolsPage);
+ await inspectedPage.bringToFront();
+ await inspectedPage.page.click('button#update');
+ await devToolsPage.bringToFront();
+ await takeHeapSnapshot('Snapshot 2', devToolsPage);
+ await waitForNonEmptyHeapSnapshotData(devToolsPage);
+ await changeViewViaDropdown('Comparison', devToolsPage);
+ await setClassFilter('DuplicatedClassName', devToolsPage);
+ rows = await devToolsPage.waitForMany('tr.data-grid-data-grid-node', 3);
+ assert.strictEqual(5, await getAddedCountFromComparisonRow(rows[0], devToolsPage));
+ assert.strictEqual(1, await getRemovedCountFromComparisonRow(rows[0], devToolsPage));
+ assert.strictEqual(1, await getAddedCountFromComparisonRow(rows[1], devToolsPage));
+ assert.strictEqual(10, await getRemovedCountFromComparisonRow(rows[1], devToolsPage));
+ assert.strictEqual(0, await getAddedCountFromComparisonRow(rows[2], devToolsPage));
+ assert.strictEqual(2, await getRemovedCountFromComparisonRow(rows[2], devToolsPage));
+ });
+});
+
+describe('The Memory Panel with show-option-tp-expose-internals-in-heap-snapshot experiment', () => {
+ setup({dockingMode: 'undocked', enabledDevToolsExperiments: ['show-option-tp-expose-internals-in-heap-snapshot']});
+
+ it('Does not include backing store size in the shallow size of a JS Set', async ({devToolsPage, inspectedPage}) => {
+ await inspectedPage.goToResource('memory/set.html');
+ await navigateToMemoryTab(devToolsPage);
+ await checkExposeInternals(devToolsPage);
+ const sizes = await runJSSetTest(devToolsPage);
+
+ // The Set object is small, regardless of the contained content.
+ assert.isTrue(sizes.sizesForSet.shallowSize <= 100);
+ // The Set retains its backing storage.
+ // Note: 16 bytes is added to retainedSize to account for rounding present in the UI layer.
+ assert.isTrue(
+ sizes.sizesForSet.retainedSize + 16 >=
+ sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
+ // The backing storage contains 100 items, which occupy at least one pointer per item.
+ assert.isTrue(sizes.sizesForBackingStorage.shallowSize >= 400);
+ // The backing storage retains 100 strings, which occupy at least 16 bytes each.
+ assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= sizes.sizesForBackingStorage.shallowSize + 1600);
+ });
+});