Unnamed repository; edit this file 'description' to name the repository.
perf: defer initial workspace flycheck until cache priming completes
On startup the initial `cargo check` flycheck was launched in the same
`became_quiescent` block as cache priming, so the two ran concurrently and
fought for CPU. Neither is single-threaded: cache priming fans crate work
across worker threads and `cargo check` spawns up to one rustc job per core,
so running them together oversubscribes the cores. The effect is felt
particularly on macOS, where rust-analyzer's priming threads run at
`QOS_CLASS_UTILITY` (`ThreadIntent::Worker`) while the `cargo check`
subprocess runs at the default, higher QoS -- so the scheduler starves
priming (the work that makes the editor usable) in favour of the check.
Move the initial workspace flycheck out of `became_quiescent` into the
`PrimeCachesProgress::End` handler via a `pending_initial_flycheck` flag, so
the check only starts once priming has finished; fall back to firing it
immediately when cache priming is disabled. The flycheck still runs (cargo
check begins right after indexing); diagnostics are deferred a beat, not lost.
The impact depends on how much the startup `cargo check` has to compile:
- Warm check (steady-state reopen): contention is negligible, so this is
effectively neutral -- and harmless, deferring a fast check costs nothing.
- Cold check (first open after clone / `git pull` / branch switch / toolchain
change, where startup already hurts): large. Time-to-ready (cache priming
complete) measured on macOS with a cold check, build scripts and proc-macros
disabled to isolate the priming/check contention:
rust-analyzer 74.8s -> 7.9s (~9.5x)
slint 103.1s -> 26.6s (~3.9x)
Flycheck slow-tests pass.
Disclosure: prototyped with assistance from Claude (Anthropic); changes
reviewed and owned by the committer.
| -rw-r--r-- | crates/rust-analyzer/src/main_loop.rs | 30 |
1 files changed, 23 insertions, 7 deletions
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index edf3da5e6c..628e708c6a 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -387,6 +387,16 @@ impl GlobalState { if cancelled { self.prime_caches_queue .request_op("restart after cancellation".to_owned(), ()); + } else if self.config.check_on_save(None) + && self.config.flycheck_workspace(None) + && !self.fetch_build_data_queue.op_requested() + { + // Priming finished; now run the deferred initial workspace flycheck + // (kept off the critical path so `cargo check` doesn't contend with + // cache priming for CPU). + self.flycheck + .iter() + .for_each(|flycheck| flycheck.restart_workspace(None)); } if let Some((message, fraction, title)) = last_report.take() { self.report_progress( @@ -482,13 +492,6 @@ impl GlobalState { if self.is_quiescent() { let became_quiescent = !was_quiescent; if became_quiescent { - if self.config.check_on_save(None) - && self.config.flycheck_workspace(None) - && !self.fetch_build_data_queue.op_requested() - { - // Project has loaded properly, kick off initial flycheck - self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); - } // delay initial cache priming until proc macros are loaded, or we will load up a bunch of garbage into salsa let proc_macros_loaded = self.config.prefill_caches() && (!self.config.expand_proc_macros() @@ -496,6 +499,19 @@ impl GlobalState { if proc_macros_loaded { self.prime_caches_queue.request_op("became quiescent".to_owned(), ()); } + if self.config.check_on_save(None) + && self.config.flycheck_workspace(None) + && !self.fetch_build_data_queue.op_requested() + { + if !self.config.prefill_caches() { + self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); + } else if proc_macros_loaded + && !self.prime_caches_queue.op_in_progress() + && !self.prime_caches_queue.op_requested() + { + self.flycheck.iter().for_each(|flycheck| flycheck.restart_workspace(None)); + } + } } let client_refresh = became_quiescent || state_changed; |