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

Reduce overhead due to lsp semantic token refresh requests. #69690

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
Expand Down Expand Up @@ -44,6 +45,7 @@ internal class SemanticTokensRefreshQueue :

private readonly LspWorkspaceManager _lspWorkspaceManager;
private readonly IClientLanguageServerManager _notificationManager;
private readonly ICapabilitiesProvider _capabilitiesProvider;

private readonly IAsynchronousOperationListener _asyncListener;
private readonly CancellationTokenSource _disposalTokenSource;
Expand All @@ -61,7 +63,8 @@ public SemanticTokensRefreshQueue(
IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
LspWorkspaceRegistrationService lspWorkspaceRegistrationService,
LspWorkspaceManager lspWorkspaceManager,
IClientLanguageServerManager notificationManager)
IClientLanguageServerManager notificationManager,
ICapabilitiesProvider capabilitiesProvider)
{
_asyncListener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Classification);

Expand All @@ -70,11 +73,14 @@ public SemanticTokensRefreshQueue(

_lspWorkspaceManager = lspWorkspaceManager;
_notificationManager = notificationManager;
_capabilitiesProvider = capabilitiesProvider;
}

public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken _)
{
if (_semanticTokenRefreshQueue is null && clientCapabilities.Workspace?.SemanticTokens?.RefreshSupport is true)
if (_semanticTokenRefreshQueue is null
&& clientCapabilities.Workspace?.SemanticTokens?.RefreshSupport is true
&& _capabilitiesProvider.GetCapabilities(clientCapabilities).SemanticTokensOptions is not null)
{
// Only send a refresh notification to the client every 2s (if needed) in order to avoid
// sending too many notifications at once. This ensures we batch up workspace notifications,
Expand Down Expand Up @@ -141,21 +147,52 @@ private static ValueTask FilterLspTrackedDocumentsAsync(

private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
{
if (e.DocumentId is not null && e.Kind is WorkspaceChangeKind.DocumentChanged)
{
var document = e.NewSolution.GetRequiredDocument(e.DocumentId);
var documentUri = document.GetURI();
Uri? documentUri = null;

if (e.DocumentId is not null)
{
// We enqueue the URI since there's a chance the client is already tracking the
// document, in which case we don't need to send a refresh notification.
// We perform the actual check when processing the batch to ensure we have the
// most up-to-date list of tracked documents.
EnqueueSemanticTokenRefreshNotification(documentUri);
}
else
{
EnqueueSemanticTokenRefreshNotification(documentUri: null);
if (e.Kind is WorkspaceChangeKind.DocumentChanged)
{
var document = e.NewSolution.GetRequiredDocument(e.DocumentId);
documentUri = document.GetURI();
}
else if (e.Kind is WorkspaceChangeKind.AdditionalDocumentChanged)
{
var document = e.NewSolution.GetRequiredAdditionalDocument(e.DocumentId);

// Changes to files with certain extensions (eg: razor) shouldn't trigger semantic a token refresh
if (DisallowsAdditionalDocumentChangedRefreshes(document.FilePath))
return;
}
else if (e.Kind is WorkspaceChangeKind.DocumentReloaded)
{
var newDocument = e.NewSolution.GetRequiredDocument(e.DocumentId);
var oldDocument = e.OldSolution.GetDocument(e.DocumentId);

// If the document's attributes haven't changed, then use the document's URI for
// the call to EnqueueSemanticTokenRefreshNotification which will enable the
// tracking check before sending the WorkspaceSemanticTokensRefreshName message.
if (oldDocument?.State.Attributes is IChecksummedObject oldChecksumObject
&& newDocument.State.Attributes is IChecksummedObject newChecksumObject
&& oldChecksumObject.Checksum == newChecksumObject.Checksum)
{
documentUri = newDocument.GetURI();
}
}
}

EnqueueSemanticTokenRefreshNotification(documentUri);
}

// Duplicated from Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LoadedProject.TreatAsIsDynamicFile
private static bool DisallowsAdditionalDocumentChangedRefreshes(string? filePath)
{
var extension = Path.GetExtension(filePath);
return extension is ".cshtml" or ".razor";
Copy link
Member

Choose a reason for hiding this comment

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

some context for this -
It seems like in general additional files changing could affect semantic tokens (for example if those changes are inputs to a source generator). However, Razor semantic tokens only uses the design time file so we actually don't care of the Razor file changes (they'll already tell us when the design time file changes).

}

private void EnqueueSemanticTokenRefreshNotification(Uri? documentUri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ public ILspService CreateILspService(LspServices lspServices, WellKnownLspServer
{
var notificationManager = lspServices.GetRequiredService<IClientLanguageServerManager>();
var lspWorkspaceManager = lspServices.GetRequiredService<LspWorkspaceManager>();
var capabilitiesProvider = lspServices.GetRequiredService<ICapabilitiesProvider>();

return new SemanticTokensRefreshQueue(_asyncListenerProvider, _lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager);
return new SemanticTokensRefreshQueue(_asyncListenerProvider, _lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager, capabilitiesProvider);
}
}
}