From 03d6f656d96c326806f0d68f79780131f43fc426 Mon Sep 17 00:00:00 2001 From: YT Date: Mon, 19 Oct 2026 16:30:22 +0900 Subject: [PATCH] perf(linter): disable papaya's `fast-barrier` to avoid a `membarrier` wait on Linux (#27954) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. image ## 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. --- Cargo.lock | 4 ---- Cargo.toml | 2 +- deny.toml | 9 +++++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e86f80ba24..80f1dbc282 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3507,10 +3507,6 @@ name = "seize" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] [[package]] name = "self_cell" diff --git a/Cargo.toml b/Cargo.toml index f4d9ef1006..f00291f0f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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-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`) -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 percent-encoding = "2.3.2" # URL encoding petgraph = { version = "0.8.3", default-features = false } # Graph data structures diff --git a/deny.toml b/deny.toml index d3cbedffa8..57c0e35cb1 100644 --- a/deny.toml +++ b/deny.toml @@ -180,6 +180,15 @@ skip-tree = [ # { 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`. # More documentation about the 'sources' section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html