From 3a864279b0142e6ebc1c5248b51aefb932e98edd Mon Sep 17 00:00:00 2001 From: overlookmotel <557937+overlookmotel@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:29:45 +0000 Subject: [PATCH] perf(linter/plugins): pre-populate cache of `EnterExit` objects at startup (#20194) Perf optimization to visitor compilation. Continuation of #20187. Instead of populating the cache of `EnterExit` objects in `finalizeCompiledVisitor`, populate it in full at process startup. The main gain is that it makes these objects, which are heavily accessed during AST walk, more likely to be grouped together in memory, reducing L1 cache misses, and consuming less L1 cache. --- apps/oxlint/src-js/plugins/visitor.ts | 34 +++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/oxlint/src-js/plugins/visitor.ts b/apps/oxlint/src-js/plugins/visitor.ts index ab41eafbdfcfa..cd27605e5eba7 100644 --- a/apps/oxlint/src-js/plugins/visitor.ts +++ b/apps/oxlint/src-js/plugins/visitor.ts @@ -216,10 +216,23 @@ let hasActiveVisitors = false; // `compiledVisitor` may contain many `{ enter, exit }` objects. // Use this cache to reuse those objects across all visitor compilations. // -// `activeNonLeafVisitorsCount` is the number of populated non-leaf visitors in `compiledVisitor`, +// Pre-populate the cache with enough `EnterExit` objects for all non-leaf AST node types. +// This removes the need to check if cache contains enough objects before using it in `finalizeCompiledVisitor`. +// +// More importantly, allocating all these objects in one go will put them together in memory in "new space". +// When these objects graduate to "old space", V8 does not necessarily keep them together in memory, +// but it will tend to - it makes it more likely they'll be grouped together. +// These objects are accessed over and over during AST walk, so having them all close together in memory +// makes L1 cache misses less likely. +// +// `activeNonLeafVisitorsCount` is the number of active non-leaf visitors in `compiledVisitor`, // and therefore the number of `EnterExit` objects currently in use. const enterExitObjectCache: EnterExit[] = []; +for (let i = NON_LEAF_NODE_TYPES_COUNT; i !== 0; i--) { + enterExitObjectCache.push({ enter: null, exit: null }); +} + // `VisitProp` object cache. // // During compilation, many such objects may be required, and then they're discarded in `finalizeCompiledVisitor`. @@ -419,20 +432,13 @@ export function finalizeCompiledVisitor(): VisitorState { compiledVisitor[typeId] = mergeVisitFns(compilingLeafVisitor[typeId]!); } - // Populate `enterExitObjectCache` with enough entries for all non-leaf visitors. - // After warming up over first few files, the cache will be large enough to service all files, - // and this loop will be skipped. This avoids the main loop below from having to branch repeatedly - // on whether there are enough `EnterExit` objects in cache, and to create one if not. - while (enterExitObjectCache.length < activeNonLeafVisitorsCount) { - enterExitObjectCache.push({ enter: null, exit: null }); - } - // Merge visitors for non-leaf nodes for (let i = 0; i < activeNonLeafVisitorsCount; i++) { const typeId = activeNonLeafVisitorTypeIds[i]!; const entry = compilingNonLeafVisitor[typeId - LEAF_NODE_TYPES_COUNT]!; - // Use enter-exit object from cache. Loop above ensures cache is filled with enough objects. + // Reuse `EnterExit` object from cache. + // Cache is pre-populated with enough objects that `i` cannot be out of bounds. const enterExit = enterExitObjectCache[i]; debugAssertIsNonNull(enterExit, "`enterExit` should not be null"); @@ -485,20 +491,12 @@ export function finalizeCompiledVisitor(): VisitorState { * This frees visit functions stored in `compiledVisitor`, and makes them eligible for garbage collection. * * After calling this function, `compiledVisitor` is in a clean state, ready for next file's visitor to be compiled. - * - * `finalizeCompiledVisitor` must have been called before calling this function, to ensure that `enterExitObjectCache` - * contains at least `activeNonLeafVisitorsCount` objects. */ export function resetCompiledVisitor(): void { // Reset `compiledVisitor` array compiledVisitor.fill(null); // Reset `EnterExit` objects - debugAssert( - enterExitObjectCache.length >= activeNonLeafVisitorsCount, - "`enterExitObjectCache` should contain at least `activeNonLeafVisitorsCount` entries", - ); - for (let i = 0; i < activeNonLeafVisitorsCount; i++) { const enterExit = enterExitObjectCache[i]; enterExit.enter = null;