Skip to content

perf(linter): reduce preallocation for per-file diagnostics Vec#23705

Merged
camc314 merged 4 commits into
oxc-project:mainfrom
mds-ant:perf/diag-vec-prealloc-512-auto
Jun 25, 2026
Merged

perf(linter): reduce preallocation for per-file diagnostics Vec#23705
camc314 merged 4 commits into
oxc-project:mainfrom
mds-ant:perf/diag-vec-prealloc-512-auto

Conversation

@mds-ant

@mds-ant mds-ant commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What

ContextHost::new unconditionally preallocates a Vec<Message> with capacity 512 for every file:

const DIAGNOSTICS_INITIAL_CAPACITY: usize = 512;
// ...
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),

size_of::<Message>() == 360, so this is ~180 KB of heap per file before a single rule runs. Clean files (the overwhelming majority in real projects) produce zero diagnostics, so the allocation is almost entirely wasted; files that produce >512 diagnostics realloc past the initial capacity anyway.

Change

Start with Vec::new() (zero allocation for clean files). On the first push, reserve(32) so files that do report skip the 4→8→16→32 growth steps and start at ~11 KB instead of ~180 KB.

Memory impact

oxlint over the vscode repo (10,572 files, default 95-rule correctness set), getrusage(RUSAGE_CHILDREN).ru_maxrss, median of 3 interleaved runs after warmup:

base after Δ
peak RSS, 64 threads 343.7 MB 275.3 MB −68.4 MB (−19.9%)
peak RSS, --threads=1 214.5 MB 142.7 MB −71.8 MB (−33.5%)
minor page faults, 64 threads 99,367 79,974 −19.5%
minor page faults, --threads=1 60,228 41,931 −30.4%

No wall-clock regression (criterion linter bench is flat on all five fixtures, within noise).

Verification

  • cargo test -p oxc_linter --lib — 1162 tests pass.
  • Behavior unchanged: Vec::new() + reserve only affects allocation strategy.

@mds-ant mds-ant requested a review from camc314 as a code owner June 22, 2026 08:59
@mds-ant

mds-ant commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

WDYT, @overlookmotel @DonIsaac? I see you previously discussed this in #5806.

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi, thanks for putting up this PR.

What does the performance difference look like in some different repos?

I think that it's worth having some initial capacity (64 maybe) as it is incredibly commen that some diagnostics may be emitted, there's also bulk supressions/disable directives to be considered.

Also, how did you identify this change, and what drove you to make it?

Thanks!

@camc314 camc314 added the A-linter Area - Linter label Jun 22, 2026
@camc314 camc314 self-assigned this Jun 22, 2026
@codspeed-hq

codspeed-hq Bot commented Jun 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 66 skipped benchmarks1


Comparing mds-ant:perf/diag-vec-prealloc-512-auto (97f07f0) with main (20e8285)

Open in CodSpeed

Footnotes

  1. 66 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

…`Vec`

`ContextHost::new` previously preallocated `Vec<Message>` with capacity
512 for every file. `Message` is 360 bytes, so this was ~180 KB of heap
per file regardless of diagnostic count. Most files in real-world
projects produce zero or a handful of diagnostics, so the allocation was
almost entirely wasted. `Vec`'s amortized growth handles noisy files
fine.
@mds-ant mds-ant force-pushed the perf/diag-vec-prealloc-512-auto branch from 42b0415 to 6a836ba Compare June 23, 2026 09:46
@mds-ant

mds-ant commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@camc314 Thanks for the feedback! I now updated the implementation to allocate a Vec with a capacity of 32 once the first diagnostic is about to be reported.

Here's a hyperfine run for the VS Code repo, suggesting that this PR marginally improves performance. This sounds plausible given the reduced number of page faults:

Benchmark 1: base (cap 512)
  Time (mean ± σ):      2.459 s ±  0.034 s    [User: 2.105 s, System: 0.271 s]
  Range (min … max):    2.401 s …  2.548 s    25 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: reserve(32)
  Time (mean ± σ):      2.428 s ±  0.025 s    [User: 2.088 s, System: 0.255 s]
  Range (min … max):    2.395 s …  2.511 s    25 runs

  Warning: Ignoring non-zero exit code.

Summary
  reserve(32) ran
    1.01 ± 0.02 times faster than base (cap 512)

As for my motivation, I'm a big fan of Oxc. I wanted to see if I could contribute, so I had Claude investigate performance optimizations for Oxlint.

@mds-ant mds-ant changed the title perf(linter): remove 512-slot preallocation for per-file diagnostics Vec perf(linter): optimize 512-slot preallocation for per-file diagnostics Vec Jun 23, 2026
@mds-ant mds-ant requested a review from camc314 June 23, 2026 10:33

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for coming back to me!

I thought about this some more, and decided to keep the eager preallocation, but shrink the initial diagnostic capacity from 512 to 16.

My reasoning behind this is:

  • 512 is clearly wayyy to high and it has to alloc a lot of memory
  • Vec::new (0 cap), still doesn't feel like the best default, as for some workflows (language server/bulk supressions), it'll be common that some diagnostics are reported.
  • 16 feels like a good balance between the two cases.
    Thanks for working on this!

@camc314 camc314 changed the title perf(linter): optimize 512-slot preallocation for per-file diagnostics Vec perf(linter): reduce preallocation for per-file diagnostics Vec Jun 25, 2026
@camc314 camc314 merged commit 838fd63 into oxc-project:main Jun 25, 2026
28 checks passed
@mds-ant

mds-ant commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@camc314 Thanks for merging this! I agree that 512 was clearly too high.

Just out of curiosity, what makes you prefer the eager preallocation over the allocation (with the right capacity, say 16) when the first diagnostic is reported?

camc314 added a commit that referenced this pull request Jun 29, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 88f4455 str: [**BREAKING**] `Str` and `Ident` methods take
`&GetAllocator` (#23781) (overlookmotel)

### 🚀 Features

- f2091b3 ast: Unify old and new `AstBuilder`s (#23875) (overlookmotel)
- 1c8f50c linter: Add schema for `eslint/no-restricted-import` (#23642)
(Sysix)

### 🐛 Bug Fixes

- 7cb85c4 linter/eslint/no-negated-condition: Add autofix for negated
conditions (#23825) (Yagiz Nizipli)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d891990 linter/jsx-a11y/role-supports-aria-props: Ignore nullish prop
values (#23865) (Mikhail Baev)
- 94b6599 linter: Deduplicate missing plugin errors (#23853) (camc314)
- eff3eff linter/oxc/branches-sharing-code: Avoid else-if false
positives (#23843) (camc314)
- 2a2d3b9 linter/eslint/prefer-destructuring: Skip
`AssignmentExpression` autofixes (#23818) (camc314)
- ddc24ae linter/eslint/id-length: Respect checkGeneric for mapped type
keys (#23802) (bab)
- cd89202 linter/react/exhaustive-deps: Skip wrapper expression when
analyzing hook initializers (#23793) (camc314)
- 20e8285 linter/unicorn/prefer-native-coercion-function: Allow ts type
predicates (#23774) (camc314)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)
- 52032cf linter: Newline-terminate tsgolint errors (#23762) (Mikhail
Baev)
- 368fda7 linter/eslint/no-warning-comments: Avoid dropping generated
regex patterns (#23741) (camc314)
- ce44fbd linter/valid-title: Escape disallowed words regex (#23742)
(camc314)
- 3100d11 linter/prefer-called-exactly-once-with: Avoid out-of-bounds
slice panic at end of file (#23625) (Jerry Zhao)
- 742be36 refactor/node/handle-callback-err: Reject invalid regex config
(#23740) (camc314)
- d7be179 linter/eslint/no-restricted-globals: Handle shadowed locals
(#23736) (camc314)
- b3b1ff8 linter/vitest/expect-expect: Handle global vitest detection
correctly (#23734) (camc314)

### ⚡ Performance

- 68f9472 linter/jsx-a11y: Skip lowercasing non-aria attribute names
(#23906) (Lawrence Lin)
- b9312b4 linter/unicorn/prefer-export-from: Use keyed binding lookup
(#23893) (Marius Schulz)
- cd5204e linter/typescript/no-unsafe-declaration-merging: Use keyed
binding lookup (#23894) (Marius Schulz)
- e948498 linter/eslint/prefer-named-capture-group: Only dispatch for
relevant node types (#23868) (Connor Shea)
- 4ac7a8e linter/eslint/max-depth: Derive node types (#23896) (Connor
Shea)
- daeed09 linter/eslint/no-restricted-globals: Only scan unresolved
references (#23890) (camc314)
- e808514 linter/jest-vitest: Speed up no-standalone-expect (#23883)
(camc314)
- 8b165e5 linter/react/exhaustive-deps: Skip non-reactive calls early
(#23882) (camc314)
- 54005e7 linter/eslint/no-unused-vars: Precompute exported bindings
(#23881) (camc314)
- 9bc2f8c linter/unicorn/prefer-number-properties: Speed up global
checks (#23880) (camc314)
- 4ff104f linter: Optimize `require-hook` and `prefer-mock-*` rules to
run on specific node types (#23871) (Connor Shea)
- cc2213b linter: Run `no-underscore-dangle` only when relevant node
types are present (#23867) (Connor Shea)
- 3e55c21 linter/promise/always-return: Narrow to function node types
(#23878) (Connor Shea)
- 7136182 linter/jest-vitest: Speed up no-commented-out-tests (#23864)
(camc314)
- f138264 linter/eslint/no-script-url: Match javascript: prefix without
allocating (#23861) (Lawrence Lin)
- 7ef6895 linter/react/no-array-index-key: Delay index symbol lookup
(#23857) (camc314)
- 26bc171 linter/react/no-array-index-key: Match callback methods
directly (#23856) (camc314)
- 44fbbda linter/jsx-a11y/interactive-supports-focus: Check cheap
conditions first (#23854) (camc314)
- 84a5aa3 linter/eslint/no-extend-native: Skip lowercase references
early (#23851) (camc314)
- 88a74b2 linter/eslint/no-nonoctal-decimal-escape: Scan decimal escapes
as bytes (#23850) (camc314)
- fca69a8 linter: Skip traversal without this expressions (#23845)
(camc314)
- 838fd63 linter: Reduce preallocation for per-file diagnostics `Vec`
(#23705) (Marius Schulz)
- 417b506 linter/typescript/array-type: Remove full source text clone
(#23751) (Marius Schulz)

### 📚 Documentation

- 57e4469 linter/unicorn: Update prefer-dom-node-text-content rationale
(#23933) (Mikhail Baev)
- 3d61dea all: Correct capitalization in comments (#23887)
(overlookmotel)

### 🛡️ Security

- 3cdd18f deps: Update npm packages (#23690) (renovate[bot])
# Oxfmt
### 💥 BREAKING CHANGES

- 259e0cd oxfmt,formatter_graphql: [**BREAKING**] Support draft syntax
with removing prettier fallback (#23326) (leaysgur)
- accbc49 oxfmt: [**BREAKING**] Format `parser:css,less,scss` files +
css-in-js by `oxc_formatter_css` (#23321) (leaysgur)

### 🚀 Features

- dffa4b3 formatter_css: Implement `oxc_formatter_css` (#23320)
(leaysgur)
- 01de9ec oxfmt: Format `parser:graphql` files by
`oxc_formatter_graphql` (#23318) (leaysgur)
- 4e66212 formatter_graphql: Implement oxc_formatter_graphql (#23317)
(leaysgur)

### 🐛 Bug Fixes

- 67325ae formatter_css: Handle frontmatter language (#23819) (leaysgur)
- 3f355e5 formatter_graphql: Improve major prettier diffs (#23419)
(leaysgur)
- 48e2d78 formatter_css: Improve major prettier diffs (#23327)
(leaysgur)
- 8c07cad all: Enable `disable_old_builder` Cargo feature for `oxc_ast`
crate in tests (#23888) (overlookmotel)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)

### ⚡ Performance

- 4ddcba0 formatter_core: Add printable-ASCII fast path to TextWidth
(#23913) (Lawrence Lin)

### 📚 Documentation

- b4d0dc9 oxfmt,formatter,formatter_css,formatter_core: Update AGENTS.md
(#23814) (leaysgur)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
camc314 added a commit that referenced this pull request Jul 3, 2026
…3705)

## What

`ContextHost::new` unconditionally preallocates a `Vec<Message>` with
capacity 512 for every file:

```rust
const DIAGNOSTICS_INITIAL_CAPACITY: usize = 512;
// ...
diagnostics: RefCell::new(Vec::with_capacity(DIAGNOSTICS_INITIAL_CAPACITY)),
```

`size_of::<Message>() == 360`, so this is **~180 KB of heap per file**
before a single rule runs. Clean files (the overwhelming majority in
real projects) produce zero diagnostics, so the allocation is almost
entirely wasted; files that produce >512 diagnostics realloc past the
initial capacity anyway.

## Change

Start with `Vec::new()` (zero allocation for clean files). On the first
push, `reserve(32)` so files that do report skip the 4→8→16→32 growth
steps and start at ~11 KB instead of ~180 KB.

## Memory impact

`oxlint` over the vscode repo (10,572 files, default 95-rule
`correctness` set), `getrusage(RUSAGE_CHILDREN).ru_maxrss`, median of 3
interleaved runs after warmup:

| | base | after | Δ |
|---|--:|--:|--:|
| peak RSS, 64 threads | 343.7 MB | 275.3 MB | **−68.4 MB (−19.9%)** |
| peak RSS, `--threads=1` | 214.5 MB | 142.7 MB | **−71.8 MB (−33.5%)**
|
| minor page faults, 64 threads | 99,367 | 79,974 | −19.5% |
| minor page faults, `--threads=1` | 60,228 | 41,931 | −30.4% |

No wall-clock regression (criterion `linter` bench is flat on all five
fixtures, within noise).

## Verification

- `cargo test -p oxc_linter --lib` — 1162 tests pass.
- Behavior unchanged: `Vec::new()` + `reserve` only affects allocation
strategy.

---------

Co-authored-by: Cameron <cameron.clark@hey.com>
camc314 added a commit that referenced this pull request Jul 3, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 88f4455 str: [**BREAKING**] `Str` and `Ident` methods take
`&GetAllocator` (#23781) (overlookmotel)

### 🚀 Features

- f2091b3 ast: Unify old and new `AstBuilder`s (#23875) (overlookmotel)
- 1c8f50c linter: Add schema for `eslint/no-restricted-import` (#23642)
(Sysix)

### 🐛 Bug Fixes

- 7cb85c4 linter/eslint/no-negated-condition: Add autofix for negated
conditions (#23825) (Yagiz Nizipli)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d891990 linter/jsx-a11y/role-supports-aria-props: Ignore nullish prop
values (#23865) (Mikhail Baev)
- 94b6599 linter: Deduplicate missing plugin errors (#23853) (camc314)
- eff3eff linter/oxc/branches-sharing-code: Avoid else-if false
positives (#23843) (camc314)
- 2a2d3b9 linter/eslint/prefer-destructuring: Skip
`AssignmentExpression` autofixes (#23818) (camc314)
- ddc24ae linter/eslint/id-length: Respect checkGeneric for mapped type
keys (#23802) (bab)
- cd89202 linter/react/exhaustive-deps: Skip wrapper expression when
analyzing hook initializers (#23793) (camc314)
- 20e8285 linter/unicorn/prefer-native-coercion-function: Allow ts type
predicates (#23774) (camc314)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)
- 52032cf linter: Newline-terminate tsgolint errors (#23762) (Mikhail
Baev)
- 368fda7 linter/eslint/no-warning-comments: Avoid dropping generated
regex patterns (#23741) (camc314)
- ce44fbd linter/valid-title: Escape disallowed words regex (#23742)
(camc314)
- 3100d11 linter/prefer-called-exactly-once-with: Avoid out-of-bounds
slice panic at end of file (#23625) (Jerry Zhao)
- 742be36 refactor/node/handle-callback-err: Reject invalid regex config
(#23740) (camc314)
- d7be179 linter/eslint/no-restricted-globals: Handle shadowed locals
(#23736) (camc314)
- b3b1ff8 linter/vitest/expect-expect: Handle global vitest detection
correctly (#23734) (camc314)

### ⚡ Performance

- 68f9472 linter/jsx-a11y: Skip lowercasing non-aria attribute names
(#23906) (Lawrence Lin)
- b9312b4 linter/unicorn/prefer-export-from: Use keyed binding lookup
(#23893) (Marius Schulz)
- cd5204e linter/typescript/no-unsafe-declaration-merging: Use keyed
binding lookup (#23894) (Marius Schulz)
- e948498 linter/eslint/prefer-named-capture-group: Only dispatch for
relevant node types (#23868) (Connor Shea)
- 4ac7a8e linter/eslint/max-depth: Derive node types (#23896) (Connor
Shea)
- daeed09 linter/eslint/no-restricted-globals: Only scan unresolved
references (#23890) (camc314)
- e808514 linter/jest-vitest: Speed up no-standalone-expect (#23883)
(camc314)
- 8b165e5 linter/react/exhaustive-deps: Skip non-reactive calls early
(#23882) (camc314)
- 54005e7 linter/eslint/no-unused-vars: Precompute exported bindings
(#23881) (camc314)
- 9bc2f8c linter/unicorn/prefer-number-properties: Speed up global
checks (#23880) (camc314)
- 4ff104f linter: Optimize `require-hook` and `prefer-mock-*` rules to
run on specific node types (#23871) (Connor Shea)
- cc2213b linter: Run `no-underscore-dangle` only when relevant node
types are present (#23867) (Connor Shea)
- 3e55c21 linter/promise/always-return: Narrow to function node types
(#23878) (Connor Shea)
- 7136182 linter/jest-vitest: Speed up no-commented-out-tests (#23864)
(camc314)
- f138264 linter/eslint/no-script-url: Match javascript: prefix without
allocating (#23861) (Lawrence Lin)
- 7ef6895 linter/react/no-array-index-key: Delay index symbol lookup
(#23857) (camc314)
- 26bc171 linter/react/no-array-index-key: Match callback methods
directly (#23856) (camc314)
- 44fbbda linter/jsx-a11y/interactive-supports-focus: Check cheap
conditions first (#23854) (camc314)
- 84a5aa3 linter/eslint/no-extend-native: Skip lowercase references
early (#23851) (camc314)
- 88a74b2 linter/eslint/no-nonoctal-decimal-escape: Scan decimal escapes
as bytes (#23850) (camc314)
- fca69a8 linter: Skip traversal without this expressions (#23845)
(camc314)
- 838fd63 linter: Reduce preallocation for per-file diagnostics `Vec`
(#23705) (Marius Schulz)
- 417b506 linter/typescript/array-type: Remove full source text clone
(#23751) (Marius Schulz)

### 📚 Documentation

- 57e4469 linter/unicorn: Update prefer-dom-node-text-content rationale
(#23933) (Mikhail Baev)
- 3d61dea all: Correct capitalization in comments (#23887)
(overlookmotel)

### 🛡️ Security

- 3cdd18f deps: Update npm packages (#23690) (renovate[bot])
# Oxfmt
### 💥 BREAKING CHANGES

- 259e0cd oxfmt,formatter_graphql: [**BREAKING**] Support draft syntax
with removing prettier fallback (#23326) (leaysgur)
- accbc49 oxfmt: [**BREAKING**] Format `parser:css,less,scss` files +
css-in-js by `oxc_formatter_css` (#23321) (leaysgur)

### 🚀 Features

- dffa4b3 formatter_css: Implement `oxc_formatter_css` (#23320)
(leaysgur)
- 01de9ec oxfmt: Format `parser:graphql` files by
`oxc_formatter_graphql` (#23318) (leaysgur)
- 4e66212 formatter_graphql: Implement oxc_formatter_graphql (#23317)
(leaysgur)

### 🐛 Bug Fixes

- 67325ae formatter_css: Handle frontmatter language (#23819) (leaysgur)
- 3f355e5 formatter_graphql: Improve major prettier diffs (#23419)
(leaysgur)
- 48e2d78 formatter_css: Improve major prettier diffs (#23327)
(leaysgur)
- 8c07cad all: Enable `disable_old_builder` Cargo feature for `oxc_ast`
crate in tests (#23888) (overlookmotel)
- f7d1f50 oxlint, oxfmt: Enable `disable_old_builder` Cargo feature for
`oxc_ast` crate (#23886) (overlookmotel)
- d86f60b lsp: Normalize user config path to watch pattern (#23723)
(Sysix)

### ⚡ Performance

- 4ddcba0 formatter_core: Add printable-ASCII fast path to TextWidth
(#23913) (Lawrence Lin)

### 📚 Documentation

- b4d0dc9 oxfmt,formatter,formatter_css,formatter_core: Update AGENTS.md
(#23814) (leaysgur)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants