Skip to content

Concurrent LSP with cancelation #869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 16, 2025
Merged

Conversation

andrewbranch
Copy link
Member

This PR asyncifies the LSP serve and enables parallel execution of requests.

At the top level, one goroutine reads and parses messages from STDIN and puts them onto a queue to be handled. Another pulls from that queue and dispatches them to the project service, and puts the response objects onto another queue. A third goroutine pulls the response messages and writes them to STDOUT.

The dispatcher categorizes requests into ones that modify state and ones that only query the existing state. The former are handled synchronously within the dispatcher goroutine, while the latter are spun off into their own goroutines so they can execute in parallel.

Executing multiple requests in parallel agains the same program introduces two challenges:

  1. Type checkers are not concurrency safe, so each request needs to use a different one. Further, a request must always get the same checker if it asks the Program for one at two different times. For this, I’ve introduced a CheckerPool interface whose methods for getting a checker take a context.Context and return an available checker, along with a function to return it to the pool when done. The implementation in compiler used for tsc is just the logic that was already built into Program, while the implementation in project creates checkers dynamically and tracks which ones are in use by which requests.
  2. The text content of files the request handler inspects must be consistent over the duration of the request. Previously, line/character positions were converted to and from absolute positions in the server layer by looking at the text storage layer owned by the project system, which is mutable over time. With this PR, I’ve made all the LanguageService methods accept and return lsproto types (which are exclusively line/character) so all the conversions have to happen in that layer. Then, I removed LanguageService’s ability to access any mutable state (basically). When it asks for the line map of a file, its host attempts to pull the cached one from the text storage layer only if the versions match. Otherwise, the text storage has been updated by another request, and the line map for the old version is computed on demand from the SourceFile text, which is immutable.

@andrewbranch andrewbranch requested review from Copilot, jakebailey, ahejlsberg and gabritto and removed request for Copilot May 16, 2025 03:43
Copy link
Contributor

@Copilot Copilot AI left a comment

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 introduces asynchronous handling and cancellation support to the LSP server by refactoring service methods to be context-aware and by using a CheckerPool for concurrent type checking. Key changes include:

  • Adding a context argument to most LanguageService, API, and compiler methods.
  • Refactoring checker handling to use a CheckerPool for concurrent requests.
  • Updating several conversion and completion functions to work with the new asynchronous model.

Reviewed Changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/lsp/lsproto/jsonrpc.go Added new message types and helper functions for JSONRPC messages.
internal/ls/utilities.go Updated LSP range conversion to remove error handling.
internal/ls/languageservice.go Refactored service creation and file lookup to accept context.
internal/ls/hover.go Updated hover functionality to use context and improved code fencing.
internal/ls/host.go Removed outdated host methods and adjusted interface to use GetLineMap.
internal/ls/diagnostics.go Switched to context-based diagnostics and updated report types.
internal/ls/definition.go Updated definition lookup to be context-aware.
internal/ls/converters.go Refactored converters to use a simplified Script interface without error returns.
internal/ls/completions*.go Modified completion functions to support context and concurrent checkers.
internal/api/* All API methods now accept context; updated server request handling accordingly.
internal/compiler/program.go Replaced direct checker use with CheckerPool calls for concurrency control.
internal/compiler/emitHost.go Adjusted to use context when retrieving type checkers.
internal/compiler/checkerpool.go Added CheckerPool implementation for managing checkers concurrently.
internal/checker/* Minor updates to support new cancellation and context handling.

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

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

LGTM with a go mod tidy.

@jakebailey
Copy link
Member

I approved this but I think you said that there's some bug with duplicate program creation you want to address (so I'll happy look again after too)


wg.RunAndWait()

p.fileAssociations = make(map[*ast.SourceFile]*checker.Checker, len(p.program.files))
Copy link
Member

Choose a reason for hiding this comment

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

this is good for now but in theory the questions are asked only on open files so assign only for those ?

Copy link
Member

Choose a reason for hiding this comment

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

I meant in editor scenario not cli

Copy link
Member Author

Choose a reason for hiding this comment

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

This implementation is only used in the CLI. In the editor this map is created with no capacity hint.


textChanges := make([]ls.TextChange, len(changes))
for i, change := range changes {
if partialChange := change.TextDocumentContentChangePartial; partialChange != nil {
Copy link
Member

Choose a reason for hiding this comment

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

There is delayUpdateProjectGraph in this file- in strada we use to schedule the update (unless new change came in - then we would cancel and restart the timer) - with this change does this mean - send the refresh diagnostics notification - instead of updating as we want to update on next request instead ?

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't send the diagnostic refresh notification from here. The client will ask for new diagnostics after an update, so I don't think there's any need for us to schedule anything.

@andrewbranch andrewbranch enabled auto-merge May 16, 2025 20:18
@andrewbranch andrewbranch added this pull request to the merge queue May 16, 2025
Merged via the queue into microsoft:main with commit cab7a0b May 16, 2025
23 checks passed
@andrewbranch andrewbranch deleted the lsp-async branch May 16, 2025 20:54
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.

5 participants