Skip to content

Fix LSP crash loop: timing logs + panic crash reporting + handler isolation + reproduced panics - #1

Open
Feel-ix-343 wants to merge 3 commits into
mainfrom
devin/1780375161-fix-async-executor-starvation
Open

Fix LSP crash loop: timing logs + panic crash reporting + handler isolation + reproduced panics#1
Feel-ix-343 wants to merge 3 commits into
mainfrom
devin/1780375161-fix-async-executor-starvation

Conversation

@Feel-ix-343

@Feel-ix-343 Feel-ix-343 commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Three layers addressing the Zed crash loop and making future crashes/slowness debuggable live:

1. Per-request timing in the logs (INFO)

A shared timed(label, fut) helper times every user-facing request and logs "<method> took <ms>ms (ok|error)" at INFO, so slow requests are visible in the editor's LSP log:

textDocument/definition took 0.93ms (ok)
textDocument/references took 0.18ms (ok)
textDocument/completion took 3.83ms (ok)
textDocument/codeAction took 0.77ms (ok)
update_vault took 1.90ms
Diagnostics took 1ms

Covers definition, references, completion, codeAction, hover, documentSymbol, workspace/symbol, rename, codeLens, semanticTokens, inlayHint, plus the indexing hot paths (update_vault, diagnostics, vault construction). The pre-existing ad-hoc completion/semantic-token timers are folded into the same helper.

2. Crash reporting (so panics are debuggable live)

A panic in an LSP handler does not exit the process (no panic = "abort"): it's caught at the tokio task boundary, but it kills the request-dispatch path, so the server stops reading stdin and goes silent → editor times out shutdown → force-restart (the crash loop), with no backtrace anywhere because the process never exits.

  • install_panic_hook() (first in main): logs every panic — message, file:line, full backtrace — to stderr (editors capture this) and to markdown-oxide-panic.log.
  • catch_panic(context, f) wraps the synchronous vault work in bind_vault/bind_vault_mut so a panic becomes a failed request (InternalError) instead of a dead server, and is still fully reported by the hook.

3. Panics reproduced over LSP and fixed

site trigger fix
did_change empty contentChangesremove(0) use last (full-document) change; ignore empty list
MDFile::new stemless URI (file:///) → file_stem().expect() file_stem().and_then(to_str).unwrap_or_default()
daily-note (commands.rs/link_completer.rs) invalid dailynote format → date.format(fmt).to_string() panics format via write!(.., "{}", date.format(fmt)).ok()?
tag_completer/link_completer/unindexed_block_completer completion at col 0 / empty line → usize underflow checked_sub/saturating_sub

Validation

  • Timing: verified all handler + indexing timing lines emit at INFO (window/logMessage) over a live LSP session.
  • Crash isolation: responsiveness after each former crash trigger 0/3 → 3/3 (debug + release).
  • cargo test 74/74; no new clippy/build warnings (the 3 in symbol.rs/commands.rs are pre-existing).

Link to Devin session: https://app.devin.ai/sessions/5d37c33bc30944d58b1a376f829ce8d1
Requested by: @Feel-ix-343

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>
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@exa-labs exa-labs deleted a comment from devin-ai-integration Bot Jun 17, 2026
…nics

A panic in any LSP handler does not exit the process (no panic=abort): it is
caught at the task boundary, but it kills the request-dispatch loop, so the
server stops reading stdin and goes silent. The editor then sees a hung server,
times out shutdown, and force-restarts it — the reported crash loop — with no
backtrace anywhere because the process never actually exits.

Crash reporting (debug live):
- install a global panic hook that logs every panic (message, location, full
  backtrace) to stderr (editors capture this in their LSP log) and to
  markdown-oxide-panic.log.
- wrap the synchronous vault work in bind_vault/bind_vault_mut with catch_unwind
  so a panic in a query handler becomes a failed request instead of a silent
  server death.

Fix the panics reproduced by driving the real server over LSP:
- main.rs: empty didChange contentChanges -> remove(0) panic; use the last
  (full-document) change and ignore an empty list.
- vault/mod.rs: file_stem().expect() on a stemless path -> handle gracefully.
- commands.rs / link_completer.rs: date.format(dailynote).to_string() panics on
  an invalid format specifier; format via write! and skip on error.
- tag_completer / link_completer / unindexed_block_completer: usize underflows
  (character-1, len()-1) -> checked_sub/saturating_sub.

Co-Authored-By: Felix Zeller <felixazeller@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title Fix LSP crash loop under sustained editing load Fix LSP crash loop: panic crash reporting + handler isolation + reproduced panics Jun 19, 2026
Time every user-facing request (definition, references, completion,
codeAction, hover, documentSymbol, workspace/symbol, rename, codeLens,
semanticTokens, inlayHint) via a shared timed() helper that logs
'<method> took <ms>ms (ok|error)' at INFO, so slow requests are visible
in the editor's LSP log. Also surface the indexing hot paths
(update_vault, diagnostics, vault construction) at INFO with their
durations. Folds the pre-existing ad-hoc completion/semantic-token
timers into the same helper.

Co-Authored-By: Felix Zeller <felixazeller@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title Fix LSP crash loop: panic crash reporting + handler isolation + reproduced panics Fix LSP crash loop: timing logs + panic crash reporting + handler isolation + reproduced panics Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant