perf(linter): disable papaya's fast-barrier to avoid a membarrier wait on Linux (#27954)

## Summary

On Linux, every `oxlint` run waits 20–40 ms when it creates its first
papaya map. This PR removes the wait for both the CLI and the npm
package by disabling `fast-barrier` feature of the papaya crate. It also
adds a `cargo deny` rule that fails if any crate enables the feature
again so we can notice it.

## Why it waits

With `fast-barrier`, `seize::Collector::new()` calls
`membarrier(MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED)`. If the process
already has more than one thread, the kernel waits for an RCU grace
period before it returns
([kernel/sched/membarrier.c](https://github.com/torvalds/linux/blob/v6.12/kernel/sched/membarrier.c#L442-L470)).

`oxc_linter` has one papaya map:
[`modules_by_path`](https://github.com/oxc-project/oxc/blob/faa75d8a8dff299eb9607ae65d04dfb163c28a8c/crates/oxc_linter/src/service/runtime.rs#L59-L66),
the module graph that the import plugin uses for rules across files,
such as `import/no-cycle`. The map is a field of
[`Runtime`](https://github.com/oxc-project/oxc/blob/faa75d8a8dff299eb9607ae65d04dfb163c28a8c/crates/oxc_linter/src/service/runtime.rs#L45),
which holds the state of one lint run.
[`LintService::new()`](https://github.com/oxc-project/oxc/blob/faa75d8a8dff299eb9607ae65d04dfb163c28a8c/crates/oxc_linter/src/service/mod.rs#L68-L71)
creates the `Runtime` once per run.
[`Runtime::new()`](https://github.com/oxc-project/oxc/blob/faa75d8a8dff299eb9607ae65d04dfb163c28a8c/crates/oxc_linter/src/service/runtime.rs#L208)
first starts the rayon thread pool
([#L227](https://github.com/oxc-project/oxc/blob/faa75d8a8dff299eb9607ae65d04dfb163c28a8c/crates/oxc_linter/src/service/runtime.rs#L227)),
and then it creates the map
([#L268](https://github.com/oxc-project/oxc/blob/faa75d8a8dff299eb9607ae65d04dfb163c28a8c/crates/oxc_linter/src/service/runtime.rs#L268)).
So the main thread waits on every run. This happens even with the
default config, which does not use the import plugin.

The npm package waits too. Creating the map earlier would not fix it:
the binding does not run `main()`, and Node.js already runs several
threads when it loads the binding.

## Results

I measured `oxlint` with its default rules on three inputs:

- 3 files: a project with three files of 1–4 lines each, so a run is
almost only fixed cost.
- excalidraw: 590 files from
[excalidraw](https://github.com/excalidraw/excalidraw/tree/f6d85bc80fe328e8f472636eb0d541f7bb891aa0)
(`excalidraw-app` and `packages`).
- vscode `src`: 4,662 files from [vscode
1.100.0](https://github.com/microsoft/vscode/tree/19e0f9e681ecb8e5c09d8784acaa601316ca4571).

Each input ran with 1 and 8 threads, with the CLI and with the npm
package, so there are 12 cases. The machine is Linux x86_64 (AMD Zen 4,
8 vCPUs), and each bar is the mean of 30 runs.

<img width="2300" height="1520" alt="image"
src="https://github.com/user-attachments/assets/943b9ac4-9335-4ea2-b94d-498373042fae"
/>


## Is anything slower?

With `fast-barrier`, the registration at startup lets every `pin()` (one
per map call) use a `Relaxed` store. This is the fast path. Without
`fast-barrier`, seize uses its portable barriers, which `oxlint` already
uses on macOS: each pin does a `SeqCst` store (`xchg` on x86_64), and
nothing has to register at startup.

To find how much time the fast path saves, I ran three builds on vscode
`src` with 8 threads (mean of 30 runs per build, in turn):

| Build | `fast-barrier` | Waits at startup | Wall time |
|---|---|---|---|
| Before this PR | on | yes | 219.6 ms |
| Early registration\* | on | no | 191.8 ms |
| This PR | off | no | 191.9 ms |

\* Before this PR, plus one map created at the start of `main()`, so the
registration happens before the thread pool starts.

- Before this PR → Early registration: the startup wait, 27.9 ms.
- Early registration → This PR: the time that the fast path saves, 0.2
ms, too small to measure.
- With `--import-plugin` (9,317 map calls per run), the same difference
is 1.8 ms.

Windows also loses its fast path, but Windows has no startup wait to
remove. I did not measure Windows.

---

AI disclosure: I used an AI coding assistant to run the measurements and
to draft this description. I reviewed the change, the data, and this
text.
This commit is contained in:
YT authored and GitHub committed 2026-10-19 07:30:22 +00:00
1 parent a5d8184f92
commit 03d6f656d9
3 files changed
+10 -5

No files matched your search

Generated
-4
View File
@@ -3507,10 +3507,6 @@ name = "seize"
version = "0.5.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "self_cell" name = "self_cell"
+1 -1
View File
@@ -246,7 +246,7 @@ oxc-graphql-parser = "0.0.9" # GraphQL parser (direct AST; apollo-parser 0.8.6 f
oxc-markdown-parser = "0.0.3" # CommonMark + GFM parser (span-faithful, style-preserving typed AST) oxc-markdown-parser = "0.0.3" # CommonMark + GFM parser (span-faithful, style-preserving typed AST)
oxc-toml = "0.15.1" # TOML parser and formatter oxc-toml = "0.15.1" # TOML parser and formatter
oxc-yaml-parser = "0.0.7" # YAML 1.2 parser (comment-preserving typed AST, node naming from `yaml-unist-parser`) oxc-yaml-parser = "0.0.7" # YAML 1.2 parser (comment-preserving typed AST, node naming from `yaml-unist-parser`)
papaya = "0.2.4" # Concurrent hash map papaya = { version = "0.2.5", default-features = false } # Concurrent hash map
pastey = "0.2.3" # Identifier concatenation in declarative macros pastey = "0.2.3" # Identifier concatenation in declarative macros
percent-encoding = "2.3.2" # URL encoding percent-encoding = "2.3.2" # URL encoding
petgraph = { version = "0.8.3", default-features = false } # Graph data structures petgraph = { version = "0.8.3", default-features = false } # Graph data structures
+9
View File
@@ -180,6 +180,15 @@ skip-tree = [
# { name = "ansi_term", version = "=0.11.0", depth = 20 }, # { name = "ansi_term", version = "=0.11.0", depth = 20 },
] ]
# papaya enables seize's `fast-barrier` feature by default. On Linux it makes seize register
# `MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED` when the first collector is created, and in a
# multi-threaded process the kernel then waits for an RCU grace period (tens of ms, on every
# oxlint run). The workspace disables papaya's default features. Cargo features are additive,
# so this rule fails if any other crate enables the feature again and helps us notice that.
[[bans.features]]
crate = "seize"
deny = ["fast-barrier"]
# This section is considered when running `cargo deny check sources`. # This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here: # More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html