Skip to content

perf(linter/reporter/stylish): compute diagnostic Info once per diagnostic#24525

Merged
graphite-app[bot] merged 1 commit into
mainfrom
perf-stylish-formatter
Jul 15, 2026
Merged

perf(linter/reporter/stylish): compute diagnostic Info once per diagnostic#24525
graphite-app[bot] merged 1 commit into
mainfrom
perf-stylish-formatter

Conversation

@connorshea

@connorshea connorshea commented Jul 14, 2026

Copy link
Copy Markdown
Member

AI Disclosure: This was generated with Claude Code, Fable 5. It has been tested and reviewed by me for correctness.

format_stylish recomputed Info::new for every diagnostic in multiple passes — inside sort_by_key (which re-evaluates its key on every comparison), then again when grouping by filename, measuring the position column width, and rendering. Each Info::new call performs two read_span calls, which scan the file's source text from offset 0 to compute line/column, so --format=stylish output time grew quadratically with the number of diagnostics in a file.

This change computes Info once per diagnostic into a Vec<(Info, &Error)> and reuses it for sorting, grouping, and rendering. Output is byte-identical (all output-formatter snapshot tests pass unchanged).

This does have a downside in that it uses more peak memory in storing the Info objects in the Vec. I think the time savings are worth it, and it'd only be a problem for the (less likely) case where there are thousands of diagnostics. Though that's also the case where the optimization is most notable, obviously.

Benchmark

Fixture: a single 1.4MB file with 20k debugger; statements interleaved with filler lines (and a 10× smaller variant), release build, macOS.

Command Before After
--format=stylish, 20k diagnostics 98.5s 18.9s (5.2× faster)
--format=stylish, 2k diagnostics 1.09s 0.22s
--format=unix, 20k diagnostics (unchanged reference) 20.6s 20.6s

The fixed stylish now matches the streaming unix formatter on the same input. The remaining ~19s is a separate, shared issue: Info::new scans the source linearly per diagnostic, making every formatter that uses it quadratic in diagnostics-per-file — that would need a fix in oxc_diagnostics (e.g. computing positions for all of a file's diagnostics in one scan) and is not addressed here.

@github-actions github-actions Bot added A-linter Area - Linter A-cli Area - CLI labels Jul 14, 2026
@connorshea
connorshea marked this pull request as ready for review July 14, 2026 23:39
@connorshea
connorshea requested a review from camc314 as a code owner July 14, 2026 23:39
@camc314 camc314 added the 0-merge Merge with Graphite Merge Queue label Jul 15, 2026
@camc314 camc314 self-assigned this Jul 15, 2026

camc314 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Merge activity

…ostic (#24525)

AI Disclosure: This was generated with Claude Code, Fable 5. It has been tested and reviewed by me for correctness.

`format_stylish` recomputed `Info::new` for every diagnostic in multiple passes — inside `sort_by_key` (which re-evaluates its key on every comparison), then again when grouping by filename, measuring the position column width, and rendering. Each `Info::new` call performs two `read_span` calls, which scan the file's source text from offset 0 to compute line/column, so `--format=stylish` output time grew quadratically with the number of diagnostics in a file.

This change computes `Info` once per diagnostic into a `Vec<(Info, &Error)>` and reuses it for sorting, grouping, and rendering. Output is byte-identical (all output-formatter snapshot tests pass unchanged).

This does have a downside in that it uses more peak memory in storing the `Info` objects in the Vec. I think the time savings are worth it, and it'd only be a problem for the (less likely) case where there are thousands of diagnostics. Though that's also the case where the optimization is most notable, obviously.

## Benchmark

Fixture: a single 1.4MB file with 20k `debugger;` statements interleaved with filler lines (and a 10× smaller variant), release build, macOS.

| Command | Before | After |
|---|---|---|
| `--format=stylish`, 20k diagnostics | 98.5s | 18.9s (5.2× faster) |
| `--format=stylish`, 2k diagnostics | 1.09s | 0.22s |
| `--format=unix`, 20k diagnostics (unchanged reference) | 20.6s | 20.6s |

The fixed `stylish` now matches the streaming `unix` formatter on the same input. The remaining ~19s is a separate, shared issue: `Info::new` scans the source linearly per diagnostic, making every formatter that uses it quadratic in diagnostics-per-file — that would need a fix in `oxc_diagnostics` (e.g. computing positions for all of a file's diagnostics in one scan) and is not addressed here.
@graphite-app
graphite-app Bot force-pushed the perf-stylish-formatter branch from a879290 to 90ae040 Compare July 15, 2026 10:13
@graphite-app
graphite-app Bot merged commit 90ae040 into main Jul 15, 2026
27 checks passed
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Jul 15, 2026
@graphite-app
graphite-app Bot deleted the perf-stylish-formatter branch July 15, 2026 10:18
camc314 added a commit that referenced this pull request Jul 21, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 7b045cd minfier: Drop last break from last switch case (#24673)
(Armano)
- dd18383 linter/node: Implement no-top-level-await rule (#24634)
(Connor Shea)
- 16a65f2 linter/react: Implement function-component-definition rule
(#24471) (Cole Ellison)
- 7f1f585 linter: Reuse `jest/padding-around-test-blocks` for
`vitest/padding-around-test-blocks` (#24519) (Mikhail Baev)
- 99978a8 linter/import/consistent-type-specifier-style: Support
`prefer-top-level-if-only-type-imports` option (#24502) (camc314)

### 🐛 Bug Fixes

- 0184ad6 linter/unicorn/no-useless-undefined: Preserve valid parameter
defaults (#24686) (camc314)
- 8694167 linter/eslint/prefer-destructuring: Handle typed declarations
(#24616) (camc314)
- 477cf0f linter/eslint/no-throw-literal: Handle assigned errors
(#24561) (Cole Ellison)
- ac9200a linter: Detect React components from returned JSX (#24521)
(camc314)
- c0a6522 linter/eslint/no-useless-computed-key: Allow TS syntax in
computed keys (#24524) (Cole Ellison)

### ⚡ Performance

- 346eed1 linter/unicorn/prefer-event-target: Only run on `Class` and
`NewExpression` nodes (#24685) (Mikhail Baev)
- 7be5cf0 oxlint/lsp: Only invoke lint on code actions when document is
not opened (#24676) (Sysix)
- d3f07a0 diagnostics: Box OxcDiagnosticInner to reduce binary size
(#24665) (Boshen)
- 90ae040 linter/reporter/stylish: Compute diagnostic Info once per
diagnostic (#24525) (connorshea)

### 📚 Documentation

- e6f7174 linter/valid-expect: Fix correct example being identical to
incorrect one (#24468) (mkan0141)
# Oxfmt
### 💥 BREAKING CHANGES

- 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and
`NewTarget` (#24557) (camc314)

### 🚀 Features

- 3d22307 parser: Add `ParseOptions::enable_ident_hashes` (#24491)
(Boshen)

### 🐛 Bug Fixes

- 6fe866a oxfmt: Keep tailwind classes glued to template expr with
`preserveWhitespace` (#24609) (leaysgur)
- 33e32d8 formatter_css: Use `line_suffix` for EOL line comment (#24580)
(leaysgur)
- 5f76998 formatter_graphql: Keep same line comments pending across
intervening tokens (#24579) (leaysgur)

Co-authored-by: Boshen <[email protected]>
Co-authored-by: Cameron <[email protected]>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants