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

Don't hold onto Roslyn projects longer than necessary #11458

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

#if !NET
using System;
#endif

using System.Diagnostics;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Razor.ProjectSystem;

internal static class Extensions
{
/// <summary>
/// Returns <see langword="true"/> if this <see cref="ProjectKey"/> matches the given <see cref="Project"/>.
/// </summary>
public static bool Matches(this ProjectKey projectKey, Project project)
{
// In order to perform this check, we are relying on the fact that Id will always end with a '/',
// because it is guaranteed to be normalized. However, CompilationOutputInfo.AssemblyPath will
// contain the assembly file name, which AreDirectoryPathsEquivalent will shave off before comparing.
// So, AreDirectoryPathsEquivalent will return true when Id is "C:/my/project/path/"
// and the assembly path is "C:\my\project\path\assembly.dll"

Debug.Assert(projectKey.Id.EndsWith('/'), $"This method can't be called if {nameof(projectKey.Id)} is not a normalized directory path.");

return FilePathNormalizer.AreDirectoryPathsEquivalent(projectKey.Id, project.CompilationOutputInfo.AssemblyPath);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.ProjectSystem;

namespace Microsoft.CodeAnalysis;

internal static class SolutionExtensions
{
public static bool TryGetProject(this Solution solution, ProjectId projectId, [NotNullWhen(true)] out Project? result)
{
result = solution.GetProject(projectId);
return result is not null;
}

public static Project GetRequiredProject(this Solution solution, ProjectId projectId)
{
return solution.GetProject(projectId)
?? ThrowHelper.ThrowInvalidOperationException<Project>($"The project {projectId} did not exist in {solution}.");
}

public static bool TryGetDocument(this Solution solution, DocumentId documentId, [NotNullWhen(true)] out Document? result)
{
result = solution.GetDocument(documentId);
return result is not null;
}

public static Document GetRequiredDocument(this Solution solution, DocumentId documentId)
{
return solution.GetDocument(documentId)
?? ThrowHelper.ThrowInvalidOperationException<Document>($"The document {documentId} did not exist in {solution.FilePath ?? "solution"}.");
}

public static Project? GetProject(this Solution solution, ProjectKey projectKey)
{
return solution.Projects.FirstOrDefault(project => projectKey.Matches(project));
}

public static bool TryGetProject(this Solution solution, ProjectKey projectKey, [NotNullWhen(true)] out Project? result)
{
result = solution.GetProject(projectKey);
return result is not null;
}

public static Project GetRequiredProject(this Solution solution, ProjectKey projectKey)
{
return solution.GetProject(projectKey)
?? ThrowHelper.ThrowInvalidOperationException<Project>($"The project {projectKey} did not exist in {solution}.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis.Razor;

namespace Microsoft.CodeAnalysis;
Expand All @@ -30,16 +29,4 @@ public static bool TryGetRazorDocument(this Solution solution, Uri razorDocument
razorDocument = document;
return true;
}

public static Project GetRequiredProject(this Solution solution, ProjectId projectId)
{
return solution.GetProject(projectId)
?? ThrowHelper.ThrowInvalidOperationException<Project>($"The projectId {projectId} did not exist in {solution}.");
}

public static Document GetRequiredDocument(this Solution solution, DocumentId documentId)
{
return solution.GetDocument(documentId)
?? ThrowHelper.ThrowInvalidOperationException<Document>($"The document {documentId} did not exist in {solution.FilePath ?? "solution"}.");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

#if !NET
using System;
#endif

using System.Diagnostics;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Serialization;
using Microsoft.AspNetCore.Razor.Utilities;
Expand All @@ -22,20 +17,4 @@ public static ProjectKey ToProjectKey(this Project project)
var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(project.CompilationOutputInfo.AssemblyPath);
return new(intermediateOutputPath);
}

/// <summary>
/// Returns <see langword="true"/> if this <see cref="ProjectKey"/> matches the given <see cref="Project"/>.
/// </summary>
public static bool Matches(this ProjectKey projectKey, Project project)
{
// In order to perform this check, we are relying on the fact that Id will always end with a '/',
// because it is guaranteed to be normalized. However, CompilationOutputInfo.AssemblyPath will
// contain the assembly file name, which AreDirectoryPathsEquivalent will shave off before comparing.
// So, AreDirectoryPathsEquivalent will return true when Id is "C:/my/project/path/"
// and the assembly path is "C:\my\project\path\assembly.dll"

Debug.Assert(projectKey.Id.EndsWith('/'), $"This method can't be called if {nameof(projectKey.Id)} is not a normalized directory path.");

return FilePathNormalizer.AreDirectoryPathsEquivalent(projectKey.Id, project.CompilationOutputInfo.AssemblyPath);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.CodeAnalysis;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal interface IProjectStateUpdater
{
void EnqueueUpdate(ProjectKey key, ProjectId? id);

void CancelUpdates();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;

namespace Microsoft.VisualStudio.Razor.Remote;
namespace Microsoft.VisualStudio.Razor.Discovery;

/// <summary>
/// Retrieves <see cref="TagHelperDescriptor">tag helpers</see> for a given <see cref="Project"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Threading;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal sealed partial class ProjectBuildDetector
{
internal TestAccessor GetTestAccessor() => new(this);

internal readonly struct TestAccessor(ProjectBuildDetector instance)
{
public JoinableTask InitializeTask => instance._initializeTask;
public Task? OnProjectBuiltTask => instance._projectBuiltTask;

public Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, CancellationToken cancellationToken)
=> instance.OnProjectBuiltAsync(projectHierarchy, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.Razor.Extensions;
Expand All @@ -14,14 +14,14 @@
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.Razor;
namespace Microsoft.VisualStudio.Razor.Discovery;

[Export(typeof(IRazorStartupService))]
internal class VsSolutionUpdatesProjectSnapshotChangeTrigger : IRazorStartupService, IVsUpdateSolutionEvents2, IDisposable
internal sealed partial class ProjectBuildDetector : IRazorStartupService, IVsUpdateSolutionEvents2, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly ProjectSnapshotManager _projectManager;
private readonly IProjectWorkspaceStateGenerator _workspaceStateGenerator;
private readonly IProjectStateUpdater _projectStateUpdater;
private readonly IWorkspaceProvider _workspaceProvider;
private readonly JoinableTaskFactory _jtf;
private readonly CancellationTokenSource _disposeTokenSource;
Expand All @@ -33,16 +33,16 @@ internal class VsSolutionUpdatesProjectSnapshotChangeTrigger : IRazorStartupServ
private Task? _projectBuiltTask;

[ImportingConstructor]
public VsSolutionUpdatesProjectSnapshotChangeTrigger(
public ProjectBuildDetector(
[Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
ProjectSnapshotManager projectManager,
IProjectWorkspaceStateGenerator workspaceStateGenerator,
IProjectStateUpdater projectStateUpdater,
IWorkspaceProvider workspaceProvider,
JoinableTaskContext joinableTaskContext)
{
_serviceProvider = serviceProvider;
_projectManager = projectManager;
_workspaceStateGenerator = workspaceStateGenerator;
_projectStateUpdater = projectStateUpdater;
_workspaceProvider = workspaceProvider;
_jtf = joinableTaskContext.Factory;

Expand Down Expand Up @@ -110,7 +110,7 @@ private void ProjectManager_Changed(object sender, ProjectChangeEventArgs args)
if (args.IsSolutionClosing)
{
// If the solution is closing, cancel all existing updates.
_workspaceStateGenerator.CancelUpdates();
_projectStateUpdater.CancelUpdates();
}
}

Expand All @@ -123,30 +123,22 @@ private async Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, Cancellati
}

var projectKeys = _projectManager.GetProjectKeysWithFilePath(projectFilePath);
if (projectKeys.IsEmpty)
{
return;
}

var workspace = _workspaceProvider.GetWorkspace();
var solution = workspace.CurrentSolution;

foreach (var projectKey in projectKeys)
{
if (_projectManager.TryGetProject(projectKey, out var project))
if (solution.TryGetProject(projectKey, out var workspaceProject))
{
var workspace = _workspaceProvider.GetWorkspace();
var workspaceProject = workspace.CurrentSolution.Projects.FirstOrDefault(wp => wp.ToProjectKey() == project.Key);
if (workspaceProject is not null)
{
// Trigger a tag helper update by forcing the project manager to see the workspace Project
// from the current solution.
_workspaceStateGenerator.EnqueueUpdate(workspaceProject, project);
}
// Trigger a tag helper update by forcing the project manager to see the workspace Project
// from the current solution.
_projectStateUpdater.EnqueueUpdate(projectKey, workspaceProject.Id);
}
}
}

internal TestAccessor GetTestAccessor() => new(this);

internal sealed class TestAccessor(VsSolutionUpdatesProjectSnapshotChangeTrigger instance)
{
public JoinableTask InitializeTask => instance._initializeTask;
public Task? OnProjectBuiltTask => instance._projectBuiltTask;

public Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, CancellationToken cancellationToken)
=> instance.OnProjectBuiltAsync(projectHierarchy, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal partial class ProjectStateChangeDetector
{
private sealed class Comparer : IEqualityComparer<Work>
{
public static readonly Comparer Instance = new();

private Comparer()
{
}

public bool Equals(Work x, Work y)
=> x.Key == y.Key;

public int GetHashCode(Work obj)
=> obj.Key.GetHashCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal partial class ProjectStateChangeDetector
{
internal TestAccessor GetTestAccessor() => new(this);

internal sealed class TestAccessor(ProjectStateChangeDetector instance)
{
public void CancelExistingWork()
{
instance._workQueue.CancelExistingWork();
}

public async Task WaitUntilCurrentBatchCompletesAsync()
{
await instance._workQueue.WaitUntilCurrentBatchCompletesAsync();
}

public Task ListenForWorkspaceChangesAsync(params WorkspaceChangeKind[] kinds)
{
if (instance._workspaceChangedListener is not null)
{
throw new InvalidOperationException($"There's already a {nameof(WorkspaceChangedListener)} installed.");
}

var listener = new WorkspaceChangedListener(kinds.ToImmutableArray());
instance._workspaceChangedListener = listener;

return listener.Task;
}

public void WorkspaceChanged(WorkspaceChangeEventArgs e)
{
instance.Workspace_WorkspaceChanged(instance, e);
}
}
}
Loading
Loading