Summary
On a large (~658-crate dependency graph) Cargo/Tauri workspace with a non-default rust-analyzer.cargo.targetDir configured, the server and its rust-analyzer-proc-macro-srv child processes permanently stall — genuinely zero CPU across every thread, confirmed by sampling /proc/<pid>/stat utime/stime (not misleading averaged %CPU, which just decays toward zero while the process sits frozen). It never recovers; only killing and restarting the server "fixes" it, and the next indexing pass hits the same wall.
This reproduces on both the current stable release (0.3.2981) and the current pre-release channel build (0.4.2979), so it isn't a regression introduced by one specific build — it looks like a structural issue.
Minimal reproduction: https://github.com/De-edit/ra-deadlock-repro — a small, self-contained, public project that independently reproduces the same deadlock (see the "Minimal reproduction" section below for details).
Environment
- rust-analyzer: 0.3.2981 (stable) and 0.4.2979 (pre-release) — both affected
- VS Code: 1.129.0, Linux (Fedora 44), x86_64
- rustc/cargo: 1.92.0 stable (
edition = "2024")
- Workspace: Cargo workspace with a Tauri 2.x desktop app crate + an
xtask member, ~658 crates in the full dependency graph (tauri + plugins, tokio, reqwest, snafu, serde, tonic, opentelemetry, etc. — several proc-macro-heavy dependencies)
- Relevant
.vscode/settings.json:
Reproduction
- Open a large workspace (ours: ~658 crates) with
rust-analyzer.cargo.targetDir set to a custom, non-default directory.
- Leave both
cargo.buildScripts.enable and proc-macro support at their defaults (both enabled).
- Let the server index from a clean
target-analyzer (i.e. delete it first to force a full rebuild).
- Watch the server process: CPU usage starts high, tapers as expected for a while, then permanently flatlines to zero before indexing completes. It never recovers.
Isolation performed
| Config |
Result |
Both buildScripts.enable and proc-macro on (default) |
Reliable deadlock — reproduced repeatedly across multiple clean-rebuild attempts |
buildScripts.enable: false, proc-macro on |
Clean — confirmed via 120–150s of sustained, genuinely progressing CPU activity, no stall |
buildScripts.enable: true, procMacro.enable: false |
Clean — same confirmation method |
Both on, rust-analyzer.numThreads raised from default (6 physical cores) to 24 |
Still deadlocks — froze at the same signature (flat utime for 60+ consecutive seconds) even with 4x the thread pool capacity |
The last result seems informative: if this were simple thread-pool exhaustion (too few workers for the concurrent blocking workload), giving the pool substantially more headroom should have helped. It didn't, which points at a genuine circular wait rather than starvation.
Detection methodology (in case it's useful for others tracking this class of bug)
ps's %CPU column is a lifetime average and stays misleadingly nonzero (slowly decaying) for a long time after a process has actually frozen. We instead sampled /proc/<pid>/stat field 14 (utime) a few seconds apart, across the main server PID and both rust-analyzer-proc-macro-srv child PIDs. When frozen, utime is bit-for-bit identical across samples taken minutes apart, and every thread in /proc/<pid>/task/*/status shows State: S (sleeping) — i.e. actually blocked, not spinning.
Minimal reproduction
We built a small, self-contained, public-shareable Cargo project (no proprietary code) that independently reproduces the same deadlock, confirming this isn't specific to our real codebase. It pulls in tauri plus a handful of other proc-macro-heavy crates (serde+derive, clap+derive, snafu, strum+derive, sqlx, tonic, axum, tokio full, opentelemetry, prost/tonic-build in a real, non-no-op build.rs) to approximate the scale of the original report — 620 packages in cargo metadata, vs. ~658 in our real workspace. Same .vscode/settings.json shape (rust-analyzer.cargo.targetDir set to a custom directory, everything else default).
Opening this project fresh (code --new-window, clean/no prior target-dir) reproduced the deadlock on the first attempt: server utime went flat at a fixed value and stayed bit-for-bit identical for 90+ seconds, with every one of its ~30 threads reported as State: S (sleeping) in /proc/<pid>/task/*/status — the same signature observed on the real workspace. ps's %CPU column stayed nonzero (~16%, decaying average) the whole time despite the process being genuinely frozen, underscoring why %CPU alone is misleading for detecting this.
Repro project: https://github.com/De-edit/ra-deadlock-repro
Suspected mechanism
Not 100% confirmed, but reading the relevant source suggests a plausible root cause:
GlobalState.task_pool (crates/rust-analyzer/src/global_state.rs) is a single bounded pool, sized by main_loop_num_threads() (defaults to physical core count), shared by fetch_workspaces, fetch_build_data (build-script cargo invocation — blocks synchronously on the child process), fetch_proc_macros, and regular LSP request handling.
proc-macro-api/src/process.rs guards the proc-macro-srv stdin/stdout pipe with Mutex<ProcessSrvState>, and every request does a blocking, non-timed-out round trip while holding that lock (with_locked_io).
- On a workspace this size, with both build-script fetching and proc-macro fetching contending for the same pool at once, it seems plausible for a proc-macro request to stall (for any reason — a slow/pathological macro expansion, IPC hiccup, etc.) while holding that un-timed-out mutex, permanently blocking every subsequent proc-macro request behind it, while build-script fetching independently consumes the remaining pool capacity — and nothing left to break the cycle.
This is consistent with rust-analyzer's own prior internal deadlock in this same shape (shared target-dir + blocking cargo invocation) fixed for its own proc_macro_test build script in #9360 / #9363 (2021) — but that fix gave that specific internal process its own target directory; it wasn't a general fix, and cargo.targetDir today is still an all-or-nothing knob covering cargo check, build-scripts, and proc-macro building together, with no way to isolate them from each other.
What would help
- A timeout on the proc-macro-srv IPC round trip (so a stalled single macro expansion can't permanently wedge the shared mutex), and/or
- Separating the thread-pool capacity used for build-script/metadata fetching from the capacity used for proc-macro IPC, so one blocking subsystem can't starve the other, and/or
- Any documented way to give build-script execution and proc-macro compilation separate target directories under one workspace, rather than the current single
cargo.targetDir.
Happy to provide more logs/traces or test a patched build against our repro workspace if that's useful — the deadlock is easy to reproduce here (100% hit rate with both subsystems enabled).
Summary
On a large (~658-crate dependency graph) Cargo/Tauri workspace with a non-default
rust-analyzer.cargo.targetDirconfigured, the server and itsrust-analyzer-proc-macro-srvchild processes permanently stall — genuinely zero CPU across every thread, confirmed by sampling/proc/<pid>/statutime/stime(not misleading averaged%CPU, which just decays toward zero while the process sits frozen). It never recovers; only killing and restarting the server "fixes" it, and the next indexing pass hits the same wall.This reproduces on both the current stable release (0.3.2981) and the current pre-release channel build (0.4.2979), so it isn't a regression introduced by one specific build — it looks like a structural issue.
Minimal reproduction: https://github.com/De-edit/ra-deadlock-repro — a small, self-contained, public project that independently reproduces the same deadlock (see the "Minimal reproduction" section below for details).
Environment
edition = "2024")xtaskmember, ~658 crates in the full dependency graph (tauri + plugins, tokio, reqwest, snafu, serde, tonic, opentelemetry, etc. — several proc-macro-heavy dependencies).vscode/settings.json:{ "rust-analyzer.cargo.targetDir": "target-analyzer", "rust-analyzer.cargo.buildScripts.enable": true, // default // proc-macro support left at default (enabled) }Reproduction
rust-analyzer.cargo.targetDirset to a custom, non-default directory.cargo.buildScripts.enableand proc-macro support at their defaults (both enabled).target-analyzer(i.e. delete it first to force a full rebuild).Isolation performed
buildScripts.enableand proc-macro on (default)buildScripts.enable: false, proc-macro onbuildScripts.enable: true,procMacro.enable: falserust-analyzer.numThreadsraised from default (6 physical cores) to 24utimefor 60+ consecutive seconds) even with 4x the thread pool capacityThe last result seems informative: if this were simple thread-pool exhaustion (too few workers for the concurrent blocking workload), giving the pool substantially more headroom should have helped. It didn't, which points at a genuine circular wait rather than starvation.
Detection methodology (in case it's useful for others tracking this class of bug)
ps's%CPUcolumn is a lifetime average and stays misleadingly nonzero (slowly decaying) for a long time after a process has actually frozen. We instead sampled/proc/<pid>/statfield 14 (utime) a few seconds apart, across the main server PID and bothrust-analyzer-proc-macro-srvchild PIDs. When frozen,utimeis bit-for-bit identical across samples taken minutes apart, and every thread in/proc/<pid>/task/*/statusshowsState: S (sleeping)— i.e. actually blocked, not spinning.Minimal reproduction
We built a small, self-contained, public-shareable Cargo project (no proprietary code) that independently reproduces the same deadlock, confirming this isn't specific to our real codebase. It pulls in
tauriplus a handful of other proc-macro-heavy crates (serde+derive, clap+derive, snafu, strum+derive, sqlx, tonic, axum, tokio full, opentelemetry, prost/tonic-build in a real, non-no-opbuild.rs) to approximate the scale of the original report — 620 packages incargo metadata, vs. ~658 in our real workspace. Same.vscode/settings.jsonshape (rust-analyzer.cargo.targetDirset to a custom directory, everything else default).Opening this project fresh (
code --new-window, clean/no priortarget-dir) reproduced the deadlock on the first attempt: serverutimewent flat at a fixed value and stayed bit-for-bit identical for 90+ seconds, with every one of its ~30 threads reported asState: S (sleeping)in/proc/<pid>/task/*/status— the same signature observed on the real workspace.ps's%CPUcolumn stayed nonzero (~16%, decaying average) the whole time despite the process being genuinely frozen, underscoring why%CPUalone is misleading for detecting this.Repro project: https://github.com/De-edit/ra-deadlock-repro
Suspected mechanism
Not 100% confirmed, but reading the relevant source suggests a plausible root cause:
GlobalState.task_pool(crates/rust-analyzer/src/global_state.rs) is a single bounded pool, sized bymain_loop_num_threads()(defaults to physical core count), shared byfetch_workspaces,fetch_build_data(build-scriptcargoinvocation — blocks synchronously on the child process),fetch_proc_macros, and regular LSP request handling.proc-macro-api/src/process.rsguards the proc-macro-srv stdin/stdout pipe withMutex<ProcessSrvState>, and every request does a blocking, non-timed-out round trip while holding that lock (with_locked_io).This is consistent with rust-analyzer's own prior internal deadlock in this same shape (shared target-dir + blocking cargo invocation) fixed for its own
proc_macro_testbuild script in #9360 / #9363 (2021) — but that fix gave that specific internal process its own target directory; it wasn't a general fix, andcargo.targetDirtoday is still an all-or-nothing knob coveringcargo check, build-scripts, and proc-macro building together, with no way to isolate them from each other.What would help
cargo.targetDir.Happy to provide more logs/traces or test a patched build against our repro workspace if that's useful — the deadlock is easy to reproduce here (100% hit rate with both subsystems enabled).