Skip to content

Reduce per-node dispatch overhead by inlining visitor function calls for single-listener selectors #125

Description

@alanzabihi

Hypothesis

In callSyncSingle (source-code-visitor.js:87-93), for each matched selector, the code does: `Map.get(name)` → check if array exists → iterate array → call each function. The majority of selectors have exactly 1 registered listener (one rule per selector name). For these single-listener selectors, the array iteration (for loop setup, length check, index increment) is unnecessary overhead.

If we store single-listener selectors differently (as a direct function reference instead of a 1-element array), we can call the function directly without array overhead. This is analogous to how V8 optimizes monomorphic call sites vs megamorphic.

Proposed Change

Modify SourceCodeVisitor to distinguish single-listener entries:

```js
add(name, func) {
const existing = this.#functions.get(name);
if (existing === undefined) {
// First listener: store function directly
this.#functions.set(name, func);
} else if (typeof existing === 'function') {
// Second listener: convert to array
this.#functions.set(name, [existing, func]);
} else {
// Third+: push to existing array
existing.push(func);
}
}

callSyncSingle(name, arg) {
const entry = this.#functions.get(name);
if (entry !== undefined) {
if (typeof entry === 'function') {
entry(arg); // Fast path: single listener
} else {
for (let i = 0; i < entry.length; i++) {
entryi;
}
}
}
}
```

Expected Impact

  • Most selectors (e.g., "Identifier", "BlockStatement") have 1-2 listeners from different rules
  • For single-listener selectors: eliminates array allocation, length check, index variable
  • ~90K nodes × ~3-5 matched selectors per node = ~270K-450K dispatch calls per file
  • If 60-70% are single-listener: ~162K-315K calls take the fast path
  • Per-call savings: ~5-15ns (branch prediction + no array overhead)
  • Conservative estimate: 3-7ms composite improvement

Key Files

  • `lib/linter/source-code-visitor.js` — add(), callSyncSingle(), callSync2(), callSync3()

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions