Skip to content

perf(lsp): avoid blocking on diagnostic requests#23768

Draft
Sysix wants to merge 1 commit into
06-21-test_lsp_add_test_for_non-blocking_requestsfrom
06-24-perf_lsp_avoid_blocking_on_diagnostic_requests
Draft

perf(lsp): avoid blocking on diagnostic requests#23768
Sysix wants to merge 1 commit into
06-21-test_lsp_add_test_for_non-blocking_requestsfrom
06-24-perf_lsp_avoid_blocking_on_diagnostic_requests

Conversation

@Sysix

@Sysix Sysix commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Run pull diagnostics on a spawned blocking task so slow diagnostic work does not block unrelated LSP requests.
  • Make TextDocument own its URI, so diagnostic work can be moved into spawned tasks safely.
  • Store workspace workers behind Arc and return cloned worker handles from WorkerManager.
  • Update the request-lock test to verify code actions can respond before slower diagnostic requests finish.

Generated by codex 5.5 - reviewed by myself.

Sysix commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added the A-editor Area - Editor and Language Server label Jun 24, 2026
@Sysix
Sysix force-pushed the 06-24-perf_lsp_avoid_blocking_on_diagnostic_requests branch from 8bbea75 to de5ca03 Compare June 24, 2026 18:40
@github-actions github-actions Bot added A-linter Area - Linter A-cli Area - CLI A-formatter Area - Formatter labels Jun 24, 2026
@Sysix
Sysix force-pushed the 06-21-test_lsp_add_test_for_non-blocking_requests branch from 4780095 to ee1f6aa Compare June 24, 2026 19:07
@Sysix
Sysix force-pushed the 06-24-perf_lsp_avoid_blocking_on_diagnostic_requests branch from de5ca03 to cc897b0 Compare June 24, 2026 19:07
@Sysix
Sysix force-pushed the 06-24-perf_lsp_avoid_blocking_on_diagnostic_requests branch from cc897b0 to 111bdc5 Compare June 24, 2026 21:06
@evilsamaritan

evilsamaritan commented Jul 15, 2026

Copy link
Copy Markdown

I've been running this stack in IntelliJ on a large monorepo and it seems to help a lot — the wait before Fix-All (where it used to sit behind other runs for a few seconds) is basically gone in my testing, and stale runs get dropped as expected. Thanks for putting it together.

One small follow-up I noticed on top: with diagnostics running off-thread, a single save seems to kick off two full type-aware lints for the same (uri, version) — the on-save lint and the editor's Fix-All pull — at the same time. Joining identical (uri, version) requests into one shared future removed the duplicate run for me (~25% off Fix-All on a big file). I put it up as a draft stacked on your branches: #24560 — very happy to just hand it to you to fold in if that's easier.

Would you be up for pushing this stack forward — marking #23768 / #23797 ready and pinging a maintainer to get them reviewed and landed? They've been sitting for a few weeks, and they'd unblock a real, measurable win for the JetBrains users (oxc-project/oxc-intellij-plugin#366).

@Sysix

Sysix commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Thank you, @evilsamaritan! I will look into your improvements, sadly this PR is not ready yet. I did not have time to finish it up. + I found one big problem working on oxc-project/eslint-plugin-oxlint#741. Somehow a code action request will not receive a response.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the oxc_language_server LSP implementation to prevent slow/CPU-heavy diagnostics work from blocking unrelated requests by moving diagnostic execution into background blocking tasks and making the involved data/worker handles safe to move across tasks.

Changes:

  • Move diagnostic execution (didOpen/didChange/didSave and pull diagnostics request) into tokio::task::spawn_blocking to avoid blocking the async runtime.
  • Change TextDocument to own its Uri to allow safe 'static moves into spawned tasks.
  • Store WorkspaceWorker instances behind Arc in WorkerManager and return cloned handles instead of lock-tied guards.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
crates/oxc_language_server/src/worker.rs Adjust TextDocument references and file-system document retrieval to work with owned URIs.
crates/oxc_language_server/src/worker_manager.rs Store workers as Arc<WorkspaceWorker> and return cloned worker handles for task-safe usage.
crates/oxc_language_server/src/tests.rs Re-enable the request-lock test (removes #[ignore]).
crates/oxc_language_server/src/lib.rs Make TextDocument own Uri (removes lifetime parameter).
crates/oxc_language_server/src/file_system.rs Update get_document to return an owned TextDocument by taking Uri by value.
crates/oxc_language_server/src/backend.rs Spawn diagnostics on blocking tasks and adapt call sites to Arc workers / owned TextDocument.
apps/oxlint/src/lsp/tester.rs Update TextDocument::new call sites for owned Uri.
apps/oxlint/src/lsp/server_linter.rs Pass &document.uri where APIs now expect &Uri.
apps/oxfmt/src/lsp/server_formatter.rs Pass &document.uri where APIs now expect &Uri.

Comment on lines +589 to +597
// use `spawn_blocking` because this is a very CPU intensive task and we do not want to block the async runtime.
let diagnostics = tokio::task::spawn_blocking(move || {
handle.block_on(async move { worker.run_diagnostic_on_save(&document).await })
})
.await
// unwrap because we want to panic if the task panics,
// because it should not happen and we want to know about it.
.unwrap();

Comment on lines +643 to +651
// use `spawn_blocking` because this is a very CPU intensive task and we do not want to block the async runtime.
let diagnostics = tokio::task::spawn_blocking(move || {
handle.block_on(async move { worker.run_diagnostic_on_change(&document).await })
})
.await
// unwrap because we want to panic if the task panics,
// because it should not happen and we want to know about it.
.unwrap();

Comment on lines +714 to +722
// use `spawn_blocking` because this is a very CPU intensive task and we do not want to block the async runtime.
let diagnostics = tokio::task::spawn_blocking(move || {
handle.block_on(async move { worker.run_diagnostic(&document).await })
})
.await
// unwrap because we want to panic if the task panics,
// because it should not happen and we want to know about it.
.unwrap();

Comment on lines +880 to +887
// use `spawn_blocking` because this is a very CPU intensive task and we do not want to block the async runtime.
let diagnostics = tokio::task::spawn_blocking(move || {
handle.block_on(async move { worker.run_diagnostic(&document).await })
})
.await
// unwrap because we want to panic if the task panics,
// because it should not happen and we want to know about it.
.unwrap();
Comment on lines 901 to 905
let uri_diagnostics = diagnostics
.iter()
.filter(|(diag_uri, _)| diag_uri == uri)
.filter(|(diag_uri, _)| *diag_uri == uri)
.flat_map(|(_, diags)| diags.clone())
.collect::<Vec<_>>();
Comment on lines 907 to +908
let related_diagnostics =
diagnostics.into_iter().filter(|(diag_uri, _)| diag_uri != uri).collect::<Vec<_>>();
diagnostics.into_iter().filter(|(diag_uri, _)| *diag_uri != uri).collect::<Vec<_>>();
graphite-app Bot pushed a commit that referenced this pull request Jul 19, 2026
…ot opened (#24676)

Some clients can send code actions for documents, which are not opened and already analyzed with a lint process.
A good example is the IntelliJ plugin, which allows the "fix all" code action from the file explorer.

> This PR refactors the language-server → tool code-action API to pass a single request struct (including an “is document open” flag), enabling the oxlint LSP linter to avoid re-running lint for code-action requests when the document is already open (and presumably already linted/cached).

This is mostly needed for #23768
graphite-app Bot pushed a commit that referenced this pull request Jul 22, 2026
This PR comes from different use cases, and I hope I did not have a wrong understanding about spawning child.

### `std::spawn` in `tokio` runtime

Because the LSP is in a `tokio` runtime, we should avoid spawning threads with `std`, instead using `tokio::spawn`.
I try to work already with `tokio::spawn` in the `oxc_language_server` side with #23768
So this will spawn the complete diagnostic path into one separate process. We already need to wait for the `tsgolint` result, so spawning one thread there is suboptimal(?)

### `disable_directives_map` lock for the whole thread spawn

Asked Codex Terra 5.6 where a lock could be generated in #23768, and it found this long-lock.

> The problematic lock scope is tsgolint.rs line 403: the stdout-reader takes disable_directives_map.lock() before iterating the entire TsGoLintMessageStream. The diagnostic reader therefore serializes all type-aware runs, including code actions. This is especially visible after introducing concurrent diagnostics.

### tsgolint process still running

Some users did report that many tsgolint processes are still running, even if not typed. My first guess is these are zombie processes and can be safely killed.
I did not find the exact issue but oxc-project/oxc-vscode#264 should be good enough for a reference.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-editor Area - Editor and Language Server A-formatter Area - Formatter A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants