-
Notifications
You must be signed in to change notification settings - Fork 66
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
Perform read lock on LSP requests #1640
base: main
Are you sure you want to change the base?
Conversation
As what I understood from the discussion in the weekly dev meeting, the problem is the following: Maybe the solution in this PR is the only one, but I still want to share some other ideas (not sure it works well with Langium):
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some small things I noticed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I share @dhuebner's concerns. Is it right to push through an LSP request even though the text has changed in the meantime?
Reading the specification section Implementation Considerations, it looks like the best solution would be to return an error code ContentModified
when a change has happened.
return await serviceCall(language, document, params, cancelToken); | ||
const result = await lock.read(async () => { | ||
return await serviceCall(language, document, params, cancelToken); | ||
}, true); // Give this priority, since we already waited until the target state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost all LSP requests are now treated with priority? That seems too much to me: especially implicitly sent requests like DocumentHighlights should not block the build process. And what about Completion requests that are sent while typing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these are getting cancelled by the language client - they shouldn't be blocking the workspace in any way for longer than our timeout (5ms).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does that 5 ms timeout come from?
@spoenemann The link you've posted literally says this about changed content:
We do exactly this: Prevent the response from failing by blocking the workspace lock and returning the result. In the meantime, the client is free to cancel the pending request which we completely respect and unblock the workspace lock |
But that section also says:
This PR is about long running requests; when the state of a document is changed so that we can no longer finish the request, we should return an error. Note that this is not the same as canceling the request (which should be done only by the client). |
Could it help to use a utility function like this in LSP request processing? export async function interruptAndCheckDocument(document: LangiumDocument, token: CancellationToken): Promise<void> {
const previousState = document.state;
await interruptAndCheck(token);
if (document.state < previousState) {
throw new ResponseError(LSPErrorCodes.ContentModified, 'Document content has been modified.');
}
} |
The issue is: We cannot really know? For example, for the document highlight:
Aborting the initial operation for line 2 when receiving a document update is not the correct move here - the data for that highlighting is still valid and aborting the request might leave the user with incorrectly highlighted text. This is the case for most LSP requests. The internal state change the documentation is talking about (like project context switches) are much more fundamental changes than the "normal" document changes that make the result literally useless - however our results are probably still useful (except if the language client deems the change to be too large - in which case we get a cancellation anyway). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I see the problem with semantic highlighting (Semantic Tokens) (you said document highlighting, but that's a different service). The situation is different for other services. But it's true that it's the client that should decide whether to use a potentially outdated result or not.
We could apply this and then check how it affects the editing experience in larger projects. I'm particularly curious how it performs when completion is continuously triggered while typing.
Question: with this change, does it make any sense for LSP services to use Should we make it clear in the function documentation that it should be used only in the document building process and the associated services? |
const document = await documents.getOrCreateDocument(uri); | ||
return await serviceCall(language, document, params, cancelToken); | ||
// Give this priority, since we already waited until the target state | ||
const result = await lock.read(async () => await serviceCall(language, document, params, token), WorkspaceLockPriority.Immediate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Detail: the async
/ await
in the inline function is not necessary because simply returning the promise returned by serviceCall
has the same effect.
check(): Promise<void>; | ||
} | ||
|
||
export namespace CancellationToken { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand the advantage of the new interfaces and classed you defined here, but they are potentially very confusing because of the different ways to use them and because vscode-languageserver already has such definitions. We should try to prevent the confusion by
- documenting the new API thoroughly, especially what API is used in which context and what's the difference to vscode's definitions,
- writing a detailed explanation and migration guide in the CHANGELOG,
- checking whether this change is breaking in any way.
const now = performance.now(); | ||
if (time - now >= 10) { | ||
time = now; | ||
await delayNextTick(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this delay? If this is used only by LSP requests, shouldn't they proceed until finished or cancelled?
this.performNextOperation(); | ||
} | ||
})(); | ||
return deferred.promise; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can get rid of the Deferred
if you return
the result in the inline function and either return
the inline function's result or eliminate it by making the whole method async
.
import * as CstUtils from './cst-utils.js'; | ||
import * as GrammarUtils from './grammar-utils.js'; | ||
import * as RegExpUtils from './regexp-utils.js'; | ||
export { AstUtils, Cancellation, CstUtils, GrammarUtils, RegExpUtils }; | ||
export { AstUtils, CstUtils, GrammarUtils, RegExpUtils }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should keep the Cancellation
namespace to avoid a braking change. Can we mark it deprecated?
I've noticed that some long running requests (i.e. completion in large files, sometimes semantic highlighting) can break pretty heavily and lead to unresolved references if the specified document gets a new build triggered. This change adds a new type of read to the
WorkspaceLock
service that allows to instantly serve a read request - all other requests are queued up until this read request has finished (or has been aborted).