-
Notifications
You must be signed in to change notification settings - Fork 652
Expand file tree
/
Copy pathmemory-helpers.ts
More file actions
389 lines (340 loc) · 15.6 KB
/
memory-helpers.ts
File metadata and controls
389 lines (340 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
// 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 type {DevToolsPage} from '../shared/frontend-helper.js';
const NEW_HEAP_SNAPSHOT_BUTTON = 'devtools-button[aria-label="Take heap snapshot"]';
const MEMORY_PANEL_CONTENT = 'div[aria-label="Memory panel"]';
const PROFILE_TREE_SIDEBAR = 'div.profiles-tree-sidebar';
export const MEMORY_TAB_ID = '#tab-heap-profiler';
const CLASS_FILTER_INPUT = 'div[aria-placeholder="Filter by class"]';
export const SELECTED_RESULT = '#profile-views table.data tr.data-grid-data-grid-node.revealed.parent.selected';
export async function navigateToMemoryTab(devToolsPage: DevToolsPage) {
await devToolsPage.click(MEMORY_TAB_ID);
await devToolsPage.waitFor(MEMORY_PANEL_CONTENT);
await devToolsPage.waitFor(PROFILE_TREE_SIDEBAR);
}
export async function takeDetachedElementsProfile(devToolsPage: DevToolsPage) {
await devToolsPage.click('xpath///label[text()="Detached elements"]');
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(devToolsPage: DevToolsPage) {
await devToolsPage.click('xpath///label[text()="Allocation sampling"]');
await devToolsPage.click('devtools-button[aria-label="Start heap profiling"]');
await new Promise(r => setTimeout(r, 200));
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,
},
devToolsPage: DevToolsPage) {
await devToolsPage.click('xpath///label[text()="Allocations on timeline"]');
if (recordStacks) {
await devToolsPage.click('[title="Allocation stack traces (more overhead)"]');
}
await devToolsPage.click('devtools-button[aria-label="Start recording heap profile"]');
await new Promise(r => setTimeout(r, 200));
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', devToolsPage: 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(devToolsPage: DevToolsPage) {
await devToolsPage.waitFor('#profile-views');
await devToolsPage.waitFor('#profile-views .data-grid');
const rowCountMatches = async () => {
const rows = await getDataGridRows('#profile-views table.data', devToolsPage);
if (rows.length > 0) {
return rows;
}
return undefined;
};
return await devToolsPage.waitForFunction(rowCountMatches);
}
export async function waitForNonEmptyHeapSnapshotData(devToolsPage: DevToolsPage) {
const rows = await waitForHeapSnapshotData(devToolsPage);
assert.isTrue(rows.length > 0);
}
export async function getDataGridRows(selector: string, devToolsPage: DevToolsPage) {
// The grid in Memory Tab contains a tree
const grid = await devToolsPage.waitFor(selector);
return await devToolsPage.$$('.data-grid-data-grid-node', grid);
}
export async function setClassFilter(text: string, devToolsPage: DevToolsPage) {
const classFilter = await devToolsPage.waitFor(CLASS_FILTER_INPUT);
await classFilter.focus();
void devToolsPage.pasteText(text);
}
export async function setSearchFilter(text: string, devToolsPage: DevToolsPage) {
const grid = await devToolsPage.waitFor('#profile-views table.data');
await grid.focus();
await devToolsPage.pressKey('f', {control: true});
const SEARCH_QUERY = '[aria-label="Find"]';
const inputElement = await devToolsPage.waitFor(SEARCH_QUERY);
assert.isOk(inputElement, 'Unable to find search input field');
await inputElement.focus();
await inputElement.type(text);
}
export async function waitForSearchResultNumber(results: number, devToolsPage: DevToolsPage) {
const findMatch = async () => {
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 devToolsPage.waitForFunction(findMatch);
}
/**
*
* @param searchResult
* @param searchMatch Leave undefined if you want to go over all instances
* @param devToolsPage
*/
export async function findSearchResult(
searchResult: string, searchMatch: string|RegExp|undefined, devToolsPage: DevToolsPage) {
await devToolsPage.waitForFunction(async () => {
if (!searchMatch) {
const match = await devToolsPage.waitFor('#profile-views table.data');
const result = await devToolsPage.$textContent(searchResult, match);
if (result) {
return true;
}
} else {
const matches = await devToolsPage.waitFor('.search-results-matches');
const matchesText = await matches.evaluate(element => {
return element.textContent;
});
if (typeof searchMatch === 'string' && matchesText === searchMatch) {
return true;
}
if (typeof searchMatch !== 'string' && searchMatch.test(matchesText)) {
return true;
}
}
await devToolsPage.click('[aria-label="Show next result"]');
return;
});
const match = await devToolsPage.waitFor('#profile-views');
await devToolsPage.waitForElementWithTextContent(searchResult, match);
}
const normalizRetainerName = (retainerName: string) => {
// Retainers starting with `Window /` might have host information in their
// name, including the port, so we need to strip that. We can't distinguish
// Window from Window / either, because on Mac it is often just Window.
if (retainerName.startsWith('Window /')) {
return 'Window';
}
// Retainers including double-colons :: are names from the C++ implementation
// exposed through V8's gn arg `cppgc_enable_object_names`; these should be
// considered implementation details, so we normalize them.
if (retainerName.includes('::')) {
if (retainerName.startsWith('Detached')) {
return 'Detached InternalNode';
}
return 'InternalNode';
}
return retainerName;
};
interface RetainerChainEntry {
propertyName: string;
retainerClassName: string;
}
export async function checkRetainerChainSatisfies(
p: (retainerChain: RetainerChainEntry[]) => boolean, devToolsPage: DevToolsPage) {
// Give some time for the expansion to finish.
const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
const retainerChain = [];
for (let i = 0; i < retainerGridElements.length; ++i) {
const retainer = retainerGridElements[i];
const propertyName = await retainer.$eval('span.property-name', el => el.textContent);
const rawRetainerClassName = await retainer.$eval('span.value', el => el.textContent);
assert.isOk(propertyName, 'Could not get retainer name');
assert.isOk(rawRetainerClassName, 'Could not get retainer value');
const retainerClassName = normalizRetainerName(rawRetainerClassName);
retainerChain.push({propertyName, retainerClassName});
if (await retainer.evaluate(el => !el.classList.contains('expanded'))) {
// Only follow the shortest retainer chain to the end. This relies on
// the retainer view behavior that auto-expands the shortest retaining
// chain.
break;
}
}
return p(retainerChain);
}
export async function waitUntilRetainerChainSatisfies(
p: (retainerChain: RetainerChainEntry[]) => boolean, devToolsPage: DevToolsPage) {
await devToolsPage.waitForFunction(checkRetainerChainSatisfies.bind(null, p, devToolsPage));
}
export function appearsInOrder(targetArray: string[], inputArray: string[]) {
let i = 0;
let j = 0;
if (inputArray.length > targetArray.length) {
return false;
}
if (inputArray === targetArray) {
return true;
}
while (i < targetArray.length && j < inputArray.length) {
if (inputArray[j] === targetArray[i]) {
j++;
}
i++;
}
if (j === inputArray.length) {
return true;
}
return false;
}
export async function waitForRetainerChain(expectedRetainers: string[], devToolsPage: DevToolsPage) {
await devToolsPage.waitForFunction(checkRetainerChainSatisfies.bind(null, retainerChain => {
const actual = retainerChain.map(e => e.retainerClassName);
return appearsInOrder(actual, expectedRetainers);
}, devToolsPage));
}
export async function changeViewViaDropdown(newPerspective: string, devToolsPage: DevToolsPage) {
const perspectiveDropdownSelector = 'select[aria-label="Perspective"]';
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}`);
}
await dropdown.select(optionValue);
}
export async function changeAllocationSampleViewViaDropdown(newPerspective: string, devToolsPage: DevToolsPage) {
const perspectiveDropdownSelector = 'select[aria-label="Profile view mode"]';
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}`);
}
await dropdown.select(optionValue);
}
export async function focusTableRowWithName(text: string, devToolsPage: 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>, devToolsPage: DevToolsPage) {
// Click in a numeric cell, to avoid accidentally clicking a link.
await devToolsPage.click('.numeric-column', {
root: row,
});
}
export async function expandFocusedRow(devToolsPage: DevToolsPage) {
await devToolsPage.pressKey('ArrowRight');
await devToolsPage.waitFor('.selected.data-grid-data-grid-node.expanded');
}
function parseByteString(str: string): number {
const number = parseFloat(str);
if (str.endsWith('kB')) {
return number * 1000;
}
if (str.endsWith('MB')) {
return number * 1000 * 1000;
}
if (str.endsWith('GB')) {
return number * 1000 * 1000 * 1000;
}
return number;
}
async function getSizesFromRow(row: puppeteer.ElementHandle<Element>, devToolsPage: DevToolsPage) {
const numericData = await devToolsPage.$$('.numeric-column>.profile-multiple-values>span', row);
assert.lengthOf(numericData, 4);
function readNumber(e: Element): string {
return e.textContent;
}
const shallowSize = parseByteString(await numericData[0].evaluate(readNumber));
const retainedSize = parseByteString(await numericData[2].evaluate(readNumber));
assert.isTrue(retainedSize >= shallowSize);
return {shallowSize, retainedSize};
}
export async function getSizesFromSelectedRow(devToolsPage: 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|undefined, devToolsPage: DevToolsPage): ReturnType<DevToolsPage['waitFor']>;
export async function getCategoryRow(
text: string, wait: false, devToolsPage: DevToolsPage): ReturnType<DevToolsPage['$']>;
export async function getCategoryRow(text: string, wait = true, devToolsPage: DevToolsPage) {
const selector = `//td[text()="${text}"]/ancestor::tr`;
const row = await (wait ? devToolsPage.waitFor(selector, undefined, undefined, 'xpath') :
devToolsPage.$(selector, undefined, 'xpath'));
return row;
}
export async function getSizesFromCategoryRow(text: string, devToolsPage: DevToolsPage) {
const row = await getCategoryRow(text, undefined, devToolsPage);
return await getSizesFromRow(row, devToolsPage);
}
export async function getDistanceFromCategoryRow(text: string, devToolsPage: DevToolsPage) {
const row = await getCategoryRow(text, undefined, devToolsPage);
const numericColumns = await devToolsPage.$$('.numeric-column', row);
return await numericColumns[0].evaluate(e => parseInt(e.textContent, 10));
}
export async function getCountFromCategoryRowWithName(text: string, devToolsPage: DevToolsPage) {
const row = await getCategoryRow(text, undefined, devToolsPage);
return await getCountFromCategoryRow(row, devToolsPage);
}
export async function getCountFromCategoryRow(row: puppeteer.ElementHandle<Element>, devToolsPage: 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, devToolsPage: DevToolsPage) {
const row = await getCategoryRow(text, undefined, devToolsPage);
return await getAddedCountFromComparisonRow(row, devToolsPage);
}
export async function getAddedCountFromComparisonRow(
row: puppeteer.ElementHandle<Element>, devToolsPage: 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>, devToolsPage: 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, devToolsPage: DevToolsPage) {
const retainersPane = await devToolsPage.waitFor('.retaining-paths-view');
await devToolsPage.click(`xpath///span[text()="${retainerName}"]`, {
root: retainersPane,
clickOptions: {
button: 'right',
// Push the click right a bit further to avoid the disclosure triangle.
offset: {x: 35, y: 0},
},
});
await devToolsPage.click(`aria/${menuItem}`);
}
export async function restoreIgnoredRetainers(devToolsPage: DevToolsPage) {
await devToolsPage.click('devtools-button[aria-label="Restore ignored retainers"]');
}
export async function setFilterDropdown(filter: string, devToolsPage: DevToolsPage) {
const select = await devToolsPage.waitFor('devtools-toolbar select[aria-label="Filter"]');
await select.select(filter);
}
export async function checkExposeInternals(devToolsPage: DevToolsPage) {
const element = await devToolsPage.waitForElementWithTextContent('Internals with implementation details');
await devToolsPage.clickElement(element);
}