Skip to content
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

Add support for LSP semantic token refresh. #61003

Merged
merged 19 commits into from
Apr 28, 2022

Conversation

CyrusNajmabadi
Copy link
Member

This PR gets rid of the IsFinalized bit on semantic tokens and replaces it with the new server-initialized notification that information may be changed and semantic tokens should be 'refreshed'.

The impl here works similarly to what roslyn already does in semantic tagging. Namely, we attempt to service the tagging request using a 'frozen partial' view of hte world (for fast latency). Then, we also issue a request to our OOP server to ensure it is fully up to date with teh compilation for the corresponding project. Once that compilation is ready on the OOP side, we notify teh tagger that it should retag.

Things are slightly more complicated in the LSP side as we don't have a dedicated party asking for tags and a dedicated party notifying us of changes. Instead, it's the same party. e.g. we have to use the presence of a request to get semantic tokens as our way of knowing to kick off the compilation computation work in the OOP process. This comes with a potential downside of running into the following infinite loop:

  1. client calls into roslyn LSP to get semantic tokens.
  2. roslyn LSP computes fast results and returns them, while also kicking off work to get compilation.
  3. after compilation is produced, we issue a semantic tokens refresh notification. this causes:
  4. client calls into roslyn LSP to get semantic tokens (e.g. step1, and this cycles endlessly).

To avoid this, we insert the following fixed point break in the above code:

  1. after compilation is produced, we compute and attempt to store the checksum of hte solution we computed against. If this checksum is the same as the last time we did this, we stop processing. If it is different, then we issue a semantic tokens refresh notification.

This means that when the client calls in in response to the semantic tokens refresh, we will attempt to make the compilation. However, we will see that we produced it for the same solution checksum, and decide to not notify the client to refresh a second time, ending hte loop.

/// Cancellation tokens controlling background computation of the compilation.
/// </summary>
private readonly ReferenceCountedDisposable<CancellationSeries> _cancellationSeries = new(new CancellationSeries());
private readonly CompilationAvailableEventSource _eventSource;
Copy link
Member Author

Choose a reason for hiding this comment

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

common code extracted into this new type.

@@ -59,6 +60,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler

public readonly IGlobalOptionService GlobalOptions;

public readonly LspWorkspaceManager LspWorkspaceManager;
public readonly ILanguageServerNotificationManager NotificationManager;
public readonly CancellationToken QueueCancellationToken;
Copy link
Member Author

@CyrusNajmabadi CyrusNajmabadi Apr 27, 2022

Choose a reason for hiding this comment

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

ew.

#61002 tracks a nicer way to export/discover LSP services so we don't have to pass all this around in this RequestCOntext.

{
if (isFullyLoaded && !_projectIdToCompilation.ContainsKey(project.Id))
if (_projectIdLastComputedSolutionChecksum.TryGetValue(project.Id, out var lastComputedChecksum) &&
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs to be moved up - this looks like we compute the compilation then afterwards we compare to the last result. But that means when the checksum is the same we compute the compilation anyway, we just don't store it or send a notification

{
lock (_gate)
{
if (_supportsRefresh == null)
Copy link
Member

Choose a reason for hiding this comment

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

I apologize for this mess :(
Evidence for #61002


public override bool MutatesSolutionState => false;
public override bool RequiresLSPSolution => true;

private readonly object _gate = new();
private readonly Dictionary<ProjectId, CompilationAvailableEventSource> _projectIdToEventSource = new();
Copy link
Member

Choose a reason for hiding this comment

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

could this be a pair of last checksum and compilationavailable event source in one map?

@CyrusNajmabadi
Copy link
Member Author

@allisonchou @dibarbet @ryanbrandenburg @NTaylorMullen Please try this out at your soonest convenience to see how well this works (both in terms of correctness and in terms of performance).

Note: that you should expect a potential long delay before a semantic change happening, and seeing the refresh for another file. Right now there is a hardcoded 3s delay on us producing compilations in OOP (due to cost) that can be very observable if people are waiting on it. However, the hope is that in practice this delay is good for keeping utilization low, while ensuring that in the BG as people are doing other things that we're bringing things up to day.

@CyrusNajmabadi
Copy link
Member Author

Note: #60993 was merged into this PR.

Copy link
Contributor

@allisonchou allisonchou left a comment

Choose a reason for hiding this comment

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

LGTM! I manually ran a VS instance with these changes and colorization overall looks good. I noticed minor lag when typing but that's also present without the changes.

In terms of what changes with Cyrus' modifications, I didn't notice a difference even in larger files. However, it's probable that this bug manifests itself occasionally (+ my testing likely wasn't comprehensive), so I think it'd still be good to take this through M2 if possible.

{
_globalOptions = globalOptions;
_asyncListener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Classification);
Copy link
Contributor

Choose a reason for hiding this comment

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

question: In the prior implementation I passed in FeatureAttribute.Workspace here, was that a bug/what implications does passing in FeatureAttribute.Classification vs FeatureAttribute.Workspace have?

Copy link
Member Author

Choose a reason for hiding this comment

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

Great question. The general idea of hte "Async Listener" system is to allow features to let an external system know "i'm doing async work, so if you need information from me, you should wait until that work is done". This system exists only for test purposes so that tests can simulate what the user is doing, and then check for the expected results as soon as possible (e.g. instead of polling and then timing out once a certain amount of time has gone past).

The 'feature attribute' is jsut a broad way of saying what sort of functionality the async work is being done for. that way the test can just way for that general area to be done, as opposed to waiting for async work anywhere (as there may be lots of async work happening that the test doesn't care about at all).

So here, we want to use 'Classification' so that if we have a test of LSP classification, and it only wanted to run once there was no bg-classification work, it would have a way to do it.

In general, using 'Workspace' is not a great idea as it's a very 'catch all bucket' that can mean the test has to wait a long time for everything in the workspace to settle down.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! That was a very helpful explanation

// to completion) prior to the next request running and completing. In practice this should not happen
// as cancellation is checked fairly regularly. However, if it does, check and do not bother to issue a
// refresh in this case.
if (ChecksumIsUnchanged_NoLock(project, projectChecksum))
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe just making this a local function and calling it something like IsChecksumUnchanged would be the most concise? I feel like the naming of ChecksumIsUnchanged_NoLock is confusing without context. I'm guessing it just means that the method's implementation isn't protected by a lock, but without context and just looking at the name, I feel like it could also be interpreted as that the method shouldn't be called under a lock

// for isFinalized. It may not be completely accurate but this is only a a temporary fix until
// workspace/semanticTokens/refresh is implemented.
lock (_lock)
// Determine the checksum for this project cone. Note: this should be fast in practice because this is
Copy link
Contributor

Choose a reason for hiding this comment

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

question: what's a project "cone"? Is it just another way to say project version?

Copy link
Member Author

Choose a reason for hiding this comment

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

i use "cone" to mean "this project and the projects it depends on". "cones" are useful for features that need semantics, but couldn't be affected by any projects outside of htat set. For example if i have projects A<-B<-C then to get classification in 'B', i only need the information in 'A' and 'B' as the information in C cannot affect it. So when we're sync'ing to oop, we actually say "here's the checksum for A+B (but not C), so just sync that information over".

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Apr 28, 2022

In terms of what changes with Cyrus' modifications, I didn't notice a difference even in larger files. However, it's probable that this bug manifests itself occasionally (+ my testing likely wasn't comprehensive),

@allisonchou It would be good to know if you're seeing a reasonable set of LSP messages happening. E.g. that you do see the eventual call to 'refresh' and that that causes a callback that does not cause another refresh, etc. etc.

so I think it'd still be good to take this through M2 if possible.

If we're not really noticing any difference, my strong preference would be to take this in P2 instead of through P1. There's no need to take on risk here IMO if we don't have a substantive problem and this PR isn't making massive improvements.

@allisonchou
Copy link
Contributor

@CyrusNajmabadi I used the VS telemetry monitor and it appears that we're getting the refresh notifications properly. I had 5 projects open and saw exactly 5 refresh notifications after adding a file. Didn't notice any cycling:

image

As for delaying this PR til preview 2, if you think the risk factor is high then that would be OK with me. @ryanbrandenburg what do you think? My only concern is that my manual testing could have missed something and that this PR would solve any edge cases.

@CyrusNajmabadi
Copy link
Member Author

@CyrusNajmabadi I used the VS telemetry monitor and it appears that we're getting the refresh notifications properly. I had 5 projects open and saw exactly 5 refresh notifications after adding a file. Didn't notice any cycling:

Awesome. you rock!

@CyrusNajmabadi
Copy link
Member Author

As for delaying this PR til preview 2, if you think the risk factor is high then that would be OK with me. @ryanbrandenburg what do you think? My only concern is that my manual testing could have missed something and that this PR would solve any edge cases.

My main concern is that if we don't really see a problem with our current code, and we're not seeing much of a change, then i think it's a bit hard to justify the risk. We have plenty of runway in front of us i think (though @jinujoseph to weigh in), so we can put this in P2 and get lots of time to preview/dogfood/etc.

@jinujoseph
Copy link
Contributor

I agree , lets not rush the churn in preview1 ...we can take this for preview2 ( feel free to retarget to main )

@CyrusNajmabadi CyrusNajmabadi changed the base branch from release/dev17.3 to main April 28, 2022 19:49
@CyrusNajmabadi
Copy link
Member Author

Retargetting to main.

@CyrusNajmabadi CyrusNajmabadi merged commit f4cc5c0 into dotnet:main Apr 28, 2022
@ghost ghost added this to the Next milestone Apr 28, 2022
@CyrusNajmabadi CyrusNajmabadi deleted the lspRefresh branch April 29, 2022 21:11
@Cosifne Cosifne modified the milestones: Next, 17.3 P2 May 31, 2022
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.

5 participants