Skip to content

Semantic highlighting flickers #576

Closed

Description

@matklad

I've tried new semantic highlighting API in rust-analyzer (which was implemented by Jeremy Kolb (@kjeremy)). I am super excited to see this feature finally in the LSP!

However, test drive shows this flickering:

flicker

I am not exactly sure, but I think the reason here is content modifies errors:

[Trace - 4:17:49 PM] Received response 'textDocument/semanticTokens - (437)' in 15ms. Request failed: content modified (-32801).

The editor asks for highlights, and then continues with sending modifications. Server than cancels the in-flight highlighting request. This causes the provider to return undefined, and that apparently clears the previous highlights. Because user continues typing, the highlighting is not re-applied until the user stops and the server is able to catch up.

It seems like the better behavior in this case is to just shift previous highlighting ranges, instead of clearing them. Not sure if this something to be fixed in vscode-languageserver-node, vscode itself, or the API for SemanticTokensProvider.

Activity

  1. rcjsuen commented on Feb 26, 2020

    @rcjsuen
    Contributor

    It seems like the better behavior in this case is to just shift previous highlighting ranges, instead of clearing them. Not sure if this something to be fixed in vscode-languageserver-node, vscode itself, or the API for SemanticTokensProvider.

    Monaco itself seems to be okay based on some random testing I just did in my editor so it may be something at the LSP level?

  2. matklad commented on Feb 26, 2020

    @matklad
    ContributorAuthor

    Found another related with content-modified cancellation: if highlighting is in progress and then some outside change occures (for example, file on disk changes), it gets canceled and is not retried. This is diffrenet from the case where the modification is triggered by the user, because in that case we retry and get the correct highlighting.

    For rust-analyzer, the first case happens on startup: when we ingest project files from disk, we trigger internal modifications and cancel highlighting request. This specific case can and should be handled on our side by delaying requests until the whole project is loaded, but the general issue of missed highlighting due to unrelated changes still exists.

  3. dbaeumer commented on Feb 27, 2020

    @dbaeumer
    Member

    The VS Code editor actually shifts the tokens automatically.

    Alexandru Dima (@alexdima) what is the suggested way to inform the editor about a rejected sematnic token request. Should I simply rethrow or do you have a special error code that I can use to inform you to retrigger the request later (e.g. the busy case we discussed).

  4. alexdima commented on Feb 27, 2020

    @alexdima
    Member

    the current implementation

    Returning undefined is the same as returning an empty array and it will make the editor clear all semantic tokens, so that is what leads to the flickering. Currently, throwing an error with the message busy aka throw new Error('busy');/return Promise.resolve(new Error('busy')) will be swallowed by the editor (not logged to error telemetry) and the semantic tokens will not be dropped and a new request will be made. This is what our built-in TS semantic tokenization does when it cannot map back offsets to (ln;col) coordinates...

    about the 'busy'

    Dirk Bäumer (@dbaeumer) I've talked with Johannes Rieken (@jrieken) and in the vscode API we don't have a clear way to specify this "busy" situation. You mentioned you also think LSP should have a "busy" concept, not sure what would be the way to express that in LSP? How should that be better expressed in the vscode API? I am not very happy with an error with a certain message...

  5. matklad commented on Feb 27, 2020

    @matklad
    ContributorAuthor

    Confirmed that throwing busy fixes the fickering: rust-lang/rust-analyzer#3339.

    Also confirmed that it doesn't fix the second problem with the absence of retry behavior. Ie, the client sends one semanticTokens request, it fails with busy, and the file remains not highlighted until I type something, thus triggering the request again.

  6. alexdima commented on Feb 27, 2020

    @alexdima
    Member

    Alex Kladov (@matklad) You're correct. After an error, a new request is issued only if text changes occurred in the meantime -- https://github.com/microsoft/vscode/blob/8758dc9dddf95f358eb93c969353e5cf245ed1e4/src/vs/editor/common/services/modelServiceImpl.ts#L796-L810

    But why do you throw busy if there are no text changes in the meantime? In other words, what would you expect to happen? Should the editor request the semantic tokens again even without more text changes? At what rate should that be? i.e. Should that be limited somehow? ... and why?

  7. matklad commented on Feb 27, 2020

    @matklad
    ContributorAuthor

    Alexandru Dima (@alexdima) the fundamental issue here is that highlighting of a file might be invalidated by changes to other files. For example, while the server computes the highlighting, the client might change git branches in the terminal (which modify some unrelated files), and the server gives up highlighting because it is now obsolete. Another hypothetical case is "I can't highlight this file right now, I am waiting for this library to be downloaded from the Internet".

    In the specific case of rust-analyzer, this behavior is an artifact of an implementation. We have a single data structure, global state, which is protected by rw lock. Highlighting and other features work under r lock, any modification needs a w lock. When a new change arrives, we cancel all in-process r operations, which causes all pending requests to fail with "content-modified" error. One motivation for this is that, if the state has changed, the results of requests could be obsoleted anyway.

    What is the correct retry behavior is an interesting question. The answer is clear in the push model: if the client registers an interest in a file, server responds with highlights as soon as quiescent state is reached (ie, there are no pending in-flight changes).

    For pull model, I am not sure what's the right behavior. "retry indefinitely without delays", paradoxically, seems the theoretically right answer at least for rust-analyzer's architecture? Requests only get cancelled if modification arrives afterwards; if there are some pending modifications, and a new requests arrives, it's just scheduled after the modifications.

  8. jrieken commented on Feb 27, 2020

    @jrieken

    For pull model, I am not sure what's the right behavior. "retry indefinitely without delays", paradoxically, seems the theoretically right answer at least for rust-analyzer's architecture?

    For the pull model we usually solve this by allowing providers to have an event that signals a change. The CodeLensProvider is a sample for that

  9. Gama11 commented on Feb 27, 2020

    @Gama11
    Contributor

    That event still isn't available in the LSP though / requires nasty hacks. :( microsoft/language-server-protocol#192

  10. matklad commented on Feb 27, 2020

    @matklad
    ContributorAuthor

    Hit a very real case of stale highlighting right now:

    image

  11. dbaeumer commented on Feb 28, 2020

    @dbaeumer
    Member

    Johannes Rieken (@jrieken) we discussed that a while back ago and I somehow thought that having a busy indication to signal that a server is currently not able to answer the questions is a good idea. IMO this is even unrelated to having an event to invalidate persented data and ask the client to refetch the data.

    Alexandru Dima (@alexdima) a model that would make sense to me is as follows:

    • user changes text
    • client fetches semantic tokens
    • promise is resolved with busy. This indicates the data is not processed and the old semantic tokens are still valid
    • client issues another request either after the user has typed or after a timeout
    • if we want we can define that a server / extension can only report busy n times before it is ignored.

    I would indicate Busy through a special Error class (comparable to FileSystemError).

    I can of course simulated this with a change event. However this has the disadvantage that the exension has no knowledge about part visibility and therefore might trigger change events although no data is presented.

  12. jrieken commented on Mar 2, 2020

    @jrieken

    Johannes Rieken (@jrieken) we discussed that a while back ago and I somehow thought that having a busy indication to signal that a server is currently not able to answer the questions is a good idea.

    Sure, I am still in agreement with that but we decided to accept "any" error and not clear semantic highlights in its presence. However, I don't see how the error solves the case of two editors where typing in A causes changes in B - for that I can only image the event.

  13. alexdima commented on Mar 2, 2020

    @alexdima
    Member

    With microsoft/vscode@c2a604a , I've added support for a change event from the provider.

  14. dbaeumer commented on Mar 3, 2020

    @dbaeumer
    Member

    Regarding the busy indication: I discussed this with Alexandru Dima (@alexdima) again and noticed that naming it busy is pretty misleading. A better name would very likely be server side cancelation of an operation.

    Johannes Rieken (@jrieken): what is your take on this. However it still doesn't solve how we would communicate this (e.g. using an exception or a special return object).

  15. matklad commented on Mar 3, 2020

    @matklad
    ContributorAuthor

    One possible way out of this design question is to hide the retry loop inside the vscode-langaugeserver-node. Ie, the VS Code provider in this repository can retry the LSP query itself instead of propagating busy outwards. if VS Code itself correctly cancels uncompleted provider calls after file modifications, this should probably work out.

  16. 15 remaining items

  17. alexdima commented on Aug 27, 2020

    @alexdima
    Member

    Not fully finished -- microsoft/vscode#93686

  18. dbaeumer commented on Sep 1, 2020

    @dbaeumer
    Member

    Alexandru Dima (@alexdima) can we make progress on this in September so that I can do that in LSP.

  19. alexdima commented on Sep 2, 2020

    @alexdima
    Member

    Johannes Rieken (@jrieken) To be honest, this is more on your plate, since such an error would be relevant to any of the other providers. Would you be willing to drive this since it has so much more impact than the semantic tokens provider?

  20. dbaeumer commented on Oct 8, 2020

    @dbaeumer
    Member

    Johannes Rieken (@jrieken) any additional insights here?

  21. jrieken commented on Oct 8, 2020

    @jrieken

    No update, work will happen here: microsoft/vscode#93686

  22. dbaeumer commented on Nov 18, 2020

    @dbaeumer
    Member

    Moving to 3.17 since no progress in VS Code so far.

  23. kjeremy commented on Feb 15, 2021

    @kjeremy
    Contributor

    Dirk Bäumer (@dbaeumer) does this fix the issue? dae62de

  24. dbaeumer commented on Feb 16, 2021

    @dbaeumer
    Member

    Jeremy Kolb (@kjeremy) yes it should have improved it. I still need to convert the code to the new CancelError from VS Code API.

  25. dbaeumer commented on Feb 16, 2021

    @dbaeumer
    Member

    Throwing new CancellationError() now.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions