blob: 9503fab687dbe32ef8384de038df1a556f633dc3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import type * as puppeteer from 'puppeteer-core';
import {
step,
} 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';
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(30_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()', '3 of 4', devToolsPage);
await waitForRetainerChain(
[
'Detached V8EventListener', 'Detached EventListener', 'Detached InternalNode', 'Detached InternalNode',
'Detached InternalNode', 'Detached <div>', 'Retainer',
`Window (global*) / localhost:${inspectedPage.serverPort}`,
`system / NativeContext / https://localhost:${inspectedPage.serverPort}`
],
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()', '3 of 4', 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 pendingActivitiesRow =
await devToolsPage.waitFor('ancestor-or-self::tr', pendingActivitiesSpan, undefined, 'xpath');
try {
await devToolsPage.clickElement(pendingActivitiesSpan);
} catch {
return false;
}
const res = await pendingActivitiesRow.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 pendingActivitiesChildren = await devToolsPage.waitForElementsWithTextContent('MediaQueryList');
return pendingActivitiesChildren.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 (global*) / ://`),
devToolsPage);
});
it('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\"', '2 of 4', 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 === 13;
});
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);
});
assert.isOk(sharedInLeakingElementRow, '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 pendingActivitiesChildren = await devToolsPage.waitForElementsWithTextContent('V8EventListener');
return pendingActivitiesChildren.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>;
assert.isOk(nextRow, 'Could not find row below "shared in leaking()" row');
const nextNextRow =
(await nextRow.evaluateHandle(e => e.nextSibling)).asElement() as puppeteer.ElementHandle<HTMLElement>;
assert.isOk(nextNextRow, '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'));
});
it('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(9, 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);
// Find the first one as these are system
await findSearchResult('system / DescriptorArray', /1 of/, devToolsPage);
await devToolsPage.hover('.selected.data-grid-data-grid-node span.object-value-null');
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;
}
}
assert.isOk(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;
assert.isOk(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;
});
assert.isOk(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(6, await getDistanceFromCategoryRow('CustomClass1', devToolsPage));
assert.strictEqual(7, await getDistanceFromCategoryRow('CustomClass2', devToolsPage));
assert.strictEqual(3, await getDistanceFromCategoryRow('CustomClass3', devToolsPage));
assert.strictEqual(9, 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"', '2 of 2', devToolsPage);
await waitForRetainerChain(
[
'{y}', 'KeyType', `Window (global*) / localhost:${inspectedPage.serverPort}`,
`system / NativeContext / https://localhost:${inspectedPage.serverPort}`
],
devToolsPage);
await clickOnContextMenuForRetainer('KeyType', 'Ignore this retainer', devToolsPage);
await waitForRetainerChain(
[
'{y}', '{x}', `Window (global*) / localhost:${inspectedPage.serverPort}`,
`system / NativeContext / https://localhost:${inspectedPage.serverPort}`
],
devToolsPage);
await clickOnContextMenuForRetainer('x', 'Ignore this retainer', devToolsPage);
await waitForRetainerChain(
[
'{y}', '(internal array)[]', 'WeakMap', `Window (global*) / localhost:${inspectedPage.serverPort}`,
`system / NativeContext / https://localhost:${inspectedPage.serverPort}`
],
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 (global*) / localhost:${inspectedPage.serverPort}`,
`system / NativeContext / https://localhost:${inspectedPage.serverPort}`,
],
devToolsPage);
await clickOnContextMenuForRetainer('b', 'Ignore this retainer', devToolsPage);
await waitForRetainerChain(['(Internalized strings)', '(GC roots)'], devToolsPage);
await restoreIgnoredRetainers(devToolsPage);
await waitForRetainerChain(
[
'{y}', 'KeyType', `Window (global*) / localhost:${inspectedPage.serverPort}`,
`system / NativeContext / https://localhost:${inspectedPage.serverPort}`
],
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));
await setFilterDropdown('Objects retained by Event Handlers', devToolsPage);
await getCategoryRow('ObjectRetainedByEventHandler', undefined, devToolsPage);
assert.isNotOk(await getCategoryRow('ObjectRetainedByConsole', false, devToolsPage));
assert.isNotOk(await getCategoryRow('ObjectRetainedByDetachedDom', false, devToolsPage));
const functionCategoryRow = await getCategoryRow('Function', undefined, devToolsPage);
await focusTableRow(functionCategoryRow, devToolsPage);
await expandFocusedRow(devToolsPage);
await focusTableRowWithName('handleEventForTest()', 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.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-to-expose-internals-in-heap-snapshot experiment', () => {
setup({dockingMode: 'undocked', enabledDevToolsExperiments: ['show-option-to-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);
// TODO: the backing storage seems to be the same as the shallow size
// going from Chrome 142.0.7421.0 to 142.0.7427.0.
assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= sizes.sizesForBackingStorage.shallowSize);
});
});