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

Fix workspace pull diagnostics #66886

Merged
merged 4 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -113,6 +113,15 @@ protected abstract ValueTask<ImmutableArray<IDiagnosticSource>> GetOrderedDiagno

protected abstract string? GetDiagnosticCategory(TDiagnosticsParams diagnosticsParams);

/// <summary>
/// Used by public workspace pull diagnostics to allow it to keep the connection open until
/// changes occur to avoid the client spamming the server with requests.
/// </summary>
protected virtual Task WaitForChangesAsync(RequestContext context)
{
return Task.CompletedTask;
}

public async Task<TReturn?> HandleRequestAsync(
TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -182,6 +191,11 @@ await ComputeAndReportCurrentDiagnosticsAsync(
}
}

// Some implementations of the spec will re-open requests as soon as we close them, spamming the server.
// In those cases, we wait for the implementation to indicate that changes have occurred, then we close the connection
// so that the client asks us again.
await WaitForChangesAsync(context).ConfigureAwait(false);

// If we had a progress object, then we will have been reporting to that. Otherwise, take what we've been
// collecting and return that.
context.TraceInformation($"{this.GetType()} finished getting diagnostics");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,34 @@
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public;

[ExportCSharpVisualBasicLspServiceFactory(typeof(PublicWorkspacePullDiagnosticsHandler)), Shared]
internal class PublicWorkspacePullDiagnosticHandlerFactory : ILspServiceFactory
{
private readonly LspWorkspaceRegistrationService _registrationService;
private readonly IDiagnosticAnalyzerService _analyzerService;
private readonly EditAndContinueDiagnosticUpdateSource _editAndContinueDiagnosticUpdateSource;
private readonly IGlobalOptionService _globalOptions;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public PublicWorkspacePullDiagnosticHandlerFactory(
LspWorkspaceRegistrationService registrationService,
IDiagnosticAnalyzerService analyzerService,
EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource,
IGlobalOptionService globalOptions)
{
_registrationService = registrationService;
_analyzerService = analyzerService;
_editAndContinueDiagnosticUpdateSource = editAndContinueDiagnosticUpdateSource;
_globalOptions = globalOptions;
}

public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind)
=> new PublicWorkspacePullDiagnosticsHandler(_analyzerService, _editAndContinueDiagnosticUpdateSource, _globalOptions);
{
var workspaceManager = lspServices.GetRequiredService<LspWorkspaceManager>();
return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, _registrationService, _analyzerService, _editAndContinueDiagnosticUpdateSource, _globalOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public;

Expand All @@ -21,14 +22,32 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public;
using WorkspaceDiagnosticPartialReport = SumType<WorkspaceDiagnosticReport, WorkspaceDiagnosticReportPartialResult>;

[Method(Methods.WorkspaceDiagnosticName)]
internal class PublicWorkspacePullDiagnosticsHandler : AbstractPullDiagnosticHandler<WorkspaceDiagnosticParams, WorkspaceDiagnosticPartialReport, WorkspaceDiagnosticReport?>
internal class PublicWorkspacePullDiagnosticsHandler : AbstractPullDiagnosticHandler<WorkspaceDiagnosticParams, WorkspaceDiagnosticPartialReport, WorkspaceDiagnosticReport?>, IDisposable
Copy link
Member

Choose a reason for hiding this comment

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

curious. why only do this in the public workspace pull side? is the non-public side not affected?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, the client polls in the other version.

{
private readonly LspWorkspaceRegistrationService _workspaceRegistrationService;
private readonly LspWorkspaceManager _workspaceManager;

/// <summary>
/// Flag that represents whether the LSP view of the world has changed.
/// It is totally fine for this to somewhat over-report changes
/// as it is an optimization used to delay closing workspace diagnostic requests
/// until something has changed.
/// </summary>
private int _lspChanged = 0;

public PublicWorkspacePullDiagnosticsHandler(
LspWorkspaceManager workspaceManager,
LspWorkspaceRegistrationService registrationService,
IDiagnosticAnalyzerService analyzerService,
EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource,
IGlobalOptionService globalOptions)
: base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions)
{
_workspaceRegistrationService = registrationService;
_workspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged;

_workspaceManager = workspaceManager;
_workspaceManager.LspTextChanged += OnLspTextChanged;
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

nit: why do we listen for text-changed if we're alrady listening to solution changed? the former seems unnecessary. would that help simplify?

Copy link
Member Author

Choose a reason for hiding this comment

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

LspSolutionChange means the backing workspace solution changed, it doesn't include changes for open documents (because we don't create a solution on LSP text changes, only when a request needing a solution comes in).

}

dibarbet marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
Expand Down Expand Up @@ -118,4 +137,41 @@ protected override ValueTask<ImmutableArray<IDiagnosticSource>> GetOrderedDiagno
}
}).ToImmutableArray();
}

private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e)
{
UpdateLspChanged();
}

private void OnLspTextChanged(object? sender, EventArgs e)
{
UpdateLspChanged();
}

private void UpdateLspChanged()
{
Interlocked.Exchange(ref _lspChanged, 1);
}

protected override async Task WaitForChangesAsync(RequestContext context)
Copy link
Member

Choose a reason for hiding this comment

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

💡 Consider adding another way to exit this (some sort of shutdown cancellation token)

Copy link
Member Author

Choose a reason for hiding this comment

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

🤦 ... yes this should absolutely use the cancellation token passed in by the queue. that'll take care of either request cancellation / shutdown. fixed!

{
// Spin waiting until our LSP change flag has been set. When the flag is set (meaning LSP has changed),
// we reset the flag to false and exit out of the loop allowing the request to close.
// The client will automatically trigger a new request as soon as we close it, bringing us up to date on diagnostics.
while (Interlocked.CompareExchange(ref _lspChanged, value: 0, comparand: 1) == 0)
{
// There have been no changes between now and when the last request finished - we will hold the connection open while we poll for changes.
await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
}

context.TraceInformation("Closing workspace/diagnostics request");
// We've hit a change, so we close the current request to allow the client to open a new one.
return;
dibarbet marked this conversation as resolved.
Show resolved Hide resolved
}

public void Dispose()
{
_workspaceManager.LspTextChanged -= OnLspTextChanged;
_workspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged;
}
Copy link
Member

Choose a reason for hiding this comment

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

can you please put Dispose right after constructor? i find it much easier to reason about that way. :)

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public LspWorkspaceManager(
_lspWorkspaceRegistrationService = lspWorkspaceRegistrationService;
}

public EventHandler<EventArgs>? LspTextChanged;
Copy link
Member

Choose a reason for hiding this comment

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

doc.


#region Implementation of IDocumentChangeTracker

/// <summary>
Expand All @@ -102,6 +104,8 @@ public void StartTracking(Uri uri, SourceText documentText)

// If LSP changed, we need to compare against the workspace again to get the updated solution.
_cachedLspSolutions.Clear();

LspTextChanged?.Invoke(this, EventArgs.Empty);
}

/// <summary>
Expand All @@ -120,6 +124,8 @@ public void StopTracking(Uri uri)

// Also remove it from our loose files workspace if it is still there.
_lspMiscellaneousFilesWorkspace.TryRemoveMiscellaneousDocument(uri);

LspTextChanged?.Invoke(this, EventArgs.Empty);
}

/// <summary>
Expand All @@ -135,6 +141,8 @@ public void UpdateTrackedDocument(Uri uri, SourceText newSourceText)

// If LSP changed, we need to compare against the workspace again to get the updated solution.
_cachedLspSolutions.Clear();

LspTextChanged?.Invoke(this, EventArgs.Empty);
}

public ImmutableDictionary<Uri, SourceText> GetTrackedLspText() => _trackedDocuments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,52 @@ public async Task TestWorkspaceDiagnosticsDoesNotThrowIfProjectWithoutFilePathEx
Assert.Equal(@"C:/C2.cs", results[2].TextDocument.Uri.AbsolutePath);
}

[Fact]
public async Task TestPublicWorkspaceDiagnosticsWaitsForLspTextChanges()
{
var markup1 =
@"class A {";
var markup2 = "";
await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync(
new[] { markup1, markup2 }, BackgroundAnalysisScope.FullSolution, useVSDiagnostics: false);

var resultTask = RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: false, useProgress: true);

// Assert that the connection isn't closed and task doesn't complete even after some delay.
await Task.Delay(TimeSpan.FromSeconds(5));
Assert.False(resultTask.IsCompleted);

var uri = testLspServer.GetCurrentSolution().Projects.First().Documents.First().GetURI();
await testLspServer.OpenDocumentAsync(uri);

// Assert the task completes after a change occurs
var results = await resultTask;
Assert.NotEmpty(results);
}

[Fact]
public async Task TestPublicWorkspaceDiagnosticsWaitsForLspSolutionChanges()
{
var markup1 =
@"class A {";
var markup2 = "";
await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync(
new[] { markup1, markup2 }, BackgroundAnalysisScope.FullSolution, useVSDiagnostics: false);

var resultTask = RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: false, useProgress: true);

// Assert that the connection isn't closed and task doesn't complete even after some delay.
await Task.Delay(TimeSpan.FromSeconds(5));
Assert.False(resultTask.IsCompleted);

var projectInfo = testLspServer.TestWorkspace.Projects.Single().ToProjectInfo();
testLspServer.TestWorkspace.OnProjectReloaded(projectInfo);

// Assert the task completes after a change occurs
var results = await resultTask;
Assert.NotEmpty(results);
}

#endregion
}
}