Skip to content

Comments

perf(linter/plugins): reduce cost of workspaces#18758

Merged
graphite-app[bot] merged 1 commit intomainfrom
om/01-31-perf_linter_plugins_reduce_cost_of_workspaces
Jan 31, 2026
Merged

perf(linter/plugins): reduce cost of workspaces#18758
graphite-app[bot] merged 1 commit intomainfrom
om/01-31-perf_linter_plugins_reduce_cost_of_workspaces

Conversation

@overlookmotel
Copy link
Member

@overlookmotel overlookmotel commented Jan 31, 2026

Workspaces support (#17809) added overhead on JS side of hashmap lookups. Every call to lintFile has to look up details (e.g. registered rules, rule options) from hashmaps keyed by workspace URI.

Perf

Take a different approach which is optimized for common case of there being only 1 workspace:

Before:

  • Get rules, options, and CWD from hashmaps keyed by workspace URI.

After:

  • Quick check "is the current workspace the one we need?".
  • If so, do nothing - the top-level vars registeredRules, allOptions, and cwd elready contain the right settings for the desired workspace.
  • If current workspace is not the right one, look up the details for desired workspace in workspaces hash map and write them into the top-level vars.

In CLI, workspaceUri passed to lintFile etc is null, so the quick "in the right workspace?" check is particularly cheap - just null === null. So this reduces the overhead that workspace support adds to CLI to just that 1 instruction.

Add of these functions are entirely synchronous, so there's no possibility of a race where 1 call switches out the values in the top-level vars, while another call is busy using them.

CLI: No call to createWorkspace

CLI now doesn't need to call createWorkspace at all. The "are we in right workspace?" check always succeeds, so it's irrelevant whether a workspace has been created or not - workspaces hashmap is never accessed.

So workspaces is now an LSP-only thing.

Bundle

This PR also removes all imports from other files from src-js/workspace/index.ts. It now only imports debugAssert, which is optimized out in release build., leaving no imports at all.

Previously, the imports from other files were causing most of the JS plugins code to get loaded on first call to createWorkspace, which broke the lazy loading optimization - Oxlint CLI and LSP were both loading all the JS plugins code eagerly even if the user doesn't use JS plugins.

With this change, the lazy loading optimization is restored. This is visible in the conformance snapshot change in this PR.

Copy link
Member Author

overlookmotel commented Jan 31, 2026

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 workspace management in the JS plugin system by reducing overhead for the common single-workspace case. Instead of looking up workspace data in hash maps on every operation, it maintains a "current workspace" concept and stores data in top-level variables. Switching workspaces only occurs when necessary, making the CLI path (with null workspace URI) extremely fast - just a single null check.

Changes:

  • Removed eager workspace setup from CLI, enabling lazy loading of JS plugin code
  • Refactored workspace management to use direct variable access instead of hash map lookups
  • Introduced a workspace switching mechanism that's optimized for single-workspace and same-workspace scenarios

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/oxlint/src/lint.rs Removed CLI workspace setup code since CLI no longer needs to call createWorkspace
apps/oxlint/src-js/workspace/index.ts Refactored workspace management with current workspace tracking and removed imports to enable lazy loading
apps/oxlint/src-js/plugins/workspace.ts New file implementing workspace switching logic with fast-path optimization
apps/oxlint/src-js/plugins/options.ts Converted from Map-based to direct variable access, integrated workspace switching
apps/oxlint/src-js/plugins/load.ts Converted from Map-based to direct variable access for rules, integrated workspace switching
apps/oxlint/src-js/plugins/lint.ts Updated to use workspace switching instead of hash map lookups
apps/oxlint/src-js/plugins/context.ts Separated CWD management from file context setup to support workspace switching
apps/oxlint/src-js/package/rule_tester.ts Removed workspace creation/destruction, simplified cleanup logic
apps/oxlint/conformance/snapshots/stylistic.md Updated stack traces reflecting bundle restructuring from lazy loading optimization

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

@overlookmotel overlookmotel force-pushed the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch from ac826e7 to 7778e54 Compare January 31, 2026 13:49
@overlookmotel overlookmotel force-pushed the om/01-31-refactor_linter_plugins_remove_workspace_and_workspaceidentifier_types branch from 96b68e3 to 7dbaf0f Compare January 31, 2026 13:49
@overlookmotel overlookmotel force-pushed the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch from 7778e54 to cd7ed7a Compare January 31, 2026 14:13
@overlookmotel
Copy link
Member Author

overlookmotel commented Jan 31, 2026

CI is failing on #18763, but that's probably not related to the changes in that PR. VSCode task didn't run on this PR, and I think it's more likely that it's this PR that's actually causing the fail.

VSCode task did run on #18755 lower down the stack, and it passed. And this is the only other PR in the stack which makes significant changes.

EDIT: CI now failing on #18755 too! Seems like it's flaky.

@overlookmotel overlookmotel force-pushed the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch from cd7ed7a to a4af0c5 Compare January 31, 2026 15:14
@overlookmotel overlookmotel force-pushed the om/01-31-refactor_linter_plugins_remove_workspace_and_workspaceidentifier_types branch from 7dbaf0f to 38e069f Compare January 31, 2026 15:14
@camc314 camc314 added the 0-merge Merge with Graphite Merge Queue label Jan 31, 2026
Copy link
Contributor

camc314 commented Jan 31, 2026

Merge activity

@graphite-app graphite-app bot added 0-merge Merge with Graphite Merge Queue and removed 0-merge Merge with Graphite Merge Queue labels Jan 31, 2026
@graphite-app graphite-app bot force-pushed the om/01-31-refactor_linter_plugins_remove_workspace_and_workspaceidentifier_types branch from 38e069f to 3ea3ea1 Compare January 31, 2026 15:43
graphite-app bot pushed a commit that referenced this pull request Jan 31, 2026
Workspaces support (#17809) added overhead on JS side of hashmap lookups. Every call to `lintFile` has to look up details (e.g. registered rules, rule options) from hashmaps keyed by workspace URI.

### Perf

Take a different approach which is optimized for common case of there being only 1 workspace:

* At any time there's 1 "current workspace".
* Store rules, options, and CWD in top-level vars (back to how it was before #17809).
* Switch workspaces only when required. Switching reassigns the values in these top level vars.

Before:

* Get rules, options, and CWD from hashmaps keyed by workspace URI.

After:

* Quick check "is the current workspace the one we need?".
* If so, do nothing - the top-level vars `registeredRules`, `allOptions`, and `cwd` elready contain the right settings for the desired workspace.
* If current workspace is not the right one, look up the details for desired workspace in `workspaces` hash map and write them into the top-level vars.

In CLI, `workspaceUri` passed to `lintFile` etc is `null`, so the quick "in the right workspace?" check is particularly cheap - just `null === null`. So this reduces the overhead that workspace support adds to CLI to just that 1 instruction.

Add of these functions are entirely synchronous, so there's no possibility of a race where 1 call switches out the values in the top-level vars, while another call is busy using them.

### CLI: No call to `createWorkspace`

CLI now doesn't need to call `createWorkspace` at all. The "are we in right workspace?" check always succeeds, so it's irrelevant whether a workspace has been created or not - `workspaces` hashmap is never accessed.

So workspaces is now an LSP-only thing.

### Bundle

This PR also removes all imports from other files from `src-js/workspace/index.ts`. It now only imports `debugAssert`, which is optimized out in release build., leaving no imports at all.

Previously, the imports from other files were causing most of the JS plugins code to get loaded on first call to `createWorkspace`, which broke the lazy loading optimization - Oxlint CLI and LSP were both loading all the JS plugins code eagerly even if the user doesn't use JS plugins.

With this change, the lazy loading optimization is restored. This is visible in the conformance snapshot change in this PR.
@graphite-app graphite-app bot force-pushed the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch from a4af0c5 to e4a85e4 Compare January 31, 2026 15:43
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Jan 31, 2026
@camc314 camc314 added the 0-merge Merge with Graphite Merge Queue label Jan 31, 2026
@camc314 camc314 changed the base branch from om/01-31-refactor_linter_plugins_remove_workspace_and_workspaceidentifier_types to graphite-base/18758 January 31, 2026 16:08
graphite-app bot pushed a commit that referenced this pull request Jan 31, 2026
Workspaces support (#17809) added overhead on JS side of hashmap lookups. Every call to `lintFile` has to look up details (e.g. registered rules, rule options) from hashmaps keyed by workspace URI.

### Perf

Take a different approach which is optimized for common case of there being only 1 workspace:

* At any time there's 1 "current workspace".
* Store rules, options, and CWD in top-level vars (back to how it was before #17809).
* Switch workspaces only when required. Switching reassigns the values in these top level vars.

Before:

* Get rules, options, and CWD from hashmaps keyed by workspace URI.

After:

* Quick check "is the current workspace the one we need?".
* If so, do nothing - the top-level vars `registeredRules`, `allOptions`, and `cwd` elready contain the right settings for the desired workspace.
* If current workspace is not the right one, look up the details for desired workspace in `workspaces` hash map and write them into the top-level vars.

In CLI, `workspaceUri` passed to `lintFile` etc is `null`, so the quick "in the right workspace?" check is particularly cheap - just `null === null`. So this reduces the overhead that workspace support adds to CLI to just that 1 instruction.

Add of these functions are entirely synchronous, so there's no possibility of a race where 1 call switches out the values in the top-level vars, while another call is busy using them.

### CLI: No call to `createWorkspace`

CLI now doesn't need to call `createWorkspace` at all. The "are we in right workspace?" check always succeeds, so it's irrelevant whether a workspace has been created or not - `workspaces` hashmap is never accessed.

So workspaces is now an LSP-only thing.

### Bundle

This PR also removes all imports from other files from `src-js/workspace/index.ts`. It now only imports `debugAssert`, which is optimized out in release build., leaving no imports at all.

Previously, the imports from other files were causing most of the JS plugins code to get loaded on first call to `createWorkspace`, which broke the lazy loading optimization - Oxlint CLI and LSP were both loading all the JS plugins code eagerly even if the user doesn't use JS plugins.

With this change, the lazy loading optimization is restored. This is visible in the conformance snapshot change in this PR.
@graphite-app graphite-app bot force-pushed the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch from e4a85e4 to 84a1363 Compare January 31, 2026 16:10
@graphite-app graphite-app bot changed the base branch from graphite-base/18758 to om/01-31-refactor_linter_plugins_remove_workspace_and_workspaceidentifier_types January 31, 2026 16:10
Base automatically changed from om/01-31-refactor_linter_plugins_remove_workspace_and_workspaceidentifier_types to main January 31, 2026 16:16
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Jan 31, 2026
Workspaces support (#17809) added overhead on JS side of hashmap lookups. Every call to `lintFile` has to look up details (e.g. registered rules, rule options) from hashmaps keyed by workspace URI.

### Perf

Take a different approach which is optimized for common case of there being only 1 workspace:

* At any time there's 1 "current workspace".
* Store rules, options, and CWD in top-level vars (back to how it was before #17809).
* Switch workspaces only when required. Switching reassigns the values in these top level vars.

Before:

* Get rules, options, and CWD from hashmaps keyed by workspace URI.

After:

* Quick check "is the current workspace the one we need?".
* If so, do nothing - the top-level vars `registeredRules`, `allOptions`, and `cwd` elready contain the right settings for the desired workspace.
* If current workspace is not the right one, look up the details for desired workspace in `workspaces` hash map and write them into the top-level vars.

In CLI, `workspaceUri` passed to `lintFile` etc is `null`, so the quick "in the right workspace?" check is particularly cheap - just `null === null`. So this reduces the overhead that workspace support adds to CLI to just that 1 instruction.

Add of these functions are entirely synchronous, so there's no possibility of a race where 1 call switches out the values in the top-level vars, while another call is busy using them.

### CLI: No call to `createWorkspace`

CLI now doesn't need to call `createWorkspace` at all. The "are we in right workspace?" check always succeeds, so it's irrelevant whether a workspace has been created or not - `workspaces` hashmap is never accessed.

So workspaces is now an LSP-only thing.

### Bundle

This PR also removes all imports from other files from `src-js/workspace/index.ts`. It now only imports `debugAssert`, which is optimized out in release build., leaving no imports at all.

Previously, the imports from other files were causing most of the JS plugins code to get loaded on first call to `createWorkspace`, which broke the lazy loading optimization - Oxlint CLI and LSP were both loading all the JS plugins code eagerly even if the user doesn't use JS plugins.

With this change, the lazy loading optimization is restored. This is visible in the conformance snapshot change in this PR.
@camc314 camc314 force-pushed the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch from 84a1363 to 9862224 Compare January 31, 2026 16:20
@camc314 camc314 added the 0-merge Merge with Graphite Merge Queue label Jan 31, 2026
@graphite-app graphite-app bot merged commit 9862224 into main Jan 31, 2026
19 checks passed
@graphite-app graphite-app bot deleted the om/01-31-perf_linter_plugins_reduce_cost_of_workspaces branch January 31, 2026 16:26
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Jan 31, 2026
camc314 added a commit that referenced this pull request Feb 2, 2026
# Oxlint
### 💥 BREAKING CHANGES

- b34a155 linter/plugins: [**BREAKING**] `RuleTester` set
`context.filename` to absolute path (#18702) (overlookmotel)

### 🚀 Features

- 1753209 linter/vscode: Run extension when JS configs are detected
(#18832) (camc314)
- c962dd2 linter/lsp: Implement support for oxlint.config.ts (#18826)
(camc314)
- da32203 linter: Auto generate oxlint.config.ts types (#18597)
(camc314)
- 19b4df7 oxlint: Introduce `defineConfig` helper (#18596) (camc314)
- ea97231 linter: Implement `oxlint.config.ts` support (#17563)
(camc314)
- 17ca42d linter: Implement `react/no-multi-comp` rule. (#18794)
(connorshea)
- 88f30e0 linter/plugins: Move eslint compatible plugin conversion to
`eslintCompatPlugin` function (#18791) (overlookmotel)
- 2a72794 linter/plugins: `RuleTester` take `cwd` property (#18756)
(overlookmotel)
- 9f533db linter: Add `find_prev_token_within` method for token search
(#18769) (camc314)
- 772ea70 linter: Introduce `load_js_configs` napi callback (#18767)
(camc314)
- e9690c1 linter: Introduce `DiscoveredConfig` in preparation for JS
configs (#18674) (camc314)
- 558b588 linter/prefer-namespace-keyword: Move to correctness (#18733)
(camc314)
- 7a5c268 oxlint/lsp: Support `jsPlugins` (#17840) (Sysix)
- c07497c linter/prefer-modern-dom-apis: Implement suggestion (#17965)
(Mikhail Baev)
- 8531bc9 linter: Implement `prefer-const` (#18687) (camchenry)
- 8670b18 parser: Error on ambient class accessor implementations
(#18592) (camc314)
- 6b8a5ae linter: Add `eslint-plugin-import/no-nodejs-modules` rule
(#18006) (Mikhail Baev)
- 04f400d linter/no-duplicates: Add support for `considerQueryString`
option (#18657) (camc314)
- 3b7f260 linter/consistent-generic-constructor: Implement fixer
(#18616) (camc314)
- 794f9e4 linter/prefer-exponentation-operator: Implement suggestion
(#18602) (camc314)
- 773d916 linter: `eslint/sort_keys` ignore leading and trailing spreads
in auto-fix (#18485) (Lonami)
- 20d4ede linter: Implement `import/no-relative-parent-imports` rule
(#18513) (Valentin Maerten)
- 0da45ef vscode: Fallback to globally installed oxlint/oxfmt packages
(#18007) (Sysix)

### 🐛 Bug Fixes

- a3417b1 linter/plugins: Clear state when reloading workspace (#18837)
(overlookmotel)
- c879992 linter: Error on arrays passed in as config (#18822) (camc314)
- 5c80422 linter/tsdown: Ensure relative path for globals import starts
with `./` (#18820) (camc314)
- 7419dfb linter: Remove invalid debug assersion, add test (#18819)
(camc314)
- 0ca6269 ci: Fix the repo path normalization logic for tests on
Windows. (#18815) (connorshea)
- c7b0a65 linter: Fix config option docs for `react/jsx-boolean-value`
rule. (#18811) (connorshea)
- cce374e linter/prefer-const: Replace entire declaration over just the
`let` kw (#18814) (camc314)
- 41f92d1 linter: Error when given config options for a lint rule that
has no config options defined. (#18809) (connorshea)
- 0867a36 linter/consistent-index-object-style: False positive with
mapped + generic types (#18801) (camc314)
- 1d34b42 linter: Fix 32 bit build (#18783) (camc314)
- 95df577 linter/plugins: Handle error from `destroyWorkspace` (#18763)
(overlookmotel)
- b3261dc linter: Fix the curly rule config to enforce the shape of the
config and emit correct docs (#18743) (connorshea)
- d981978 linter/plugins: Use non-blocking mode when calling
`destroyWorkspace` (#18762) (overlookmotel)
- 3f43d4c linter: Accept bools as valid values for `fixable` (#18772)
(camc314)
- 005910a linter/plugins: Support plugins outside of workspace (#18755)
(overlookmotel)
- fd92711 vscode: Use `fsPath` for workspace mapping (#18728) (Sysix)
- 358b2c1 linter/consistent-generic-constructors: Check bounds when
searching for `:` token (#18745) (connorshea)
- abd0c28 linter/capitalized-comments: Fix generated rule option docs
(#18748) (connorshea)
- d90a9f6 linter: Add more tests for `prefer-const`'s fixer and fix its
invalid behavior. (#18747) (connorshea)
- f82011b oxlint/lsp: Disable JS plugins support in LSP except in tests
(#18727) (overlookmotel)
- 94505c8 linter/jest: Change `prefer-spy-on` autofix to suggestion
(#18152) (Ben Lowery)
- 6ec1112 linter: Mark unused disable directive fix as suggestion
(#18703) (ddmoney420)
- 49609ec linter/no-useless-constructor: Consider argument
transformation as used (#18706) (ddmoney420)
- 40218de linter: Fix behavior of
jsx-a11y/no-static-element-interactions rule. (#17817) (connorshea)
- db9751d linter/no-html-link-for-pages: Handle `target=_blank`
correctly (#18693) (camc314)
- e440b78 linter/plugins: Pass all args to CFG event handlers when 2
rules use same handler (#18683) (overlookmotel)
- b393430 linter/curly: Fix multi-or-nest and consistent conflict
(#18660) (camc314)
- 2e1fbc2 linter/plugins: Implement `context.parserPath` (#18644)
(overlookmotel)
- 34951ed linter/plugins: `filename` option takes precedence over
`parserOptions.lang` in `RuleTester` (#18643) (overlookmotel)
- 28df160 linter/plugins: Allow line number passed to `report` to be 0
(#18642) (overlookmotel)
- 14fabec vscode: Use built-in `getWorkspaceFolder` for detecting the
right workspace of a given uri (#18583) (Sysix)
- 0ff4cea oxlint/cli: Report error when nested config could not be
parsed (#18504) (Sysix)

### ⚡ Performance

- 9862224 linter/plugins: Reduce cost of workspaces (#18758)
(overlookmotel)
- 6bc0bde linter: Remove string allocation (#18725) (overlookmotel)
- 3a6b41e linter/plugins: Replace ESLint Traverser with lightweight
traverseNode (#18529) (Rintaro Itokawa)

### 📚 Documentation

- dd1a653 linter: Fix doc comment for ignoreStateless config option.
(#18808) (connorshea)
- 5909085 linter/plugins: Add doc comments (#18753) (overlookmotel)
- ffe53a3 linter: Update lint function docs (#18766) (camc314)
- b82faec linter: Glob for any css module for no-unassigned-import
(#18713) (Ben Stickley)
- cd86347 linter: Mark some react rules as unsupported, misc docs
improvements (#18617) (connorshea)
- 23401d8 linter: Update fixes and suggestions status for tsgolint rules
(#18619) (camchenry)
# Oxfmt
### 🚀 Features

- ee30de9 oxfmt: Add config migration from biome (#18638) (Luca Fischer)

### 🐛 Bug Fixes

- e754b18 oxfmt/migrate-prettier: Set `experimentalSortPackagejson:
false` by default (#18831) (leaysgur)
- a83c266 formatter: Keep decorated function pattern hugged when params
break (#18830) (Dunqing)
- 0c8efe1 formatter: Quote numeric property keys with `quoteProps:
consistent` (#18803) (Dunqing)
- 9c14c3e formatter: Ignore comment does not work for sequence
expressions in arrow function body (#18799) (Dunqing)
- 54984ae formatter: Handle leading comments in arrow function sequence
expressions (#18798) (Dunqing)
- 61bb2b5 formatter: Correctly expand JSX returned from arrow callbacks
in JSX expression containers (#18797) (Dunqing)
- 34ee194 formatter: Tailwindcss sorting doesn't work for object
property keys (#18773) (Dunqing)
- 48f1e35 oxfmt: Prevent ThreadsafeFunction crash on Node.js exit
(#18723) (Boshen)
- e96adca formatter: Follow Prettier's approach for for-in initializer
parentheses (#18695) (Dunqing)
- 1215a6f formatter: Preserve quote for class property key in TypeScript
(#18692) (Dunqing)
- 059acae formatter: Incorrect comments placement for union type in
`TSTypeIntersection` (#18690) (Dunqing)
- c3d05c1 formatter,oxfmt: Handle CRLF with embedded formatting (#18686)
(leaysgur)
- 7cb3085 formatter: Preserve comments on rest elements (#18649)
(Dunqing)
- 21984dd formatter: Preserve type cast comments on rest parameters
(#18648) (Dunqing)
- 2f70254 formatter: Don't add extra semicolon on suppressed class
properties (#18631) (Dunqing)
- ac1ff4e oxfmt: Use `empty_line` IR for empty xxx-in-js line (#18623)
(leaysgur)
- 8f76900 oxfmt: Dedent xxx-in-js templates before calling prettier
(#18622) (leaysgur)
- 6b726ef oxfmt: Trim whitespace only xxx-in-js templates (#18621)
(leaysgur)

Co-authored-by: camc314 <18101008+camc314@users.noreply.github.com>
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.

2 participants