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

Ignore irrelevant exceptions when reporting LSP server NFW #75150

Merged
merged 3 commits into from
Sep 23, 2024
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
13 changes: 9 additions & 4 deletions src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
Expand Down Expand Up @@ -49,6 +50,12 @@ public AbstractRefreshQueue(
}

public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken)
{
Initialize(clientCapabilities);
return Task.CompletedTask;
}

public void Initialize(ClientCapabilities clientCapabilities)
{
if (_refreshQueue is null && GetRefreshSupport(clientCapabilities) is true)
{
Expand All @@ -66,11 +73,9 @@ public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestCon
_isQueueCreated = true;
_lspWorkspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged;
}

return Task.CompletedTask;
}

private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
protected virtual void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
{
if (e.DocumentId is not null && e.Kind is WorkspaceChangeKind.DocumentChanged)
{
Expand Down Expand Up @@ -113,7 +118,7 @@ private ValueTask FilterLspTrackedDocumentsAsync(
{
return notificationManager.SendRequestAsync(GetWorkspaceRefreshName(), cancellationToken);
}
catch (StreamJsonRpc.ConnectionLostException)
catch (Exception ex) when (ex is ObjectDisposedException or ConnectionLostException)
{
// It is entirely possible that we're shutting down and the connection is lost while we're trying to send a notification
// as this runs outside of the guaranteed ordering in the queue. We can safely ignore this exception.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,15 @@ private async Task RefreshOptionsAsync(CancellationToken cancellationToken)

private async Task<ImmutableArray<string?>> GetConfigurationsAsync(CancellationToken cancellationToken)
{
try
{
var configurationParams = new ConfigurationParams() { Items = _configurationItems.AsArray() };
var options = await _clientLanguageServerManager.SendRequestAsync<ConfigurationParams, JsonArray>(
Methods.WorkspaceConfigurationName, configurationParams, cancellationToken).ConfigureAwait(false);

// Failed to get result from client.
Contract.ThrowIfNull(options);
var converted = options.SelectAsArray(token => token?.ToString());
return converted;
}
catch (Exception e)
{
_lspLogger.LogException(e, $"Exception occurs when make {Methods.WorkspaceConfigurationName}.");
}

return [];
// Attempt to get configurations from the client. If this throws we'll get NFW reports.
var configurationParams = new ConfigurationParams() { Items = _configurationItems.AsArray() };
var options = await _clientLanguageServerManager.SendRequestAsync<ConfigurationParams, JsonArray>(
Methods.WorkspaceConfigurationName, configurationParams, cancellationToken).ConfigureAwait(false);

// Failed to get result from client.
Contract.ThrowIfNull(options);
var converted = options.SelectAsArray(token => token?.ToString());
return converted;
}

private static ImmutableArray<(IOption2 option, string? langaugeName)> GenerateOptionsNeedsToRefresh()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,16 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;

/// <summary>
/// Batches requests to refresh the semantic tokens to optimize user experience.
/// </summary>
/// <remarks>This implements <see cref="IOnInitialized"/> to avoid race conditions related to creating the queue on the
/// first request.</remarks>
internal class SemanticTokensRefreshQueue :
IOnInitialized,
ILspService,
IDisposable
internal class SemanticTokensRefreshQueue : AbstractRefreshQueue
{
/// <summary>
/// Lock over the mutable state that follows.
Expand All @@ -36,108 +29,38 @@ internal class SemanticTokensRefreshQueue :
/// </summary>
private readonly Dictionary<ProjectId, Checksum> _projectIdToLastComputedChecksum = [];

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

private readonly IAsynchronousOperationListener _asyncListener;
private readonly CancellationTokenSource _disposalTokenSource = new();

/// <summary>
/// Debouncing queue so that we don't attempt to issue a semantic tokens refresh notification too often.
/// <para/>
/// <see langword="null"/> when the client does not support sending refresh notifications.
/// </summary>
private AsyncBatchingWorkQueue<Uri?>? _semanticTokenRefreshQueue;

private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService;

public SemanticTokensRefreshQueue(
IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
LspWorkspaceRegistrationService lspWorkspaceRegistrationService,
LspWorkspaceManager lspWorkspaceManager,
IClientLanguageServerManager notificationManager,
ICapabilitiesProvider capabilitiesProvider)
IClientLanguageServerManager notificationManager) : base(asynchronousOperationListenerProvider, lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager)
{
_asyncListener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Classification);

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

public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken _)
{
if (_capabilitiesProvider.GetCapabilities(clientCapabilities).SemanticTokensOptions is not null)
{
Initialize(clientCapabilities);
}

return Task.CompletedTask;
}

public void Initialize(ClientCapabilities clientCapabilities)
{
if (_semanticTokenRefreshQueue is null
&& clientCapabilities.Workspace?.SemanticTokens?.RefreshSupport is true)
{
// 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, but also means we send soon
// enough after a compilation-computation to not make the user wait an enormous amount of time.
_semanticTokenRefreshQueue = new AsyncBatchingWorkQueue<Uri?>(
delay: TimeSpan.FromMilliseconds(2000),
processBatchAsync: FilterLspTrackedDocumentsAsync,
equalityComparer: EqualityComparer<Uri?>.Default,
asyncListener: _asyncListener,
_disposalTokenSource.Token);

_lspWorkspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged;
}
}

public async Task TryEnqueueRefreshComputationAsync(Project project, CancellationToken cancellationToken)
{
if (_semanticTokenRefreshQueue is not null)
{
// Determine the checksum for this project cone. Note: this should be fast in practice because this is the
// same project-cone-checksum we used to even call into OOP above when we computed semantic tokens.
var projectChecksum = await project.Solution.CompilationState.GetChecksumAsync(project.Id, cancellationToken).ConfigureAwait(false);
// Determine the checksum for this project cone. Note: this should be fast in practice because this is the
// same project-cone-checksum we used to even call into OOP above when we computed semantic tokens.
var projectChecksum = await project.Solution.CompilationState.GetChecksumAsync(project.Id, cancellationToken).ConfigureAwait(false);

lock (_gate)
{
// If this checksum is the same as the last computed result, no need to continue, we would not produce a
// different compilation.
if (_projectIdToLastComputedChecksum.TryGetValue(project.Id, out var lastChecksum) && lastChecksum == projectChecksum)
return;

// keep track of this checksum. That way we don't get into a loop where we send a refresh notification,
// then we get called back into, causing us to compute the compilation, causing us to send the refresh
// notification, etc. etc.
_projectIdToLastComputedChecksum[project.Id] = projectChecksum;

}
lock (_gate)
{
// If this checksum is the same as the last computed result, no need to continue, we would not produce a
// different compilation.
if (_projectIdToLastComputedChecksum.TryGetValue(project.Id, out var lastChecksum) && lastChecksum == projectChecksum)
return;

EnqueueSemanticTokenRefreshNotification(documentUri: null);
}
}
// keep track of this checksum. That way we don't get into a loop where we send a refresh notification,
// then we get called back into, causing us to compute the compilation, causing us to send the refresh
// notification, etc. etc.
_projectIdToLastComputedChecksum[project.Id] = projectChecksum;

private ValueTask FilterLspTrackedDocumentsAsync(
ImmutableSegmentedList<Uri?> documentUris,
CancellationToken cancellationToken)
{
var trackedDocuments = _lspWorkspaceManager.GetTrackedLspText();
foreach (var documentUri in documentUris)
{
if (documentUri is null || !trackedDocuments.ContainsKey(documentUri))
return _notificationManager.SendRequestAsync(Methods.WorkspaceSemanticTokensRefreshName, cancellationToken);
}

// LSP is already tracking all changed documents so we don't need to send a refresh request.
return ValueTaskFactory.CompletedTask;
EnqueueRefreshNotification(documentUri: null);
}

private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
protected override void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
{
Uri? documentUri = null;

Expand Down Expand Up @@ -173,7 +96,7 @@ private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
}
}

EnqueueSemanticTokenRefreshNotification(documentUri);
EnqueueRefreshNotification(documentUri);
}

// Duplicated from Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.LoadedProject.TreatAsIsDynamicFile
Expand All @@ -183,21 +106,9 @@ private static bool DisallowsAdditionalDocumentChangedRefreshes(string? filePath
return extension is ".cshtml" or ".razor";
}

private void EnqueueSemanticTokenRefreshNotification(Uri? documentUri)
{
// We should have only gotten here if semantic tokens refresh is supported and initialized.
Contract.ThrowIfNull(_semanticTokenRefreshQueue);
_semanticTokenRefreshQueue.AddWork(documentUri);
}
protected override string GetFeatureAttribute() => FeatureAttribute.Classification;

public void Dispose()
{
lock (_gate)
{
_lspWorkspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged;
}
protected override bool? GetRefreshSupport(ClientCapabilities clientCapabilities) => clientCapabilities.Workspace?.SemanticTokens?.RefreshSupport;

_disposalTokenSource.Cancel();
_disposalTokenSource.Dispose();
}
protected override string GetWorkspaceRefreshName() => Methods.WorkspaceSemanticTokensRefreshName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ 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, capabilitiesProvider);
return new SemanticTokensRefreshQueue(_asyncListenerProvider, _lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager);
}
}
}
5 changes: 5 additions & 0 deletions src/LanguageServer/Protocol/RoslynRequestExecutionQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public override async Task WrapStartRequestTaskAsync(Task nonMutatingRequestTask
{
await nonMutatingRequestTask.ConfigureAwait(false);
}
catch (StreamJsonRpc.LocalRpcException localRpcException) when (localRpcException.ErrorCode == LspErrorCodes.ContentModified)
{
// Content modified exceptions are expected and should not be reported as NFWs.
throw;
}
// If we had an exception, we want to record a NFW for it AND propogate it out to the queue so it can be handled appropriately.
catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, ErrorSeverity.Critical))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,12 @@ protected override MetadataReference ReadMetadataReferenceFrom(ObjectReader read
: base.ReadMetadataReferenceFrom(reader);

protected override Checksum CreateChecksum(AnalyzerReference reference)
=> reference is TestGeneratorReference generatorReference
? generatorReference.Checksum
: base.CreateChecksum(reference);
=> reference switch
{
TestGeneratorReference generatorReference => generatorReference.Checksum,
TestAnalyzerReferenceByLanguage analyzerReferenceByLanguage => analyzerReferenceByLanguage.Checksum,
_ => base.CreateChecksum(reference)
};

protected override void WriteAnalyzerReferenceTo(AnalyzerReference reference, ObjectWriter writer)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
Expand All @@ -17,12 +18,19 @@ public TestAnalyzerReferenceByLanguage(IReadOnlyDictionary<string, ImmutableArra
{
_analyzersMap = analyzersMap;
FullPath = fullPath;

// Make up a checksum so we can calculate Project checksums containing these references
var checksumArray = Guid.NewGuid().ToByteArray();
Array.Resize(ref checksumArray, Checksum.HashSize);
this.Checksum = Checksum.From(checksumArray);
}

public override string? FullPath { get; }
public override string Display => nameof(TestAnalyzerReferenceByLanguage);
public override object Id => Display;

public Checksum Checksum;

public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
=> _analyzersMap.SelectManyAsArray(kvp => kvp.Value);

Expand Down
Loading