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

Move RazorWorkspaceListener to AsyncBatchingWorkQueue #10480

Merged
merged 12 commits into from
Jun 14, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.EnsureInitialized(Microsoft.CodeAnalysis.Workspace! workspace, string! projectInfoFileName) -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.NotifyDynamicFile(Microsoft.CodeAnalysis.ProjectId! projectId) -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.RazorWorkspaceListener(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void
virtual Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.SerializeProjectAsync(Microsoft.CodeAnalysis.ProjectId! projectId, System.Threading.CancellationToken ct) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.SerializeProjectAsync(Microsoft.CodeAnalysis.ProjectId! projectId, Microsoft.CodeAnalysis.Solution! solution, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace;

Expand All @@ -29,17 +30,19 @@ static RazorProjectInfoSerializer()
: StringComparison.OrdinalIgnoreCase;
}

public static async Task SerializeAsync(Project project, string configurationFileName, CancellationToken cancellationToken)
public static async Task SerializeAsync(Project project, string configurationFileName, ILogger? logger, CancellationToken cancellationToken)
{
var projectPath = Path.GetDirectoryName(project.FilePath);
if (projectPath is null)
{
logger?.LogTrace("projectPath is null, skipping writing info for {projectId}", project.Id);
return;
}

var intermediateOutputPath = Path.GetDirectoryName(project.CompilationOutputInfo.AssemblyPath);
if (intermediateOutputPath is null)
{
logger?.LogTrace("intermediatePath is null, skipping writing info for {projectId}", project.Id);
return;
}

Expand All @@ -49,13 +52,14 @@ public static async Task SerializeAsync(Project project, string configurationFil
// Not a razor project
if (documents.Length == 0)
{
logger?.LogTrace("No razor documents for {projectId}", project.Id);
return;
}

var csharpLanguageVersion = (project.ParseOptions as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.Default;

var options = project.AnalyzerOptions.AnalyzerConfigOptionsProvider;
var configuration = ComputeRazorConfigurationOptions(options, out var defaultNamespace);
var configuration = ComputeRazorConfigurationOptions(options, logger, out var defaultNamespace);

var fileSystem = RazorProjectFileSystem.Create(projectPath);

Expand Down Expand Up @@ -93,10 +97,10 @@ public static async Task SerializeAsync(Project project, string configurationFil
projectWorkspaceState: projectWorkspaceState,
documents: documents);

WriteToFile(configurationFilePath, projectInfo);
WriteToFile(configurationFilePath, projectInfo, logger);
}

private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfigOptionsProvider options, out string defaultNamespace)
private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfigOptionsProvider options, ILogger? logger, out string defaultNamespace)
{
// See RazorSourceGenerator.RazorProviders.cs

Expand All @@ -111,6 +115,7 @@ private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfi
if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) ||
!RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion))
{
logger?.LogTrace("Using default of latest language version");
razorLanguageVersion = RazorLanguageVersion.Latest;
}

Expand All @@ -121,7 +126,7 @@ private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfi
return razorConfiguration;
}

private static void WriteToFile(string configurationFilePath, RazorProjectInfo projectInfo)
private static void WriteToFile(string configurationFilePath, RazorProjectInfo projectInfo, ILogger? logger)
{
// We need to avoid having an incomplete file at any point, but our
// project configuration is large enough that it will be written as multiple operations.
Expand All @@ -131,6 +136,7 @@ private static void WriteToFile(string configurationFilePath, RazorProjectInfo p
if (tempFileInfo.Exists)
{
// This could be caused by failures during serialization or early process termination.
logger?.LogTrace("deleting existing file {filePath}", tempFilePath);
tempFileInfo.Delete();
}

Expand All @@ -144,9 +150,11 @@ private static void WriteToFile(string configurationFilePath, RazorProjectInfo p
var fileInfo = new FileInfo(configurationFilePath);
if (fileInfo.Exists)
{
logger?.LogTrace("deleting existing file {filePath}", configurationFilePath);
fileInfo.Delete();
}

logger?.LogTrace("Moving {tmpPath} to {newPath}", tempFilePath, configurationFilePath);
File.Move(tempFileInfo.FullName, configurationFilePath);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;

Expand All @@ -15,11 +16,14 @@ public class RazorWorkspaceListener : IDisposable

private string? _projectInfoFileName;
private Workspace? _workspace;
private ImmutableDictionary<ProjectId, TaskDelayScheduler> _workQueues = ImmutableDictionary<ProjectId, TaskDelayScheduler>.Empty;
private ImmutableArray<ProjectId> _currentProjects = ImmutableArray<ProjectId>.Empty;
private readonly CancellationTokenSource _cancellationTokenSource = new();
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
private AsyncBatchingWorkQueue<ProjectId> _workQueue;
ryzngard marked this conversation as resolved.
Show resolved Hide resolved

public RazorWorkspaceListener(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger(nameof(RazorWorkspaceListener));
_workQueue = new(TimeSpan.FromMilliseconds(500), UpdateCurrentProjectsAsync, _cancellationTokenSource.Token);
}

public void EnsureInitialized(Workspace workspace, string projectInfoFileName)
Expand All @@ -43,16 +47,11 @@ public void NotifyDynamicFile(ProjectId projectId)
if (_workspace is null || !_workspace.CurrentSolution.ContainsProject(projectId))
{
return;
}

// We expect this to be called multiple times per project so a no-op update operation seems like a better choice
// than constructing a new TaskDelayScheduler each time, and using the TryAdd method, which doesn't support a
// valueFactory argument.
var scheduler = ImmutableInterlocked.AddOrUpdate(ref _workQueues, projectId, static _ => new TaskDelayScheduler(s_debounceTime, CancellationToken.None), static (_, val) => val);
}

// Schedule a task, in case adding a dynamic file is the last thing that happens
_logger.LogTrace("{projectId} scheduling task due to dynamic file", projectId);
scheduler.ScheduleAsyncTask(ct => SerializeProjectAsync(projectId, ct), CancellationToken.None);
_workQueue.AddWork(projectId);
}

private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
Expand All @@ -61,11 +60,6 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
{
case WorkspaceChangeKind.SolutionChanged:
case WorkspaceChangeKind.SolutionReloaded:
foreach (var project in e.OldSolution.Projects)
{
RemoveProject(project);
}

foreach (var project in e.NewSolution.Projects)
{
EnqueueUpdate(project);
Expand All @@ -81,12 +75,7 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs

break;

case WorkspaceChangeKind.ProjectRemoved:
RemoveProject(e.OldSolution.GetProject(e.ProjectId));
break;

case WorkspaceChangeKind.ProjectReloaded:
RemoveProject(e.OldSolution.GetProject(e.ProjectId));
EnqueueUpdate(e.NewSolution.GetProject(e.ProjectId));
break;

Expand All @@ -111,33 +100,12 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
EnqueueUpdate(e.NewSolution.GetProject(projectId));
}

break;
case WorkspaceChangeKind.SolutionCleared:
case WorkspaceChangeKind.SolutionRemoved:
foreach (var project in e.OldSolution.Projects)
{
RemoveProject(project);
}

break;
default:
break;
}
}

private void RemoveProject(Project? project)
Copy link
Member

Choose a reason for hiding this comment

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

It's a little worrisome that we drop project removes on the floor. We definitely watch for deletions of the project.razor.bin file in the language server. If a project is deleted from disk, do we leave the language server in a descent state? Or, does that project continue to live there with all of its documents and references, responding to Rename, Component search, completion, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This never handled removing the file, it just stopped the queue. I'm not actually sure what handles cleanup. Since we're working on _workspace.CurrentSolution I'm guessing it would be safe to just delete the file when a project is removed?

That's new, and maybe it's already done somewhere else? Not sure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One thought:

We could enqueue and update for the removed ProjectId and in the ABWQ work method we could clean up the file if it exists and the project is no longer in the solution?

Copy link
Contributor

Choose a reason for hiding this comment

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

This was probably unnecessary originally, but yeah I was just trying to remove the scheduler. We'd never get any more updates for the project anyway, so could have ignored removes then too.

Not sure I like the idea of removing the file though. I think in VS Code we are more likely to be processing requests before the file is written, so it does serve more like an offline cache, even if it is one that won't be being updated any more.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I agree that this is probably OK. I saw all of the red and notice that all it did was get rid of the scheduler before. With #10475 I think VS Code will start reading in .bin even sooner, so you're right that VS Code will use it more as a cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test failures lead to one thing remove did that I didn't notice: we only queue work for items that have been reported with NotifyDynamicFile. This PR breaks that. I'm adding remove back to fix

{
if (project is null)
{
return;
}

if (ImmutableInterlocked.TryRemove(ref _workQueues, project.Id, out var scheduler))
{
scheduler.Dispose();
}
}

private void EnqueueUpdate(Project? project)
{
if (_projectInfoFileName is null ||
Expand All @@ -150,30 +118,43 @@ project is not
}

var projectId = project.Id;
if (_workQueues.TryGetValue(projectId, out var scheduler))
_workQueue.AddWork(projectId);
}

private async ValueTask UpdateCurrentProjectsAsync(ImmutableArray<ProjectId> projectIds, CancellationToken cancellationToken)
{
if (_workspace is null)
{
_logger.LogTrace("{projectId} scheduling task due to workspace event", projectId);
_logger?.LogTrace("No workspace available when trying to write project info");
return;
}
ryzngard marked this conversation as resolved.
Show resolved Hide resolved

scheduler.ScheduleAsyncTask(ct => SerializeProjectAsync(projectId, ct), CancellationToken.None);
if (_projectInfoFileName is null)
{
_logger?.LogTrace("Don't know what filename to use");
return;
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Protected for testing
protected virtual Task SerializeProjectAsync(ProjectId projectId, CancellationToken ct)
{
if (_projectInfoFileName is null || _workspace is null)
var solution = _workspace.CurrentSolution;

foreach (var projectId in projectIds.Distinct())
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
{
return Task.CompletedTask;
await SerializeProjectAsync(projectId, solution, cancellationToken).ConfigureAwait(false);
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
}
}

var project = _workspace.CurrentSolution.GetProject(projectId);
// Protected for testing
protected virtual Task SerializeProjectAsync(ProjectId projectId, Solution solution, CancellationToken cancellationToken)
{
var project = solution.GetProject(projectId);
if (project is null)
{
_logger?.LogTrace("Project {projectId} is not in workspace", projectId);
return Task.CompletedTask;
}

_logger.LogTrace("{projectId} writing json file", projectId);
return RazorProjectInfoSerializer.SerializeAsync(project, _projectInfoFileName, ct);
_logger?.LogTrace("Serializing information for {projectId}", projectId);
return RazorProjectInfoSerializer.SerializeAsync(project, _projectInfoFileName.AssumeNotNull(), _logger, cancellationToken);
}

public void Dispose()
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -183,10 +164,7 @@ public void Dispose()
_workspace.WorkspaceChanged -= Workspace_WorkspaceChanged;
}

var queues = Interlocked.Exchange(ref _workQueues, ImmutableDictionary<ProjectId, TaskDelayScheduler>.Empty);
foreach (var (_, value) in queues)
{
value.Dispose();
}
_cancellationTokenSource.Cancel();
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
_cancellationTokenSource.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Threading;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;

namespace Microsoft.AspNetCore.Razor.LanguageServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;

namespace Microsoft.AspNetCore.Razor.LanguageServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.VisualStudio.Threading;

namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.VisualStudio.Threading;

namespace Microsoft.AspNetCore.Razor.LanguageServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="MessagePack" />
<PackageReference Include="System.Threading.Tasks.Extensions" />
<PackageReference Include="Microsoft.VisualStudio.Threading" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;

namespace Microsoft.CodeAnalysis.Razor.Utilities;
namespace Microsoft.AspNetCore.Razor.Utilities;

// NOTE: This code is copied and modified slightly from dotnet/roslyn:
// https://github.com/dotnet/roslyn/blob/98cd097bf122677378692ebe952b71ab6e5bb013/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue%600.cs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;

namespace Microsoft.CodeAnalysis.Razor.Utilities;
namespace Microsoft.AspNetCore.Razor.Utilities;

// NOTE: This code is copied and modified slightly from dotnet/roslyn:
// https://github.com/dotnet/roslyn/blob/98cd097bf122677378692ebe952b71ab6e5bb013/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue%601.cs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.VisualStudio.Threading;

namespace Microsoft.CodeAnalysis.Razor.Utilities;
namespace Microsoft.AspNetCore.Razor.Utilities;

// NOTE: This code is derived from dotnet/roslyn:
// https://github.com/dotnet/roslyn/blob/98cd097bf122677378692ebe952b71ab6e5bb013/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue%602.cs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.Razor.ProjectSystem;
using Microsoft.VisualStudio.Threading;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Utilities;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.Razor.ProjectSystem;

Expand Down
Loading
Loading