Skip to content

Commit 32ed78b

Browse files
AleksanderBodurrimmalerba
authored andcommitted
fix(devtools): optimize object sanitization logic (angular#64234)
Previously this would take ~3500ms adev. This updated logic avoids the constant JSON.stringify implementation and instead checks for serializable values directly. After this change this code path for adev takes less than 20ms. (Benchmarks taken on an M1 Macbook Pro) PR Close angular#64234
1 parent 1828230 commit 32ed78b

File tree

2 files changed

+56
-24
lines changed

2 files changed

+56
-24
lines changed

devtools/projects/ng-devtools-backend/src/lib/serialization-utils.spec.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('sanitizeObject', () => {
2525
baz: 42,
2626
qux: true,
2727
quux: null,
28-
corge: undefined,
28+
corge: '[Non-serializable data]',
2929
grault: [1, 2, 3],
3030
garply: {a: 'a', b: 'b'},
3131
});
@@ -43,6 +43,22 @@ describe('sanitizeObject', () => {
4343
});
4444
});
4545

46+
it('should remove nested function', () => {
47+
const foo = {
48+
bar: 'bar',
49+
baz: {
50+
qux: () => 'qux',
51+
},
52+
};
53+
54+
expect(sanitizeObject(foo)).toEqual({
55+
bar: 'bar',
56+
baz: {
57+
qux: '[Non-serializable data]',
58+
},
59+
});
60+
});
61+
4662
it('should strip cyclic references', () => {
4763
const bar: any = {foo: null};
4864
const foo = {
@@ -51,7 +67,9 @@ describe('sanitizeObject', () => {
5167
bar.foo = foo;
5268

5369
expect(sanitizeObject(foo)).toEqual({
54-
bar: '[Non-serializable data]',
70+
bar: {
71+
foo: '[Circular]',
72+
},
5573
});
5674
});
5775
});

devtools/projects/ng-devtools-backend/src/lib/serialization-utils.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,45 @@
77
*/
88

99
export function sanitizeObject(obj: any): any {
10-
if (obj === null || typeof obj !== 'object') {
11-
return isPortSerializable(obj) ? obj : '[Non-serializable data]';
12-
}
10+
// Keep track of visited objects to detect circular references.
11+
const seen = new WeakSet();
1312

14-
if (Array.isArray(obj)) {
15-
return obj.map(sanitizeObject);
16-
}
13+
function recurse(value: any): any {
14+
// Primitives and null
15+
if (value === null || typeof value !== 'object') {
16+
// Some primitives are not serializable
17+
if (
18+
typeof value === 'function' ||
19+
typeof value === 'symbol' ||
20+
typeof value === 'bigint' ||
21+
value === undefined
22+
) {
23+
return '[Non-serializable data]';
24+
}
1725

18-
const result: Record<string, any> = {};
19-
for (const [key, value] of Object.entries(obj)) {
20-
result[key] = isPortSerializable(value) ? value : '[Non-serializable data]';
21-
}
22-
return result;
23-
}
26+
// Most other primitives are serializable
27+
return value;
28+
}
2429

25-
// This is specific to chrome.runtime.Port which like JSON.stringify, cannot serialize cyclic objects
26-
function isPortSerializable(value: any): boolean {
27-
if (typeof value === 'function') {
28-
return false; // Functions are not serializable but JSON.stringify doesn't throw, it strips them out
29-
}
30+
// Down here we only have objects
31+
// Check for circular references
32+
if (seen.has(value)) {
33+
return '[Circular]';
34+
}
35+
seen.add(value);
36+
37+
// Recursively serialize arrays
38+
if (Array.isArray(value)) {
39+
return value.map(recurse);
40+
}
3041

31-
try {
32-
JSON.stringify(value); // This mimics the runtime's limitations closely
33-
return true;
34-
} catch {
35-
return false;
42+
// Recursively serialize objects
43+
const result: Record<string, any> = {};
44+
for (const [key, propValue] of Object.entries(value)) {
45+
result[key] = recurse(propValue);
46+
}
47+
return result;
3648
}
49+
50+
return recurse(obj);
3751
}

0 commit comments

Comments
 (0)