Skip to content

perf(js-plugins): optimize CFG walker with SoA pattern and custom traverser#17423

Closed
re-taro wants to merge 2 commits intooxc-project:mainfrom
re-taro:refactor/linter-cfg
Closed

perf(js-plugins): optimize CFG walker with SoA pattern and custom traverser#17423
re-taro wants to merge 2 commits intooxc-project:mainfrom
re-taro:refactor/linter-cfg

Conversation

@re-taro
Copy link
Contributor

@re-taro re-taro commented Dec 28, 2025

Summary

Optimize walkProgramWithCfg in apps/oxlint/src-js/plugins/cfg.ts by applying multiple performance improvements based on TODO comments in the original code

Close #17232

  • Replace VisitNodeStep/CallMethodStep classes from @eslint/plugin-kit with plain number constants
  • Pre-compute type IDs in the first pass to eliminate Map lookups in the second pass
  • Use Struct of Arrays (SoA) pattern for step storage to improve memory locality and V8 optimization
  • Replace ESLint's Traverser with a lightweight custom traverseNode function that avoids unnecessary overhead

Changes

Optimization Before After
Step encoding VisitNodeStep/CallMethodStep classes Plain constants STEP_TYPE_*
Type ID lookup Map lookup on every step in 2nd pass Pre-computed in 1st pass
Data structure Array of Structs (steps[]) Struct of Arrays (stepTypes[], stepTargets[], stepTypeIds[], stepArgs[])
AST traverser ESLint's Traverser.traverse() Custom traverseNode() using visitorKeys

Removed dependencies

  • @eslint/plugin-kit (no longer needed)
  • ESLint's internal Traverser module

@github-actions github-actions bot added A-linter Area - Linter A-cli Area - CLI A-linter-plugins Area - Linter JS plugins C-performance Category - Solution not expected to change functional behavior, only performance labels Dec 28, 2025
@re-taro re-taro force-pushed the refactor/linter-cfg branch from 19b0bc3 to 71a255d Compare December 28, 2025 14:48
@re-taro re-taro marked this pull request as ready for review December 28, 2025 14:49
Copilot AI review requested due to automatic review settings December 28, 2025 14:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes the walkProgramWithCfg function by replacing ESLint's data structures and traverser with more efficient custom implementations. The optimization focuses on reducing object allocations, improving memory locality, and eliminating redundant lookups.

Key changes:

  • Replaced class-based step encoding with plain numeric constants and Struct of Arrays (SoA) pattern
  • Pre-computed type IDs during step building to eliminate Map lookups during AST walking
  • Implemented a lightweight custom traverseNode function to replace ESLint's Traverser

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.

File Description
apps/oxlint/src-js/plugins/cfg.ts Core optimization implementation: replaced step classes with SoA pattern, added custom traverser, pre-computed type IDs
apps/oxlint/package.json Removed unused @eslint/plugin-kit dependency
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +155 to +161
if (isArray(node)) {
const len = node.length;
for (let i = 0; i < len; i++) {
traverseNode(node[i], enter, leave);
}
return;
}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isArray check treats arrays as nodes to traverse recursively, but this will not call enter/leave callbacks on array elements that are actual AST nodes. The function signature accepts Node | null | undefined, not arrays, so this check appears to handle child node arrays incorrectly. Arrays should be iterated and their elements passed to traverseNode, but the type signature should clarify this or the logic should ensure enter/leave are called appropriately for node elements.

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +242
for (const [name, id] of NODE_TYPE_IDS_MAP) {
if (id === eventTypeId) {
eventNames.push(name);
break;
}
}
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverse lookup iterating through the entire NODE_TYPE_IDS_MAP is inefficient in debug builds. Consider creating a reverse map (ID to name) at module initialization for O(1) lookups instead of O(n) iteration for each event.

Copilot uses AI. Check for mistakes.
@re-taro re-taro force-pushed the refactor/linter-cfg branch 2 times, most recently from c8a6284 to 31dc842 Compare December 28, 2025 15:06
…verser

Apply multiple performance optimizations to `walkProgramWithCfg`:

- Replace `VisitNodeStep`/`CallMethodStep` classes with plain number constants
- Pre-compute type IDs in first pass to eliminate Map lookups in second pass
- Use Struct of Arrays (SoA) pattern for step storage to improve memory locality
- Replace ESLint's Traverser with lightweight custom `traverseNode` function
- Remove unused `@eslint/plugin-kit` and ESLint Traverser imports
@re-taro re-taro force-pushed the refactor/linter-cfg branch from 31dc842 to cf1fc5f Compare December 28, 2025 15:17
Copy link
Member

@overlookmotel overlookmotel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this.

I'm on holiday for 10 days, so I'm afraid I won't be able to review properly for some time. Sorry for delay.

But a couple of notes in meantime:

  1. Would it be possible to break this up into multiple PRs, split (roughly) along the lines of the points in the original TODO comment? That'd make this easier to review and get parts of it merged while we discuss other parts. I'm not sure from first glance if AI review suggesting flaws in new traversal impl are valid or not.

You can make a "stack" by basing each PR on the previous PR. I can handle rebasing each onto main before merging.

  1. I think the SOA arrays can probably be cut down from 4 to 2:
  • stepTargets and stepArgs could be combined. When step type is "enter node" or "exit node", the array element would be the node object. When it's a CFG event, the array element would be an array of args to call the visitor function with.
  • typeId and step type could be combined by using typeId unchanged for "enter node" and typeId + N for "exit node" (where N is some number greater than maximum type ID e.g. 256).

@overlookmotel
Copy link
Member

@re-taro I'm back from being away. Wondered if you have an interest in continuing with this?

@overlookmotel overlookmotel self-assigned this Jan 20, 2026
@re-taro
Copy link
Contributor Author

re-taro commented Jan 20, 2026

@overlookmotel

Yes! I am interested!! However, I am quite busy myself and it seems I can respond starting this weekend.

@overlookmotel
Copy link
Member

That's great to hear! Please ping me when you've had time to make some progress.

@re-taro
Copy link
Contributor Author

re-taro commented Jan 25, 2026

Hi @overlookmotel, thanks for your feedback!

I've split the original PR into 3 separate PRs as you suggested, following the TODO comments:

  1. perf(linter/plugins): replace ESLint step classes with plain objects #18527 - Replace ESLint step classes with plain objects + merge kind/phase into single type
  2. perf(linter/plugins): use SoA pattern with 2 arrays for CFG steps #18528 - Use SoA pattern with 2 arrays (as you suggested: stepTypeIds + stepData)
  3. perf(linter/plugins): replace ESLint Traverser with lightweight traverseNode #18529 - Replace ESLint Traverser with lightweight traverseNode

Since GitHub doesn't support PR stacks across forks, I created them as independent PRs targeting main. They should be merged in order (1 → 2 → 3). I'll rebase each one after the previous is merged.

I also incorporated your suggestion to reduce the SoA arrays from 4 to 2:

  • Combined stepTargets and stepArgs into stepData
  • Combined typeId and step type using offset encoding (enter = typeId, exit = typeId + 256)

Let me know if you'd like any changes!

@overlookmotel
Copy link
Member

Superceded by the 3 PRs listed above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter A-linter-plugins Area - Linter JS plugins C-performance Category - Solution not expected to change functional behavior, only performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linter plugins: Improve performance of CFG (code path analysis)

2 participants