Skip to content

Commit fb02e6c

Browse files
committed
perf(napi/parser): lazy deser: speed up creating NodeArrays (#11869)
`NodeArray` store length as an internal property, rather then creating an underlying `Array` with that length. This is much faster when the array is large. Produces an 18x speed-up in getting `program.body` for `cal.com.tsx` benchmark fixture, because it has 3942 statements.
1 parent 58dfff8 commit fb02e6c

File tree

2 files changed

+58
-46
lines changed

2 files changed

+58
-46
lines changed

napi/parser/raw-transfer/node-array.js

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const { TOKEN, constructorError } = require('./lazy-common.js');
1212
// via `Object.getOwnPropertySymbols` or `Reflect.ownKeys`. Therefore user code cannot unwrap the proxy.
1313
const ARRAY = Symbol();
1414

15-
// Function to get element from an array. Initialized in class static block below.
16-
let getElement;
15+
// Functions to get length of and get element from an array. Initialized in class static block below.
16+
let getLength, getElement;
1717

1818
/**
1919
* An array of AST nodes where elements are deserialized lazily upon access.
@@ -44,8 +44,8 @@ class NodeArray extends Array {
4444
constructor(pos, length, stride, construct, ast) {
4545
if (ast?.token !== TOKEN) constructorError();
4646

47-
super(length);
48-
this.#internal = { pos, ast, stride, construct };
47+
super();
48+
this.#internal = { pos, length, ast, stride, construct };
4949
return new Proxy(this, PROXY_HANDLERS);
5050
}
5151

@@ -55,24 +55,19 @@ class NodeArray extends Array {
5555
// Override `values` method with a more efficient one that avoids going via proxy for every iteration.
5656
// TODO: Benchmark to check that this is actually faster.
5757
values() {
58-
// Get actual `NodeArray`. `this` is a proxy.
59-
const arr = this[ARRAY];
60-
return new NodeArrayValuesIterator(arr.#internal, arr.length);
58+
return new NodeArrayValuesIterator(this[ARRAY].#internal);
6159
}
6260

6361
// Override `keys` method with a more efficient one that avoids going via proxy for every iteration.
6462
// TODO: Benchmark to check that this is actually faster.
6563
keys() {
66-
// `this` is a proxy, but proxy will pass through getting `this.length` to the underlying `NodeArray`
67-
return new NodeArrayKeysIterator(this.length);
64+
return new NodeArrayKeysIterator(this[ARRAY].#internal.length);
6865
}
6966

7067
// Override `entries` method with a more efficient one that avoids going via proxy for every iteration.
7168
// TODO: Benchmark to check that this is actually faster.
7269
entries() {
73-
// Get actual `NodeArray`. `this` is a proxy.
74-
const arr = this[ARRAY];
75-
return new NodeArrayEntriesIterator(arr.#internal, arr.length);
70+
return new NodeArrayEntriesIterator(this[ARRAY].#internal);
7671
}
7772

7873
// This method is overwritten with reference to `values` method below.
@@ -88,36 +83,35 @@ class NodeArray extends Array {
8883
* @returns {NodeArray} - `NodeArray` containing slice of this one
8984
*/
9085
slice(start, end) {
91-
// Get actual `NodeArray`. `this` is a proxy.
92-
const arr = this[ARRAY];
86+
const internal = this[ARRAY].#internal,
87+
{ length } = internal;
9388

9489
start = toInt(start);
9590
if (start < 0) {
96-
start = arr.length + start;
91+
start = length + start;
9792
if (start < 0) start = 0;
9893
}
9994

10095
if (end === void 0) {
101-
end = arr.length;
96+
end = length;
10297
} else {
10398
end = toInt(end);
10499
if (end < 0) {
105-
end = arr.length + end;
100+
end += length;
106101
if (end < 0) end = 0;
107-
} else if (end > arr.length) {
108-
end = arr.length;
102+
} else if (end > length) {
103+
end = length;
109104
}
110105
}
111106

112-
let length = end - start;
113-
if (length <= 0 || start >= arr.length) {
107+
let sliceLength = end - start;
108+
if (sliceLength <= 0 || start >= length) {
114109
start = 0;
115-
length = 0;
110+
sliceLength = 0;
116111
}
117112

118-
const internal = arr.#internal,
119-
{ stride } = internal;
120-
return new NodeArray(internal.pos + start * stride, length, stride, internal.construct, internal.ast);
113+
const { stride } = internal;
114+
return new NodeArray(internal.pos + start * stride, sliceLength, stride, internal.construct, internal.ast);
121115
}
122116

123117
// Make `console.log` deserialize all elements.
@@ -128,16 +122,23 @@ class NodeArray extends Array {
128122
}
129123

130124
static {
125+
/**
126+
* Get length of `NodeArray`.
127+
* @param {NodeArray} arr - `NodeArray` object
128+
* @returns {number} - Array length
129+
*/
130+
getLength = arr => arr.#internal.length;
131+
131132
/**
132133
* Get element of `NodeArray` at index `index`.
133-
* `index` must be in bounds (i.e. `< arr.length`).
134134
*
135135
* @param {NodeArray} arr - `NodeArray` object
136136
* @param {number} index - Index of element to get
137-
* @returns {*} - Element at index `index`
137+
* @returns {*|undefined} - Element at index `index`, or `undefined` if out of bounds
138138
*/
139139
getElement = (arr, index) => {
140140
const internal = arr.#internal;
141+
if (index >= internal.length) return void 0;
141142
return (0, internal.construct)(internal.pos + index * internal.stride, internal.ast);
142143
};
143144
}
@@ -154,13 +155,13 @@ module.exports = NodeArray;
154155
class NodeArrayValuesIterator {
155156
#internal;
156157

157-
constructor(arrInternal, length) {
158+
constructor(arrInternal) {
158159
const { ast, pos, stride } = arrInternal || {};
159160
if (ast?.token !== TOKEN) constructorError();
160161

161162
this.#internal = {
162163
pos,
163-
endPos: pos + length * stride,
164+
endPos: pos + arrInternal.length * stride,
164165
ast,
165166
construct: arrInternal.construct,
166167
stride,
@@ -211,13 +212,13 @@ class NodeArrayKeysIterator {
211212
class NodeArrayEntriesIterator {
212213
#internal;
213214

214-
constructor(arrInternal, length) {
215+
constructor(arrInternal) {
215216
const { ast } = arrInternal || {};
216217
if (ast?.token !== TOKEN) constructorError();
217218

218219
this.#internal = {
219220
index: 0,
220-
length,
221+
length: arrInternal.length,
221222
pos: arrInternal.pos,
222223
ast,
223224
construct: arrInternal.construct,
@@ -252,34 +253,35 @@ const PROXY_HANDLERS = {
252253
// Return `true` for indexes which are in bounds.
253254
// e.g. `'0' in arr`.
254255
has(arr, key) {
255-
if (isIndex(key)) return key * 1 < arr.length;
256+
if (isIndex(key)) return key * 1 < getLength(arr);
256257
return Reflect.has(arr, key);
257258
},
258259

259-
// Get entries which are in bounds.
260+
// Get elements and length.
260261
get(arr, key) {
261262
// Methods of `NodeArray` are called with `this` being the proxy, rather than the `NodeArray` itself.
262263
// They can "unwrap" the proxy by getting `this[ARRAY]`.
263264
if (key === ARRAY) return arr;
264-
265-
if (isIndex(key)) {
266-
key *= 1;
267-
if (key >= arr.length) return void 0;
268-
return getElement(arr, key);
269-
}
265+
if (key === 'length') return getLength(arr);
266+
if (isIndex(key)) return getElement(arr, key * 1);
270267

271268
return Reflect.get(arr, key);
272269
},
273270

274-
// Get descriptors which are in bounds.
271+
// Get descriptors for elements and length.
275272
getOwnPropertyDescriptor(arr, key) {
273+
if (key === 'length') {
274+
// Cannot return `writable: false` unfortunately
275+
return { value: getLength(arr), writable: true, enumerable: false, configurable: false };
276+
}
277+
276278
if (isIndex(key)) {
277-
key *= 1;
278-
if (key >= arr.length) return void 0;
279+
const value = getElement(arr, key * 1);
280+
if (value === void 0) return void 0;
279281
// Cannot return `configurable: false` unfortunately
280-
return { value: getElement(arr, key), writable: false, enumerable: true, configurable: true };
282+
return { value, writable: false, enumerable: true, configurable: true };
281283
}
282-
// Cannot return `writable: false` for `length` property unfortunately
284+
283285
return Reflect.getOwnPropertyDescriptor(arr, key);
284286
},
285287

@@ -304,8 +306,9 @@ const PROXY_HANDLERS = {
304306

305307
// Get keys, including element indexes.
306308
ownKeys(arr) {
307-
const keys = [];
308-
for (let i = 0; i < arr.length; i++) {
309+
const keys = [],
310+
length = getLength(arr);
311+
for (let i = 0; i < length; i++) {
309312
keys.push(i + '');
310313
}
311314
keys.push(...Reflect.ownKeys(arr));

napi/parser/test/lazy-deserialization.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,15 @@ describe('NodeArray', () => {
495495
expect(stmtsTwice[3]).toBe(body[1]);
496496
});
497497

498+
it('get length', () => {
499+
const body0 = parseSyncLazy('test.js', '').program.body;
500+
expect(body0.length).toBe(0);
501+
const body2 = parseSyncLazy('test.js', 'let x = 1; x = 2;').program.body;
502+
expect(body2.length).toBe(2);
503+
const body4 = parseSyncLazy('test.js', 'x; y; z; 123;').program.body;
504+
expect(body4.length).toBe(4);
505+
});
506+
498507
it('set length (throws)', () => {
499508
const { body } = parseSyncLazy('test.js', 'let x = 1; x = 2;').program;
500509
expect(() => body.length = 0)

0 commit comments

Comments
 (0)