From 78612d64dcb234efeb2cc90c44d3651caca33496 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 25 Sep 2024 08:01:23 -0700 Subject: [PATCH] Reapply "Load analyzers and generators in isolated ALCs in our OOP process" This reverts commit a59061f4bb4116dd15ef7fc4441cd7eb56172f4a. --- .../LanguageServerWorkspaceFactory.cs | 7 +- .../Remote/SerializationValidator.cs | 3 +- .../Services/SolutionServiceTests.cs | 6 +- .../SerializerService_Reference.cs | 30 ++- .../IAnalyzerAssemblyLoaderProvider.cs | 17 +- .../IsolatedAnalyzerFileReference.cs | 114 ++++++++++ .../IsolatedAnalyzerReferenceSet.Core.cs | 207 ++++++++++++++++++ .../IsolatedAnalyzerReferenceSet.Desktop.cs | 44 ++++ .../Workspace/IsolatedAnalyzerReferenceSet.cs | 40 ++++ .../ProjectSystem/ProjectSystemProject.cs | 128 +++++++---- .../ProjectSystemProjectFactory.cs | 38 ++-- ...t.CodeAnalysis.Workspaces.UnitTests.csproj | 6 + ....CodeAnalysis.TestAnalyzerReference.dll.v1 | Bin 0 -> 17408 bytes ....CodeAnalysis.TestAnalyzerReference.dll.v2 | Bin 0 -> 17408 bytes .../SolutionWithSourceGeneratorTests.cs | 81 +++++++ .../Remote/TestSerializerService.cs | 24 +- .../Remote/Core/AbstractAssetProvider.cs | 37 +++- .../RemoteAnalyzerAssemblyLoaderService.cs | 22 ++ .../Host/RemoteWorkspace.SolutionCreator.cs | 31 ++- .../Remote/ServiceHub/Host/RemoteWorkspace.cs | 6 +- ...soft.CodeAnalysis.Remote.ServiceHub.csproj | 7 +- .../RemoteSourceGenerationService.cs | 23 +- 22 files changed, 748 insertions(+), 123 deletions(-) create mode 100644 src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerFileReference.cs create mode 100644 src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Core.cs create mode 100644 src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Desktop.cs create mode 100644 src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.cs create mode 100644 src/Workspaces/CoreTest/Resources/Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v1 create mode 100644 src/Workspaces/CoreTest/Resources/Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v2 create mode 100644 src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoaderService.cs diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs index c921e70de6d6b..4b9c548e08fda 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs @@ -57,7 +57,12 @@ public LanguageServerWorkspaceFactory( public async Task InitializeSolutionLevelAnalyzersAsync(ImmutableArray analyzerPaths) { var references = new List(); - var analyzerLoader = Workspace.Services.GetRequiredService().SharedShadowCopyLoader; + var loaderProvider = Workspace.Services.GetRequiredService(); + + // Load all analyzers into a fresh shadow copied load context. In the future, if we want to support reloading + // of solution-level analyzer references, we should just need to listen for changes to those analyzer paths and + // then call back into this method to update the solution accordingly. + var analyzerLoader = loaderProvider.CreateNewShadowCopyLoader(); foreach (var analyzerPath in analyzerPaths) { diff --git a/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs b/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs index 2aa414a8b4f52..f197b7ef48307 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs @@ -110,7 +110,8 @@ public async Task GetValueAsync(Checksum checksum) public async Task GetSolutionAsync(SolutionAssetStorage.Scope scope) { - var solutionInfo = await new AssetProvider(this).CreateSolutionInfoAsync(scope.SolutionChecksum, CancellationToken.None).ConfigureAwait(false); + var solutionInfo = await new AssetProvider(this).CreateSolutionInfoAsync( + scope.SolutionChecksum, this.Services.SolutionServices, CancellationToken.None).ConfigureAwait(false); var workspace = new AdhocWorkspace(Services.HostServices); return workspace.AddSolution(solutionInfo); diff --git a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs index 75e0de07585db..f84016f18145c 100644 --- a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Serialization; @@ -446,7 +447,10 @@ public async Task TestRemoteWorkspace() await Verify(remoteWorkspace, currentSolution, remoteSolution3); // move to new solution backward - var solutionInfo2 = await assetProvider.CreateSolutionInfoAsync(await solution1.CompilationState.GetChecksumAsync(CancellationToken.None), CancellationToken.None); + var solutionInfo2 = await assetProvider.CreateSolutionInfoAsync( + await solution1.CompilationState.GetChecksumAsync(CancellationToken.None), + remoteWorkspace.Services.SolutionServices, + CancellationToken.None); var solution2 = remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo2); // move to new solution forward diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs index 6ccc3a847cbd5..21a36f1c139a1 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection.Metadata; -using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -54,15 +53,24 @@ private static Checksum CreateChecksum(MetadataReference reference) protected virtual Checksum CreateChecksum(AnalyzerReference reference) { +#if NET + // If we're in the oop side and we're being asked to produce our local checksum (so we can compare it to the + // host checksum), then we want to just defer to the underlying analyzer reference of our isolated reference. + // This underlying reference corresponds to the reference that the host has, and we do not want to make any + // changes as long as they're both in agreement. + if (reference is IsolatedAnalyzerFileReference { UnderlyingAnalyzerFileReference: var underlyingReference }) + reference = underlyingReference; +#endif + using var stream = SerializableBytes.CreateWritableStream(); using (var writer = new ObjectWriter(stream, leaveOpen: true)) { switch (reference) { - case AnalyzerFileReference file: - writer.WriteString(file.FullPath); - writer.WriteGuid(TryGetAnalyzerFileReferenceMvid(file)); + case AnalyzerFileReference fileReference: + writer.WriteString(fileReference.FullPath); + writer.WriteGuid(TryGetAnalyzerFileReferenceMvid(fileReference)); break; case AnalyzerImageReference analyzerImageReference: @@ -109,11 +117,11 @@ protected virtual void WriteAnalyzerReferenceTo(AnalyzerReference reference, Obj { switch (reference) { - case AnalyzerFileReference file: + case AnalyzerFileReference fileReference: writer.WriteString(nameof(AnalyzerFileReference)); - writer.WriteString(file.FullPath); + writer.WriteString(fileReference.FullPath); - // Note: it is intentional that we are not writing the MVID of the analyzer file reference over (even + // Note: it is intentional that we are not writing the MVID of the analyzer file reference over in (even // though we mixed it into the checksum). We don't actually need the data on the other side as it will // be read out from the file itself. So the flow is as follows when an analyzer-file-reference changes: // @@ -150,8 +158,10 @@ protected virtual AnalyzerReference ReadAnalyzerReferenceFrom(ObjectReader reade switch (reader.ReadString()) { case nameof(AnalyzerFileReference): - var fullPath = reader.ReadRequiredString(); - return new AnalyzerFileReference(fullPath, _analyzerLoaderProvider.SharedShadowCopyLoader); + // Rehydrate the analyzer file reference with the simple shared shadow copy loader. Note: we won't + // actually use this instance we create. Instead, the caller will use create an IsolatedAssemblyReferenceSet + // from these to ensure that all the types can be safely loaded into their own ALC. + return new AnalyzerFileReference(reader.ReadRequiredString(), _analyzerLoaderProvider.SharedShadowCopyLoader); case nameof(AnalyzerImageReference): var guid = reader.ReadGuid(); @@ -286,7 +296,7 @@ private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectRe // so that we can put xml doc comment as part of snapshot. but until we believe that is necessary, // it will go with simpler approach var documentProvider = filePath != null && _documentationService != null ? - _documentationService.GetDocumentationProvider(filePath) : XmlDocumentationProvider.Default; + _documentationService.GetDocumentationProvider(filePath) : DocumentationProvider.Default; return new SerializedPortableExecutableReference( properties, filePath, metadata, storageHandles, documentProvider); diff --git a/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IAnalyzerAssemblyLoaderProvider.cs b/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IAnalyzerAssemblyLoaderProvider.cs index 3fea95eb30918..9c5626f1e5fc8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IAnalyzerAssemblyLoaderProvider.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IAnalyzerAssemblyLoaderProvider.cs @@ -9,11 +9,24 @@ using System.IO; using Microsoft.CodeAnalysis.Host.Mef; +#if NET +using Microsoft.CodeAnalysis.Diagnostics; +using System.Runtime.Loader; +#endif + namespace Microsoft.CodeAnalysis.Host; internal interface IAnalyzerAssemblyLoaderProvider : IWorkspaceService { IAnalyzerAssemblyLoaderInternal SharedShadowCopyLoader { get; } + +#if NET + /// + /// Creates a fresh shadow copying loader that will load all s and s in a fresh . + /// + IAnalyzerAssemblyLoaderInternal CreateNewShadowCopyLoader(); +#endif } /// @@ -28,13 +41,13 @@ internal abstract class AbstractAnalyzerAssemblyLoaderProvider : IAnalyzerAssemb public AbstractAnalyzerAssemblyLoaderProvider(IEnumerable externalResolvers) { _externalResolvers = externalResolvers.ToImmutableArray(); - _shadowCopyLoader = new(CreateShadowCopyLoader); + _shadowCopyLoader = new(CreateNewShadowCopyLoader); } public IAnalyzerAssemblyLoaderInternal SharedShadowCopyLoader => _shadowCopyLoader.Value; - private IAnalyzerAssemblyLoaderInternal CreateShadowCopyLoader() + public IAnalyzerAssemblyLoaderInternal CreateNewShadowCopyLoader() => this.WrapLoader(DefaultAnalyzerAssemblyLoader.CreateNonLockingLoader( Path.Combine(Path.GetTempPath(), nameof(Roslyn), "AnalyzerAssemblyLoader"), _externalResolvers)); diff --git a/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerFileReference.cs b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerFileReference.cs new file mode 100644 index 0000000000000..6db69d9a65935 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerFileReference.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NET + +using System; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Runtime.Loader; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis; + +/// +/// Wrapper around a real . An "isolated" analyzer reference is an analyzer +/// reference associated with an that is connected to a set of other "isolated" +/// analyzer references. This allows for loading the analyzers and generators from it in a way that is associated with +/// that load context, keeping them separate from other analyzers and generators loaded in other load contexts, while +/// also allowing all of those instances to be collected when no longer needed. Being isolated means that if any of the +/// underlying assembly references change, that they can be loaded side by side with the prior references. This enables +/// functionality like live reloading of analyzers and generators when they change on disk. Note: this is only +/// supported on .Net Core, and not .Net Framework, as only the former has s. +/// +/// +/// The purpose of this type is to allow passing out a to the rest of the system that +/// then ensures that as long as it is alive (or any or +/// it passes out is alive), that the (and its corresponding ) is kept alive as well. +/// +internal sealed class IsolatedAnalyzerFileReference( + IsolatedAnalyzerReferenceSet isolatedAnalyzerReferenceSet, + AnalyzerFileReference underlyingAnalyzerReference) + : AnalyzerReference +{ + /// + /// Conditional weak tables that ensure that as long as a particular or is alive, that the corresponding (and its + /// corresponding is kept alive. + /// + private static readonly ConditionalWeakTable s_analyzerToPinnedReferenceSet = []; + + /// + private static readonly ConditionalWeakTable s_generatorToPinnedReferenceSet = []; + + /// + /// We keep a strong reference here. As long as this is passed out and + /// held onto (say by a Project instance), it should keep the IsolatedAssemblyReferenceSet (and its ALC) alive. + /// + private readonly IsolatedAnalyzerReferenceSet _isolatedAnalyzerReferenceSet = isolatedAnalyzerReferenceSet; + + /// + /// The actual real we defer our operations to. + /// + public readonly AnalyzerFileReference UnderlyingAnalyzerFileReference = underlyingAnalyzerReference; + + public override string Display => UnderlyingAnalyzerFileReference.Display; + public override string? FullPath => UnderlyingAnalyzerFileReference.FullPath; + public override object Id => UnderlyingAnalyzerFileReference.Id; + + public override ImmutableArray GetAnalyzers(string language) + => PinAnalyzers(static (reference, language) => reference.GetAnalyzers(language), language); + + public override ImmutableArray GetAnalyzersForAllLanguages() + => PinAnalyzers(static (reference, _) => reference.GetAnalyzersForAllLanguages(), default(VoidResult)); + + [Obsolete] + public override ImmutableArray GetGenerators() + => PinGenerators(static (reference, _) => reference.GetGenerators(), default(VoidResult)); + + public override ImmutableArray GetGenerators(string language) + => PinGenerators(static (reference, language) => reference.GetGenerators(language), language); + + public override ImmutableArray GetGeneratorsForAllLanguages() + => PinGenerators(static (reference, _) => reference.GetGeneratorsForAllLanguages(), default(VoidResult)); + + private ImmutableArray PinAnalyzers(Func> getItems, TArg arg) + => PinItems(s_analyzerToPinnedReferenceSet, getItems, arg); + + private ImmutableArray PinGenerators(Func> getItems, TArg arg) + => PinItems(s_generatorToPinnedReferenceSet, getItems, arg); + + private ImmutableArray PinItems( + ConditionalWeakTable table, + Func> getItems, + TArg arg) + where TItem : class + { + // Keep a reference from each generator to the IsolatedAssemblyReferenceSet. This will ensure it (and the ALC + // it points at) stays alive as long as the generator instance stays alive. + var items = getItems(this.UnderlyingAnalyzerFileReference, arg); + + foreach (var item in items) + table.TryAdd(item, _isolatedAnalyzerReferenceSet); + + // Note: we want to keep ourselves alive during this call so that neither we nor our reference set get GC'ed + // while we're computing the items. + GC.KeepAlive(this); + + return items; + } + + public override bool Equals(object? obj) + => ReferenceEquals(this, obj); + + public override int GetHashCode() + => RuntimeHelpers.GetHashCode(this); + + public override string ToString() + => $"{nameof(IsolatedAnalyzerFileReference)}({UnderlyingAnalyzerFileReference})"; +} + +#endif diff --git a/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Core.cs b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Core.cs new file mode 100644 index 0000000000000..88268396e9120 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Core.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NET + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; +using Microsoft.CodeAnalysis.Serialization; +using System.Runtime.Loader; + +namespace Microsoft.CodeAnalysis; + +/// +/// A set of s and their associated shadow copy loader (which has its own +/// ). As long as something is keeping this set alive, the ALC will be kept alive. +/// Once this set is dropped, the loader will be explicitly 'd in its finalizer. +/// +internal sealed partial class IsolatedAnalyzerReferenceSet +{ + /// + /// Gate around to ensure it is only accessed and updated atomically. + /// + private static readonly SemaphoreSlim s_isolatedReferenceSetGate = new(initialCount: 1); + + /// + /// Mapping from checksum for a particular set of assembly references, to the dedicated ALC and actual assembly + /// references corresponding to it. As long as it is alive, we will try to reuse what is in memory. But once it is + /// dropped from memory, we'll clean things up and produce a new one. + /// + private static readonly Dictionary> s_checksumToReferenceSet = []; + + private static int s_sweepCount = 0; + + /// + /// Final set of instances that will be passed through the workspace down to the compiler. + /// + public ImmutableArray AnalyzerReferences { get; } + + /// + /// Dedicated loader with its own dedicated ALC that all analyzer references will load their s within. + /// + private readonly IAnalyzerAssemblyLoaderInternal _shadowCopyLoader; + + private IsolatedAnalyzerReferenceSet( + ImmutableArray initialReferences, + IAnalyzerAssemblyLoaderProvider provider) + { + // Now make a fresh loader that uses that ALC that will ensure these references are properly isolated. + _shadowCopyLoader = provider.CreateNewShadowCopyLoader(); + + var builder = new FixedSizeArrayBuilder(initialReferences.Length); + foreach (var initialReference in initialReferences) + { + // If we already have an analyzer reference isolated to another ALC. Fish out its underlying reference so + // we can rewrap it for the new ALC we're creating. We don't want to continually wrap layers of isolated + // objects. + var analyzerReference = initialReference is IsolatedAnalyzerFileReference isolatedReference + ? isolatedReference.UnderlyingAnalyzerFileReference + : initialReference; + + // If we have an existing file reference, make a new one with a different loader/ALC. Otherwise, it's some + // other analyzer reference we don't understand (like an in-memory one created in tests). + var finalReference = analyzerReference is AnalyzerFileReference analyzerFileReference + ? new IsolatedAnalyzerFileReference(this, new AnalyzerFileReference(analyzerFileReference.FullPath, _shadowCopyLoader)) + : initialReference; + + builder.Add(finalReference); + } + + this.AnalyzerReferences = builder.MoveToImmutable(); + } + + /// + /// When the last reference this to this reference set finally goes away, it is safe to unload our loader+ALC. + /// + ~IsolatedAnalyzerReferenceSet() + { + _shadowCopyLoader.Dispose(); + } + + private static void GarbageCollectReleaseReferences_NoLock() + { + Contract.ThrowIfTrue(s_isolatedReferenceSetGate.CurrentCount != 0); + + // When we've done some reasonable number of mutations to the dictionary, we'll do a sweep to see if there are + // entries we can remove. + // + // Note: the value 128 was chosen with absolutely no data. It was to avoid doing linear sweeps on every change, + // while also still running reasonably often to clear out old entries. + // + // Note: clearing out entries isn't critical. It's really just a KeyValuePair. So + // they aren't really large at all. But it seemed nice to ensure that the dictionary doesn't grow in an + // unbounded fashion, even if the entries are small. + if (++s_sweepCount % 128 == 0) + return; + + using var _ = ArrayBuilder.GetInstance(out var checksumsToRemove); + + foreach (var (checksum, weakReference) in s_checksumToReferenceSet) + { + if (!weakReference.TryGetTarget(out var referenceSet) || + referenceSet is null) + { + checksumsToRemove.Add(checksum); + } + } + + foreach (var checksum in checksumsToRemove) + s_checksumToReferenceSet.Remove(checksum); + } + + public static async partial ValueTask> CreateIsolatedAnalyzerReferencesAsync( + bool useAsync, + ImmutableArray references, + SolutionServices solutionServices, + CancellationToken cancellationToken) + { + if (references.Length == 0) + return []; + + var serializerService = solutionServices.GetRequiredService(); + var analyzerChecksums = ChecksumCache.GetOrCreateChecksumCollection(references, serializerService, cancellationToken); + + return await CreateIsolatedAnalyzerReferencesAsync( + useAsync, + analyzerChecksums, + solutionServices, + () => Task.FromResult(references), + cancellationToken).ConfigureAwait(false); + } + + public static async partial ValueTask> CreateIsolatedAnalyzerReferencesAsync( + bool useAsync, + ChecksumCollection analyzerChecksums, + SolutionServices solutionServices, + Func>> getReferencesAsync, + CancellationToken cancellationToken) + { + if (analyzerChecksums.Children.Length == 0) + return []; + + var checksum = analyzerChecksums.Checksum; + + // Note: this method will end up fetching or creating an IsolatedAssemblyReferenceSet for this checksum. + // We'll then return the AnalyzerReferences from within it. These AnalyzerReferences (which will normally all + // be IsolatedAnalyzerFileReferences) will themselves root the IsolatedAssemblyReferenceSet, as will all the + // DiagnosticAnalyzers and ISourceGenerators returned down the line from the IsolatedAnalyzerFileReferences. + + // First, see if these were already computed and stored. + using (useAsync + ? await s_isolatedReferenceSetGate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false) + : s_isolatedReferenceSetGate.DisposableWait(cancellationToken)) + { + if (s_checksumToReferenceSet.TryGetValue(checksum, out var weakIsolatedReferenceSet) && + weakIsolatedReferenceSet.TryGetTarget(out var isolatedAssemblyReferenceSet)) + { + return isolatedAssemblyReferenceSet.AnalyzerReferences; + } + } + + // Not already stored. Fetch the actual references. + var analyzerReferences = await getReferencesAsync().ConfigureAwait(false); + var assemblyLoaderProvider = solutionServices.GetRequiredService(); + + using (useAsync + ? await s_isolatedReferenceSetGate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false) + : s_isolatedReferenceSetGate.DisposableWait(cancellationToken)) + { + // Check again to see if another thread beat us. + if (s_checksumToReferenceSet.TryGetValue(checksum, out var weakIsolatedReferenceSet) && + weakIsolatedReferenceSet.TryGetTarget(out var isolatedAssemblyReferenceSet)) + { + return isolatedAssemblyReferenceSet.AnalyzerReferences; + } + + isolatedAssemblyReferenceSet = new IsolatedAnalyzerReferenceSet(analyzerReferences, assemblyLoaderProvider); + + if (weakIsolatedReferenceSet is null) + { + // If we don't have a weak reference yet, make it and add to the dictionary. + weakIsolatedReferenceSet = new(isolatedAssemblyReferenceSet); + s_checksumToReferenceSet[checksum] = weakIsolatedReferenceSet; + } + else + { + // Otherwise, update the empty weak reference to point at the newly created set. + weakIsolatedReferenceSet.SetTarget(isolatedAssemblyReferenceSet); + } + + // Do some cleaning up of old dictionary entries that are no longer in use. + GarbageCollectReleaseReferences_NoLock(); + + return isolatedAssemblyReferenceSet.AnalyzerReferences; + } + } +} + +#endif diff --git a/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Desktop.cs b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Desktop.cs new file mode 100644 index 0000000000000..c37ad98b42d90 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.Desktop.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !NET + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Serialization; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis; + +/// +/// Basic no-op impl on .Net Framework. We can't actually isolate anything in .Net Framework, so we just return the +/// assembly references as is. +/// +internal sealed partial class IsolatedAnalyzerReferenceSet +{ + public static partial ValueTask> CreateIsolatedAnalyzerReferencesAsync( + bool useAsync, + ImmutableArray references, + SolutionServices solutionServices, + CancellationToken cancellationToken) + { + return ValueTaskFactory.FromResult(references); + } + + public static async partial ValueTask> CreateIsolatedAnalyzerReferencesAsync( + bool useAsync, + ChecksumCollection analyzerChecksums, + SolutionServices solutionServices, + Func>> getReferencesAsync, + CancellationToken cancellationToken) + { + return await getReferencesAsync().ConfigureAwait(false); + } +} + +#endif diff --git a/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.cs b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.cs new file mode 100644 index 0000000000000..3d516609709a2 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/IsolatedAnalyzerReferenceSet.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Serialization; + +namespace Microsoft.CodeAnalysis; + +internal sealed partial class IsolatedAnalyzerReferenceSet +{ + /// + /// Given a set of analyzer references, attempts to return a new set that is in an isolated AssemblyLoadContext so + /// that the analyzers and generators from it can be safely loaded side-by-side with prior versions of the same + /// references that may already be loaded. + /// + public static partial ValueTask> CreateIsolatedAnalyzerReferencesAsync( + bool useAsync, + ImmutableArray references, + SolutionServices solutionServices, + CancellationToken cancellationToken); + + /// + /// Given a checksum for a set of analyzer references, fetches the existing ALC-isolated set of them if already + /// present in this process. Otherwise, this fetches the raw serialized analyzer references from the host side, + /// then creates and caches an isolated set on the OOP side to hold onto them, passing out that isolated set of + /// references to be used by the caller (normally to be stored in a solution snapshot). + /// + public static partial ValueTask> CreateIsolatedAnalyzerReferencesAsync( + bool useAsync, + ChecksumCollection analyzerChecksums, + SolutionServices solutionServices, + Func>> getReferencesAsync, + CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index b963a26744eaa..54d2ef67dec96 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.ProjectSystem; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -33,7 +34,6 @@ internal sealed partial class ProjectSystemProject private readonly ProjectSystemProjectFactory _projectSystemProjectFactory; private readonly ProjectSystemHostInfo _hostInfo; - private readonly IAnalyzerAssemblyLoader _analyzerAssemblyLoader; /// /// A semaphore taken for all mutation of any mutable field in this type. @@ -166,11 +166,6 @@ internal ProjectSystemProject( Language = language; _displayName = displayName; - var provider = _projectSystemProjectFactory.SolutionServices.GetRequiredService(); - // NOTE: The provider will always return the same singleton, shadow copying, analyzer loader instance, which is - // important to ensure that analyzer dependencies are correctly loaded. - _analyzerAssemblyLoader = provider.SharedShadowCopyLoader; - _sourceFiles = new BatchingDocumentCollection( this, documentAlreadyInWorkspace: (s, d) => s.ContainsDocument(d), @@ -593,7 +588,7 @@ await _projectSystemProjectFactory.ApplyBatchChangeToWorkspaceMaybeAsync(useAsyn Id, solutionChanges, _projectReferencesRemovedInBatch, _projectReferencesAddedInBatch); projectUpdateState = UpdateAnalyzerReferences( - projectBeforeMutations, solutionChanges, projectUpdateState, _analyzersRemovedInBatch, _analyzersAddedInBatch); + Id, solutionChanges, projectUpdateState, _analyzersRemovedInBatch, _analyzersAddedInBatch); // Other property modifications... foreach (var propertyModification in _projectPropertyModificationsInBatch) @@ -619,7 +614,6 @@ await _projectSystemProjectFactory.ApplyBatchChangeToWorkspaceMaybeAsync(useAsyn ClearAndZeroCapacity(_analyzersRemovedInBatch); ClearAndZeroCapacity(_projectPropertyModificationsInBatch); - }).ConfigureAwait(false); foreach (var (documentId, textContainer) in documentsToOpen) @@ -724,44 +718,84 @@ static void UpdateProjectReferences( } static ProjectUpdateState UpdateAnalyzerReferences( - Project projectBeforeMutation, + ProjectId projectId, SolutionChangeAccumulator solutionChanges, ProjectUpdateState projectUpdateState, List analyzersRemovedInBatch, List analyzersAddedInBatch) { - var projectId = projectBeforeMutation.Id; + if (analyzersRemovedInBatch.Count == 0 && analyzersAddedInBatch.Count == 0) + return projectUpdateState; - // Analyzer reference removing... - if (analyzersRemovedInBatch.Count > 0) - { - projectUpdateState = projectUpdateState.WithIncrementalAnalyzerReferencesRemoved(analyzersRemovedInBatch); + // Use shared helper to figure out the new forked state. + var (newSolution, newProjectUpdateState) = UpdateProjectAnalyzerReferences( + solutionChanges.Solution, projectId, projectUpdateState, analyzersRemovedInBatch, analyzersAddedInBatch); - foreach (var analyzerReferenceFullPath in analyzersRemovedInBatch) - { - solutionChanges.UpdateSolutionForProjectAction( - projectId, - solutionChanges.Solution.RemoveAnalyzerReference( - projectId, projectBeforeMutation.AnalyzerReferences.First(a => a.FullPath == analyzerReferenceFullPath))); - } - } + solutionChanges.UpdateSolutionForProjectAction(projectId, newSolution); - // Analyzer reference adding... - if (analyzersAddedInBatch.Count > 0) - { - projectUpdateState = projectUpdateState.WithIncrementalAnalyzerReferencesAdded(analyzersAddedInBatch); + return newProjectUpdateState; + } + } - var loaderProvider = solutionChanges.Solution.Services.GetRequiredService(); - var shadowCopyLoader = loaderProvider.SharedShadowCopyLoader; + public static (Solution newSolution, ProjectUpdateState newProjectUpdateState) UpdateProjectAnalyzerReferences( + Solution solution, + ProjectId projectId, + ProjectUpdateState projectUpdateState, + List analyzersRemoved, + List analyzersAdded) + { + Contract.ThrowIfTrue(analyzersRemoved.Count == 0 && analyzersAdded.Count == 0, "Should only be called when there is work to do"); - solutionChanges.UpdateSolutionForProjectAction( - projectId, - solutionChanges.Solution.AddAnalyzerReferences(projectId, - analyzersAddedInBatch.Select(fullPath => new AnalyzerFileReference(fullPath, shadowCopyLoader)))); - } + // NOTE: Create the initial AnalyzerFileReferences for the analyzers we're adding with a shared shadow copy + // loader. This is fine as we're just creating these to pass into CreateIsolatedAnalyzerReferencesAsync which + // will properly give them an isolated ALC to use instead. + var assemblyLoaderProvider = solution.Services.GetRequiredService(); + var sharedShadowCopyLoader = assemblyLoaderProvider.SharedShadowCopyLoader; - return projectUpdateState; + var project = solution.GetRequiredProject(projectId); + + using var _ = ArrayBuilder.GetInstance(out var initialReferenceList); + + // Keep around all the project's analyzers that were not removed. + foreach (var analyzerReference in project.AnalyzerReferences) + { + // Skip any existing analyzer references we're removing. + if (analyzersRemoved.Contains(analyzerReference.FullPath!)) + continue; + +#if NET + // In .Net Core, we must have IsolatedAnalyzerFileReferences for all analyzers. + initialReferenceList.Add(((IsolatedAnalyzerFileReference)analyzerReference).UnderlyingAnalyzerFileReference); +#else + // In .NET Framework, we must have AnalyzerFileReferences for all analyzers. + initialReferenceList.Add((AnalyzerFileReference)analyzerReference); +#endif } + + // Now, create an initial analyzer file reference for all the analyzers being added. + foreach (var analyzer in analyzersAdded) + initialReferenceList.Add(new AnalyzerFileReference(analyzer, sharedShadowCopyLoader)); + + // We are only updating this state object so that we can ensure we unregister any file watchers for + // analyzers that are removed, and register new watches for analyzers that are added. Note that those file + // watchers are based on file path only. So it's ok if the analyzer references added here are not + // necessarily the exact same ones given to the solution itself. + var newProjectUpdateState = projectUpdateState + .WithIncrementalAnalyzerReferencesRemoved(analyzersRemoved) + .WithIncrementalAnalyzerReferencesAdded(analyzersAdded); + + // Attempt to isolate these analyzer references into their own ALC so that we can still load + // analyzers/generators from them if they changed on disk. + var isolatedReferences = IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( + useAsync: false, + ImmutableArray.CastUp(initialReferenceList.ToImmutableAndClear()), + solution.Services, + CancellationToken.None).VerifyCompleted(); + + // Fork the solution's project with these new isolated analyzer references. And return the forked solution and + // forked projectUpdateState back to the caller to handle them. + var newSolution = solution.WithProjectAnalyzerReferences(project.Id, isolatedReferences); + return (newSolution, newProjectUpdateState); } #endregion @@ -985,17 +1019,10 @@ public void AddAnalyzerReference(string fullPath) { // Are we adding one we just recently removed? If so, we can just keep using that one, and avoid // removing it once we apply the batch - var analyzerPendingRemoval = _analyzersRemovedInBatch.FirstOrDefault(fullPath => fullPath == mappedFullPath); _projectAnalyzerPaths.Add(mappedFullPath); - if (analyzerPendingRemoval != null) - { - _analyzersRemovedInBatch.Remove(analyzerPendingRemoval); - } - else - { + if (!_analyzersRemovedInBatch.Remove(mappedFullPath)) _analyzersAddedInBatch.Add(mappedFullPath); - } } } } @@ -1256,7 +1283,8 @@ public void RemoveFromWorkspace() _documentFileChangeContext.Dispose(); - IReadOnlyList? remainingMetadataReferences = null; + IReadOnlyList? originalMetadataReferences = null; + IReadOnlyList? originalAnalyzerReferences = null; _projectSystemProjectFactory.ApplyChangeToWorkspace(w => { @@ -1264,7 +1292,11 @@ public void RemoveFromWorkspace() // as another project being removed at the same time could result in project to project // references being converted to metadata references (or vice versa) and we might either // miss stopping a file watcher or might end up double-stopping a file watcher. - remainingMetadataReferences = w.CurrentSolution.GetRequiredProject(Id).MetadataReferences; + var project = w.CurrentSolution.GetRequiredProject(Id); + + originalMetadataReferences = project.MetadataReferences; + originalAnalyzerReferences = project.AnalyzerReferences; + _projectSystemProjectFactory.RemoveProjectFromTrackingMaps_NoLock(Id); // If this is our last project, clear the entire solution. @@ -1278,10 +1310,14 @@ public void RemoveFromWorkspace() } }); - Contract.ThrowIfNull(remainingMetadataReferences); + Contract.ThrowIfNull(originalMetadataReferences); + Contract.ThrowIfNull(originalAnalyzerReferences); - foreach (var reference in remainingMetadataReferences.OfType()) + foreach (var reference in originalMetadataReferences.OfType()) _projectSystemProjectFactory.FileWatchedPortableExecutableReferenceFactory.StopWatchingReference(reference.FilePath!, referenceToTrack: reference); + + foreach (var reference in originalAnalyzerReferences) + _projectSystemProjectFactory.FileWatchedAnalyzerReferenceFactory.StopWatchingReference(reference.FullPath!, referenceToTrack: reference); } public void ReorderSourceFiles(ImmutableArray filePaths) diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs index 0981fffb29976..b13bc77fbfa10 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs @@ -44,7 +44,7 @@ internal sealed partial class ProjectSystemProjectFactory public IFileChangeWatcher FileChangeWatcher { get; } public FileWatchedReferenceFactory FileWatchedPortableExecutableReferenceFactory { get; } - public FileWatchedReferenceFactory FileWatchedAnalyzerReferenceFactory { get; } + public FileWatchedReferenceFactory FileWatchedAnalyzerReferenceFactory { get; } public SolutionServices SolutionServices => this.Workspace.Services.SolutionServices; @@ -835,27 +835,18 @@ private Task StartRefreshingAnalyzerReferenceForFileAsync(string fullFilePath, C => StartRefreshingReferencesForFileAsync( fullFilePath, getReferences: static project => project.AnalyzerReferences.Select(r => r.FullPath!), - getFilePath: static fullPath => fullPath, - createNewReference: static (_, fullPath) => fullPath, - update: static (solution, projectId, projectUpdateState, oldReferenceFullPath, newReferenceFullPath) => + getFilePath: static filePath => filePath, + createNewReference: static (_, filePath) => filePath, + update: static (solution, projectId, projectUpdateState, oldAnalyzerFilePath, newAnalyzerFilePath) => { - // it's expected that the old and new paths are the same here. The idea is that we changed a file on - // disk, so of course the path will be the same. - Contract.ThrowIfTrue(oldReferenceFullPath != newReferenceFullPath); - - var assemblyLoaderProvider = solution.Services.GetRequiredService(); - - var project = solution.GetRequiredProject(projectId); - var oldAnalyzerReference = project.AnalyzerReferences.First(r => r.FullPath == oldReferenceFullPath); - var newAnalyzerReference = new AnalyzerFileReference(oldReferenceFullPath, assemblyLoaderProvider.SharedShadowCopyLoader); - - var newSolution = solution - .RemoveAnalyzerReference(projectId, oldAnalyzerReference) - .AddAnalyzerReference(projectId, newAnalyzerReference); - var newProjectUpdateState = projectUpdateState - .WithIncrementalAnalyzerReferenceRemoved(oldReferenceFullPath) - .WithIncrementalAnalyzerReferenceAdded(newReferenceFullPath); - + // Note: we're passing in the same path for the analyzers to remove/add. That's exactly the intent + // here. We're updating an existing analyzer in place. The call to UpdateProjectAnalyzerReferences will + // preserve all the other analyzers (with a different path), remove the one with this path, make a new + // analyzer for this path, and then created an isolated ALC to load them all in. + Contract.ThrowIfTrue(oldAnalyzerFilePath != newAnalyzerFilePath); + + var (newSolution, newProjectUpdateState) = ProjectSystemProject.UpdateProjectAnalyzerReferences( + solution, projectId, projectUpdateState, [oldAnalyzerFilePath], [newAnalyzerFilePath]); return (newSolution, newProjectUpdateState); }, cancellationToken); @@ -890,10 +881,9 @@ await ApplyBatchChangeToWorkspaceAsync((solutionChanges, projectUpdateState) => if (fullFilePath.Equals(getFilePath(oldReference), StringComparison.OrdinalIgnoreCase)) { - var newReference = createNewReference(solutionServices, oldReference); - var newSolution = solutionChanges.Solution; - (newSolution, projectUpdateState) = update(newSolution, project.Id, projectUpdateState, oldReference, newReference); + (newSolution, projectUpdateState) = update( + newSolution, project.Id, projectUpdateState, oldReference, createNewReference(solutionServices, oldReference)); solutionChanges.UpdateSolutionForProjectAction(project.Id, newSolution); } diff --git a/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj b/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj index e6d89e9db00e0..b5b2a51d4a127 100644 --- a/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj +++ b/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj @@ -6,10 +6,16 @@ $(NetRoslyn);net472 Microsoft.CodeAnalysis.UnitTests + + + + + + diff --git a/src/Workspaces/CoreTest/Resources/Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v1 b/src/Workspaces/CoreTest/Resources/Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v1 new file mode 100644 index 0000000000000000000000000000000000000000..ee7ccd6c6d156d4fee71c555a7cac0ffe67113af GIT binary patch literal 17408 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 literal 0 HcmV?d00001 diff --git a/src/Workspaces/CoreTest/Resources/Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v2 b/src/Workspaces/CoreTest/Resources/Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v2 new file mode 100644 index 0000000000000000000000000000000000000000..67e9c0527bbcc5ceeee3bf2d622788d3cffafc11 GIT binary patch literal 17408 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 Xzv_X&o^OYJt^cbP|5yJ12M_!oPa+v= literal 0 HcmV?d00001 diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index b686a59887b39..00343b9845a23 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Immutable; +using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; @@ -924,4 +927,82 @@ public async Task TestChangingGeneratorChangesChecksum(TestHost testHost) Assert.NotEqual(checksum1, checksum3); Assert.NotEqual(checksum2, checksum3); } + +#if NET + + private sealed class DoNotLoadAssemblyLoader : IAnalyzerAssemblyLoader + { + public static readonly IAnalyzerAssemblyLoader Instance = new DoNotLoadAssemblyLoader(); + + public void AddDependencyLocation(string fullPath) + { + } + + public Assembly LoadFromPath(string fullPath) + => throw new InvalidOperationException("These tests should not be loading analyzer assemblies in those host workspace, only in the remote one."); + } + + [Fact] + public async Task UpdatingAnalyzerReferenceReloadsGenerators() + { + // We have two versions of the same source generator attached to this project as a resource. Each creates a + // 'HelloWorld' class, just with a different string it emits inside. + const string AnalyzerResourceV1 = @"Microsoft.CodeAnalysis.UnitTests.Resources.Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v1"; + const string AnalyzerResourceV2 = @"Microsoft.CodeAnalysis.UnitTests.Resources.Microsoft.CodeAnalysis.TestAnalyzerReference.dll.v2"; + + using var workspace = CreateWorkspace(testHost: TestHost.OutOfProcess); + var solution = workspace.CurrentSolution; + + var project1 = solution.AddProject("P1", "P1", LanguageNames.CSharp); + + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + + var analyzerPath = Path.Combine(tempDirectory.Path, "Microsoft.CodeAnalysis.TestAnalyzerReference.dll"); + + var analyzerAssemblyLoaderProvider = workspace.Services.GetRequiredService(); + + // Add and test the v1 generator first. + { + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(AnalyzerResourceV1)) + using (var destination = File.OpenWrite(analyzerPath)) + { + stream!.CopyTo(destination); + } + + // Pass in an always throwing assembly loader so we can be sure that no loading happens on the host side. + project1 = project1.WithAnalyzerReferences([new AnalyzerFileReference(analyzerPath, DoNotLoadAssemblyLoader.Instance)]); + + var generatedDocuments = await project1.GetSourceGeneratedDocumentsAsync(); + var helloWorldDoc = generatedDocuments.Single(d => d.Name == "HelloWorld.cs"); + + var contents = await helloWorldDoc.GetTextAsync(); + Assert.True(contents.ToString().Contains("Hello, World 1!")); + } + + // Now, overwrite the analyzer reference with a new version that generates different contents + { + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(AnalyzerResourceV2)) + using (var destination = File.OpenWrite(analyzerPath)) + { + stream!.CopyTo(destination); + } + + // Make a new analyzer reference to that location (note: with the same throwing assembly loader). on the + // host side, this will simply instantiate a new reference. But this will cause all the machinery to run + // syncing this new reference to the oop side, which will load the analyzer reference in a dedicated ALC. + project1 = project1.WithAnalyzerReferences([new AnalyzerFileReference(analyzerPath, DoNotLoadAssemblyLoader.Instance)]); + + var generatedDocuments = await project1.GetSourceGeneratedDocumentsAsync(); + var helloWorldDoc = generatedDocuments.Single(d => d.Name == "HelloWorld.cs"); + + // Note that the contents are now different than what we saw before. This is with an analyzer at the same path, + // with the same assembly name and type name for the generator. Because there is a dedicated ALC, this reloads + // fine. + var contents = await helloWorldDoc.GetTextAsync(); + Assert.True(contents.ToString().Contains("Hello, World 2!")); + } + } + +#endif } diff --git a/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs b/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs index 886244601c930..95acabccf026a 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs @@ -63,12 +63,23 @@ protected override MetadataReference ReadMetadataReferenceFrom(ObjectReader read : base.ReadMetadataReferenceFrom(reader); protected override Checksum CreateChecksum(AnalyzerReference reference) - => reference switch + { +#if NET + // If we're in the oop side and we're being asked to produce our local checksum (so we can compare it to the + // host checksum), then we want to just defer to the underlying analyzer reference of our isolated reference. + // This underlying reference corresponds to the reference that the host has, and we do not want to make any + // changes as long as they're both in agreement. + if (reference is IsolatedAnalyzerFileReference { UnderlyingAnalyzerFileReference: var underlyingReference }) + reference = underlyingReference; +#endif + + return reference switch { TestGeneratorReference generatorReference => generatorReference.Checksum, TestAnalyzerReferenceByLanguage analyzerReferenceByLanguage => analyzerReferenceByLanguage.Checksum, _ => base.CreateChecksum(reference) }; + } protected override void WriteAnalyzerReferenceTo(AnalyzerReference reference, ObjectWriter writer) { @@ -135,11 +146,12 @@ public ConcurrentDictionary SharedTestGeneratorRef { lock (_gate) { - // If we're already being assigned the same set of references as before, we're fine as that won't change anything. - // Ideally, every time we created a new RemoteWorkspace we'd have a new MEF container; this would ensure that - // the assignment earlier before we create the RemoteWorkspace was always the first assignment. However the - // ExportProviderCache.cs in our unit tests hands out the same MEF container multpile times instead of implementing the expected - // contract. See https://github.com/dotnet/roslyn/issues/25863 for further details. + // If we're already being assigned the same set of references as before, we're fine as that + // won't change anything. Ideally, every time we created a new RemoteWorkspace we'd have a new + // MEF container; this would ensure that the assignment earlier before we create the + // RemoteWorkspace was always the first assignment. However the ExportProviderCache.cs in our + // unit tests hands out the same MEF container multiple times instead of implementing the + // expected contract. See https://github.com/dotnet/roslyn/issues/25863 for further details. Contract.ThrowIfFalse(_sharedTestGeneratorReferences == null || _sharedTestGeneratorReferences == value, "We already have a shared set of references, we shouldn't be getting another one."); _sharedTestGeneratorReferences = value; diff --git a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs index 6e619039fe958..0c480adc03c3a 100644 --- a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs +++ b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Serialization; using Roslyn.Utilities; @@ -26,7 +27,10 @@ internal abstract class AbstractAssetProvider public abstract ValueTask GetAssetAsync(AssetPath assetPath, Checksum checksum, CancellationToken cancellationToken); public abstract Task GetAssetsAsync(AssetPath assetPath, HashSet checksums, Action? callback, TArg? arg, CancellationToken cancellationToken); - public async Task CreateSolutionInfoAsync(Checksum solutionChecksum, CancellationToken cancellationToken) + public async Task CreateSolutionInfoAsync( + Checksum solutionChecksum, + SolutionServices solutionServices, + CancellationToken cancellationToken) { var solutionCompilationChecksums = await GetAssetAsync(AssetPathKind.SolutionCompilationStateChecksums, solutionChecksum, cancellationToken).ConfigureAwait(false); var solutionChecksums = await GetAssetAsync(AssetPathKind.SolutionStateChecksums, solutionCompilationChecksums.SolutionState, cancellationToken).ConfigureAwait(false); @@ -40,11 +44,20 @@ public async Task CreateSolutionInfoAsync(Checksum solutionChecksu await this.GetAssetHelper().GetAssetsAsync( AssetPathKind.ProjectStateChecksums, solutionChecksums.Projects.Checksums, - static (_, projectStateChecksums, tuple) => tuple.projectsTasks.Add(tuple.@this.CreateProjectInfoAsync(projectStateChecksums, tuple.cancellationToken)), - (@this: this, projectsTasks, cancellationToken), + static (_, projectStateChecksums, args) => + { + var (@this, projectsTasks, solutionServices, cancellationToken) = args; + projectsTasks.Add(@this.CreateProjectInfoAsync(projectStateChecksums, solutionServices, cancellationToken)); + }, + (@this: this, projectsTasks, solutionServices, cancellationToken), cancellationToken).ConfigureAwait(false); - var analyzerReferences = await this.GetAssetsArrayAsync(AssetPathKind.SolutionAnalyzerReferences, solutionChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false); + // Deserialize the analyzer references, then wrap them in a new isolated analyzer reference set that has its own ALC + var analyzerReference = await this.GetAssetsArrayAsync( + AssetPathKind.SolutionAnalyzerReferences, solutionChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false); + var isolatedAnalyzerReferences = await IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( + useAsync: true, analyzerReference, solutionServices, cancellationToken).ConfigureAwait(false); + var fallbackAnalyzerOptions = await GetAssetAsync>(AssetPathKind.SolutionFallbackAnalyzerOptions, solutionChecksums.FallbackAnalyzerOptions, cancellationToken).ConfigureAwait(false); // Fetch the projects in parallel. @@ -54,11 +67,14 @@ await this.GetAssetHelper().GetAssetsAsync( solutionAttributes.Version, solutionAttributes.FilePath, ImmutableCollectionsMarshal.AsImmutableArray(projects), - analyzerReferences, + isolatedAnalyzerReferences, fallbackAnalyzerOptions).WithTelemetryId(solutionAttributes.TelemetryId); } - public async Task CreateProjectInfoAsync(ProjectStateChecksums projectChecksums, CancellationToken cancellationToken) + public async Task CreateProjectInfoAsync( + ProjectStateChecksums projectChecksums, + SolutionServices solutionServices, + CancellationToken cancellationToken) { await Task.Yield(); @@ -83,6 +99,13 @@ public async Task CreateProjectInfoAsync(ProjectStateChecksums proj var additionalDocumentInfosTask = CreateDocumentInfosAsync(projectChecksums.AdditionalDocuments); var analyzerConfigDocumentInfosTask = CreateDocumentInfosAsync(projectChecksums.AnalyzerConfigDocuments); + // Deserialize the analyzer references, then wrap them in a new isolated analyzer reference set that has its own ALC. + var isolatedAnalyzerReferencesTask = IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( + useAsync: true, + await analyzerReferencesTask.ConfigureAwait(false), + solutionServices, + cancellationToken); + return ProjectInfo.Create( attributes, compilationOptions, @@ -90,7 +113,7 @@ await parseOptionsTask.ConfigureAwait(false), await documentInfosTask.ConfigureAwait(false), await projectReferencesTask.ConfigureAwait(false), await metadataReferencesTask.ConfigureAwait(false), - await analyzerReferencesTask.ConfigureAwait(false), + await isolatedAnalyzerReferencesTask.ConfigureAwait(false), await additionalDocumentInfosTask.ConfigureAwait(false), await analyzerConfigDocumentInfosTask.ConfigureAwait(false), hostObjectType: null); // TODO: https://github.com/dotnet/roslyn/issues/62804 diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoaderService.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoaderService.cs new file mode 100644 index 0000000000000..f6e503cb7ba66 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoaderService.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Remote.Diagnostics; + +/// +/// Customizes the path where to store shadow-copies of analyzer assemblies. +/// +[ExportWorkspaceService(typeof(IAnalyzerAssemblyLoaderProvider), [WorkspaceKind.RemoteWorkspace]), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RemoteAnalyzerAssemblyLoaderService( + [ImportMany] IEnumerable externalResolvers) + : AbstractAnalyzerAssemblyLoaderProvider(externalResolvers.ToImmutableArray()); diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs index 694cd7b67f5b5..83afec59d18ea 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs @@ -28,9 +28,7 @@ internal partial class RemoteWorkspace /// private readonly struct SolutionCreator(RemoteWorkspace workspace, AssetProvider assetService, Solution baseSolution) { -#pragma warning disable IDE0052 // used only in DEBUG builds private readonly RemoteWorkspace _workspace = workspace; -#pragma warning restore private readonly AssetProvider _assetProvider = assetService; private readonly Solution _baseSolution = baseSolution; @@ -70,8 +68,14 @@ public async Task CreateSolutionAsync(Checksum newSolutionChecksum, Ca if (oldSolutionChecksums.AnalyzerReferences.Checksum != newSolutionChecksums.AnalyzerReferences.Checksum) { - solution = solution.WithAnalyzerReferences(await _assetProvider.GetAssetsArrayAsync( - AssetPathKind.SolutionAnalyzerReferences, newSolutionChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false)); + // Take the new set of references we've gotten and create a dedicated set of AnalyzerReferences with + // their own ALC that they can cleanly load (and unload) from. + var deserializedAnalyzerReferences = await _assetProvider.GetAssetsArrayAsync( + AssetPathKind.SolutionAnalyzerReferences, newSolutionChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false); + var isolatedAnalyzerReferences = await IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( + useAsync: true, deserializedAnalyzerReferences, _workspace.Services.SolutionServices, cancellationToken).ConfigureAwait(false); + + solution = solution.WithAnalyzerReferences(isolatedAnalyzerReferences); } if (oldSolutionChecksums.FallbackAnalyzerOptions != newSolutionChecksums.FallbackAnalyzerOptions) @@ -275,7 +279,8 @@ await _assetProvider.GetAssetsAsync( { // Now make a ProjectInfo corresponding to the new project checksums. This should be fast due // to the bulk sync we just performed above. - var projectInfo = await _assetProvider.CreateProjectInfoAsync(newProjectChecksums, cancellationToken).ConfigureAwait(false); + var projectInfo = await _assetProvider.CreateProjectInfoAsync( + newProjectChecksums, solution.Services, cancellationToken).ConfigureAwait(false); projectInfos.Add(projectInfo); } } @@ -371,11 +376,18 @@ await _assetProvider.GetAssetAsync( // changed analyzer references if (oldProjectChecksums.AnalyzerReferences.Checksum != newProjectChecksums.AnalyzerReferences.Checksum) { - project = project.WithAnalyzerReferences(await _assetProvider.GetAssetsArrayAsync( - assetPath: project.Id, newProjectChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false)); + // Take the new set of references we've gotten and create a dedicated set of AnalyzerReferences with + // their own ALC that they can cleanly load (and unload) from. + var deserializedAnalyzerReferences = await _assetProvider.GetAssetsArrayAsync( + assetPath: project.Id, newProjectChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false); + + var isolatedAnalyzerReferences = await IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( + useAsync: true, deserializedAnalyzerReferences, _workspace.Services.SolutionServices, cancellationToken).ConfigureAwait(false); + + project = project.WithAnalyzerReferences(isolatedAnalyzerReferences); } - // changed analyzer references + // changed documents if (oldProjectChecksums.Documents.Checksum != newProjectChecksums.Documents.Checksum) { project = await UpdateDocumentsAsync( @@ -606,7 +618,8 @@ private async Task ValidateChecksumAsync( if (checksumFromRequest == currentSolutionChecksum) return; - var solutionInfo = await _assetProvider.CreateSolutionInfoAsync(checksumFromRequest, cancellationToken).ConfigureAwait(false); + var solutionInfo = await _assetProvider.CreateSolutionInfoAsync( + checksumFromRequest, _workspace.Services.SolutionServices, cancellationToken).ConfigureAwait(false); var workspace = new AdhocWorkspace(_workspace.Services.HostServices); workspace.AddSolution(solutionInfo); diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs index bb8c4b03cad06..4177e90130d7a 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs @@ -214,16 +214,16 @@ private async Task GetOrCreateSolutionToUpdateAsync( return currentSolution; // If not, have to create a new, fresh, solution instance to update. - var solutionInfo = await assetProvider.CreateSolutionInfoAsync(solutionChecksum, cancellationToken).ConfigureAwait(false); + var solutionInfo = await assetProvider.CreateSolutionInfoAsync( + solutionChecksum, this.Services.SolutionServices, cancellationToken).ConfigureAwait(false); return CreateSolutionFromInfo(solutionInfo); async Task IsIncrementalUpdateAsync() { var newSolutionCompilationChecksums = await assetProvider.GetAssetAsync( - AssetPathKind.SolutionCompilationStateChecksums, solutionChecksum, cancellationToken).ConfigureAwait(false); + AssetPathKind.SolutionCompilationStateChecksums, solutionChecksum, cancellationToken).ConfigureAwait(false); var newSolutionChecksums = await assetProvider.GetAssetAsync( AssetPathKind.SolutionStateChecksums, newSolutionCompilationChecksums.SolutionState, cancellationToken).ConfigureAwait(false); - var newSolutionInfo = await assetProvider.GetAssetAsync( AssetPathKind.SolutionAttributes, newSolutionChecksums.Attributes, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj b/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj index 30ef73b68de73..0e4ce0cb1d1ac 100644 --- a/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj +++ b/src/Workspaces/Remote/ServiceHub/Microsoft.CodeAnalysis.Remote.ServiceHub.csproj @@ -44,13 +44,14 @@ - - - + + + + diff --git a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs index 76272ef377613..3c979bffd663a 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SourceGeneration/RemoteSourceGenerationService.cs @@ -10,7 +10,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SourceGeneration; using Roslyn.Utilities; @@ -102,9 +104,6 @@ public async ValueTask HasGeneratorsAsync( var workspace = GetWorkspace(); var assetProvider = workspace.CreateAssetProvider(solutionChecksum, WorkspaceManager.SolutionAssetCache, SolutionAssetSource); - using var _1 = PooledHashSet.GetInstance(out var checksums); - checksums.AddRange(analyzerReferenceChecksums); - // Fetch the analyzer references specified by the host. Note: this will only serialize this information over // the first time needed. After that, it will be cached in the WorkspaceManager.SolutionAssetCache on the remote // side, so it will be a no-op to fetch them in the future. @@ -114,16 +113,20 @@ public async ValueTask HasGeneratorsAsync( // those will almost always be the same, we'll just fetch the precomputed values on our end, return them, and // the host will cache it. We'll only actually fetch something new and compute something new when an actual new // analyzer reference is added. - using var _2 = ArrayBuilder.GetInstance(checksums.Count, out var analyzerReferences); - await assetProvider.GetAssetHelper().GetAssetsAsync( - projectId, - checksums, - static (_, analyzerReference, analyzerReferences) => analyzerReferences.Add(analyzerReference), - analyzerReferences, + + var checksumCollection = new ChecksumCollection(analyzerReferenceChecksums); + + // Make sure the analyzer references are loaded into an isolated ALC so that we can properly load them if + // they're a new version of some analyzer reference we've already loaded. + var isolatedReferences = await IsolatedAnalyzerReferenceSet.CreateIsolatedAnalyzerReferencesAsync( + useAsync: true, + checksumCollection, + workspace.Services.SolutionServices, + () => assetProvider.GetAssetsArrayAsync(projectId, checksumCollection, cancellationToken), cancellationToken).ConfigureAwait(false); var (analyzerReferenceMap, callback) = s_languageToAnalyzerReferenceMap[language]; - foreach (var analyzerReference in analyzerReferences) + foreach (var analyzerReference in isolatedReferences) { var hasGenerators = analyzerReferenceMap.GetValue(analyzerReference, callback); if (hasGenerators.Value)