Skip to content

perf(lsp): run editor requests off the async runtime on a snapshot of the document - #931

Open
psteinroe wants to merge 1 commit into
perf/editor-walk-cachefrom
perf/editor-requests-off-runtime
Open

psteinroe wants to merge 1 commit into
perf/editor-walk-cachefrom
perf/editor-requests-off-runtime

Conversation

@psteinroe

@psteinroe psteinroe commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #930.

The handlers for hover, inlay hints, semantic tokens and code actions ran synchronously inside their async fns on the tokio workers, and the workspace held the read lock of all documents for the whole request. change_file needs the write lock, so a request that took seconds on a long file delayed applying the next keystroke by as much, and $/cancelRequest couldn't stop anything.

What changed

  • Workspace: documents are stored as Arc<Document>. on_hover, pull_inlay_hints, pull_semantic_tokens and pull_code_actions clone the Arc and release the lock right away. change_file uses Arc::make_mut, so it clones the document if a request still works on the previous version. Cloning is cheap: the statement caches (PgQueryStore, TreeSitterStore, AnnotationStore) are behind an Arc and shared, which is correct because they're keyed by the statement text.
  • The cached walk from perf(workspace): cache the walk of a document for hover, inlay hints and semantic tokens #930 checks every 64 statements whether the document's version changed, and gives up if it did. Hints and tokens then return nothing, and hover's references are empty.
  • LSP: LSPServer::run_document_request runs these handlers with spawn_blocking, like the custom workspace methods already are. If the document version changed while the request ran, it answers with ContentModified (-32801), which tells the client to ask again instead of showing outdated results.

Timings

From server.perf_tests.rs (release build): an edit while a cold full semantic-token request runs on the same document.

statements change_file during the request request
1,250 3 ms stops after 11 ms
5,000 12 ms stops after 22 ms
20,000 34 ms stops after 39 ms

Before, change_file waited for the read lock until the request finished: up to the full request time from #930's table (2.5 s for a cold full request on 20,000 statements).

Tests

test_editor_requests_use_document_snapshots: a snapshot taken before an edit keeps its content, a walk of it is abandoned, and a walk of the current document isn't. The LSP and workspace suites pass.

Not covered

  • Cancellation is only through the version check: $/cancelRequest without an edit still lets the request finish on the blocking pool.
  • Diagnostics, completions and formatting still hold the documents lock while they run.
  • Expect conflicts with the per-document statement caches being changed on main (Document's fields).

Follow-ups: Cancellation without an edit, and diagnostics, completions and formatting without the documents lock, are in #954.

… the document

Hover, inlay hints, semantic tokens and code actions ran on the tokio
workers and held the read lock of the documents for the whole walk, so
the next edit waited for them. The documents are now shared as Arc
snapshots: requests clone one and release the lock, and an edit clones
the document if a request still uses it. The LSP runs these requests on
the blocking pool. A walk checks every 64 statements whether the
document changed and stops if it did; the LSP then answers with
ContentModified so that the client asks again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant