fix: prevent LSP crash loop under sustained editing load - #473
Conversation
The server would become unresponsive after a few minutes of editing a moderately large vault: code actions then go-to-definition time out, the server goes silent, and Zed's shutdown times out and force-resets the connection, restarting the cycle. Root cause is async-runtime starvation: - Every keystroke (didChange/didOpen) recomputes diagnostics over all open buffers synchronously, which is O(open_files x references x referenceables). As open buffers accumulate, each pass eventually takes longer than the gap between keystrokes, so passes pile up faster than they finish. - This CPU-bound work runs inline on the Tokio worker threads while holding the vault lock, and the forked tower-lsp caps request concurrency at 4. The server's bounded message queue fills, the stdin reader can no longer be scheduled, and shutdown / $/cancelRequest are never read -> silence. Fixes: - Debounce diagnostics: coalesce bursts of edits so only the latest change in a quiet window triggers a diagnostics pass, bounding the load. - Offload synchronous vault work with tokio::task::block_in_place so the runtime keeps servicing stdin and control messages under load. - Raise the request concurrency limit from the default 4 to 256. Co-Authored-By: Felix Zeller <felixazeller@gmail.com>
| tokio::spawn(async move { | ||
| tokio::time::sleep(Duration::from_millis(DIAGNOSTICS_DEBOUNCE_MS)).await; | ||
| // A newer edit arrived during the debounce window; let its task run. | ||
| if backend.diag_gen.load(Ordering::SeqCst) != generation { | ||
| return; | ||
| } | ||
| if let Err(e) = backend.publish_diagnostics().await { | ||
| backend | ||
| .client | ||
| .log_message( | ||
| MessageType::ERROR, | ||
| format!("Failed calculating diagnostics {:?}", e), | ||
| ) | ||
| .await; | ||
| } | ||
| }); |
There was a problem hiding this comment.
🚩 Stale diagnostics possible when computation outlasts debounce window
If a diagnostics computation (generation N) takes longer than DIAGNOSTICS_DEBOUNCE_MS (300ms), a new edit can trigger generation N+1's task which starts and finishes first, publishing correct diagnostics. Then generation N finishes and overwrites them with stale results. There's no generation check inside publish_diagnostics (src/main.rs:193-243) after the computation completes. In practice, the staleness is short-lived (corrected on the next edit cycle) and the window is narrow, but for very large vaults where diagnostics routinely exceed 300ms, diagnostics could flicker. A mitigation would be to check diag_gen after bind_vault returns in the spawned task before publishing.
Was this helpful? React with 👍 or 👎 to provide feedback.
The server would become unresponsive after a few minutes of editing a moderately large vault: code actions then go-to-definition time out, the server goes silent, and Zed's shutdown times out and force-resets the connection, restarting the cycle.
Root cause is async-runtime starvation:
Fixes: