fix: better formatting for sparse arrays (#20379)

Right now arrays preview yields all array elements. In case
of a sparse array with a single element on index 10000000,
this results in a large string that OOM Node.js.

This patch changes pretty-printing. For example:

```ts
// Given this array
const a = [];
a[10] = 1;
// Before this patch, pretty printing will yield:
"[,,,,,,,,1]"
// With this patch, pretty printing yields:
"[empty x 9, 1]"
```

The new array pretty-printing is equal to what Chrome DevTools
do to render sparse arrays.

Fixes #20347
This commit is contained in:
Andrey Lushnikov 2023-01-27 05:07:55 -08:00 committed by GitHub
parent 5b93c1d2f9
commit 9ca9b08d90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 46 additions and 13 deletions

View File

@ -137,11 +137,7 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
tokens.push(`${name}: ${value}`);
return `{${tokens.join(', ')}}`;
}
if (object.subtype === 'array' && object.preview) {
const result = [];
for (const { name, value } of object.preview.properties)
result[+name] = value;
return '[' + String(result) + ']';
}
if (object.subtype === 'array' && object.preview)
return js.sparseArrayToString(object.preview.properties);
return object.description;
}

View File

@ -328,3 +328,27 @@ export class JavaScriptErrorInEvaluate extends Error {
export function isJavaScriptErrorInEvaluate(error: Error) {
return error instanceof JavaScriptErrorInEvaluate;
}
export function sparseArrayToString(entries: { name: string, value?: any }[]): string {
const arrayEntries = [];
for (const { name, value } of entries) {
const index = +name;
if (isNaN(index) || index < 0)
continue;
arrayEntries.push({ index, value });
}
arrayEntries.sort((a, b) => a.index - b.index);
let lastIndex = -1;
const tokens = [];
for (const { index, value } of arrayEntries) {
const emptyItems = index - lastIndex - 1;
if (emptyItems === 1)
tokens.push(`empty`);
else if (emptyItems > 1)
tokens.push(`empty x ${emptyItems}`);
tokens.push(String(value));
lastIndex = index;
}
return '[' + tokens.join(', ') + ']';
}

View File

@ -142,11 +142,7 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
tokens.push(`${name}: ${value}`);
return `{${tokens.join(', ')}}`;
}
if (object.subtype === 'array' && object.preview) {
const result = [];
for (const { name, value } of object.preview.properties!)
result[+name] = value;
return '[' + String(result) + ']';
}
if (object.subtype === 'array' && object.preview)
return js.sparseArrayToString(object.preview.properties!);
return object.description;
}

View File

@ -32,6 +32,23 @@ it('should work for complicated objects', async ({ page, browserName }) => {
expect(aHandle.toString()).toBe('JSHandle@object');
});
it('should beautifully render sparse arrays', async ({ page, browserName }) => {
const [msg] = await Promise.all([
page.waitForEvent('console'),
page.evaluateHandle(() => {
const a = [];
a[1] = 1;
a[10] = 2;
a[100] = 3;
console.log(a);
}),
]);
if (browserName === 'firefox')
expect(msg.text()).toBe('Array');
else
expect(msg.text()).toBe('[empty, 1, empty x 8, 2, empty x 89, 3]');
});
it('should work for promises', async ({ page }) => {
// wrap the promise in an object, otherwise we will await.
const wrapperHandle = await page.evaluateHandle(() => ({ b: Promise.resolve(123) }));

View File

@ -183,7 +183,7 @@ it('should use object previews for arrays and objects', async ({ page, browserNa
await page.evaluate(() => console.log([1, 2, 3], { a: 1 }, window));
if (browserName !== 'firefox')
expect(text).toEqual('[1,2,3] {a: 1} Window');
expect(text).toEqual('[1, 2, 3] {a: 1} Window');
else
expect(text).toEqual('Array JSHandle@object JSHandle@object');
});