rust-lang / rust-lang/rust-analyzer

rust-analyzer permanently deadlocks (zero CPU, never recovers) when both `cargo.buildScripts.enable` and proc-macro support are active together on a large workspace with a custom `cargo.targetDir`

Open
#22,891 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug
Dominant language
Rust
Stars
16.9k
Forks
2.2k
Avg merge
1d 12h
Merged PRs (30d)
72

Description

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:
    {
      "rust-analyzer.cargo.targetDir": "target-analyzer",
      "rust-analyzer.cargo.buildScripts.enable": true, // default
      // proc-macro support left at default (enabled)
    }
    

Reproduction

  1. Open a large workspace (ours: ~658 crates) with rust-analyzer.cargo.targetDir set to a custom, non-default directory.
  2. Leave both cargo.buildScripts.enable and proc-macro support at their defaults (both enabled).
  3. Let the server index from a clean target-analyzer (i.e. delete it first to force a full rebuild).
  4. 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).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the stall with the linked ra-deadlock-repro project, a clean custom target directory, and both build scripts and proc-macro support enabled. Read GlobalState.task_pool in crates/rust-analyzer/src/global_state.rs and with_locked_io in proc-macro-api/src/process.rs, then compare the enabled and isolated configurations. Done means the workspace completes indexing without permanently blocking under both settings.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
build-system, devtools
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.