File: Workspace\Solution\SolutionState.SkeletonReferenceCache.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Diagnostics.Tracing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
internal partial class SolutionState
{
    /// <summary>
    /// Caches the skeleton references produced for a given project/compilation under the varying <see
    /// cref="MetadataReferenceProperties"/> it might be referenced by.  Skeletons are used in the compilation tracker
    /// to allow cross-language project references with live semantic updating between VB/C# and vice versa.
    /// Specifically, in a cross language case we will build a skeleton ref for the referenced project and have the
    /// referrer use that to understand its semantics.
    /// <para>
    /// This approach works, but has the caveat that live cross-language semantics are only possible when the skeleton
    /// assembly can be built.  This should always be the case for correct code, but it may not be the case for code
    /// with errors depending on if the respective language compiler is resilient to those errors or not. In that case
    /// though where the skeleton cannot be built, this type provides mechanisms to fallback to the last successfully
    /// built skeleton so that a somewhat reasonable experience can be maintained.  If we failed to do this and instead
    /// returned nothing, a user would find that practically all semantic experiences that depended on that particular
    /// project would fail or be seriously degraded (e.g. diagnostics).  To that end, it's better to limp along with
    /// stale date, then barrel on ahead with no data.
    /// </para>
    /// <para>
    /// The implementation works by keeping metadata references around associated with a specific <see
    /// cref="VersionStamp"/> for a project. As long as the <see cref="Project.GetDependentSemanticVersionAsync"/> for
    /// that project is the same, then all the references of it can be reused.  When an <see
    /// cref="ICompilationTracker"/> forks itself, it will also <see cref="Clone"/> this, allowing previously computed
    /// references to be used by later forks. However, this means that later forks (esp. ones that fail to produce a
    /// skeleton, or which produce a skeleton for different semantics) will not leak backward to a prior <see
    /// cref="ProjectState"/>, causing it to see a view of the world inapplicable to its current snapshot.  A downside
    /// of this is that if a fork happens to a compilation tracker *prior* to the skeleton for it being computed, then
    /// when the skeleton is actually produced it won't be shared forward.  In practice the hope is that this is rare,
    /// and that eventually the compilation trackers will have computed the skeleton and will be able to pass it forward
    /// from that point onwards.
    /// </para>
    /// <para>
    /// The cached data we compute is associated with a particular compilation-tracker.  Because of this, once we
    /// compute the skeleton information for that tracker, we hold onto it for as long as the tracker is itself alive.
    /// The presumption here is that once created, it will likely be needed in the future as well as there will still be
    /// downstream projects of different languages that reference this.  The only time this won't hold true is if there
    /// was a cross language p2p ref, but then it gets removed from the solution.  However, this sort of change should
    /// be rare in a solution, so it's unlikely to happen much, and the only negative is holding onto a little bit more
    /// memory.
    /// </para>
    /// </summary>
    private partial class SkeletonReferenceCache
    {
        private static readonly EmitOptions s_metadataOnlyEmitOptions = new(metadataOnly: true);
 
        /// <summary>
        /// Static conditional mapping from a compilation to the skeleton set produced for it.  This is valuable for a
        /// couple of reasons. First, a compilation tracker may fork, but produce the same compilation.  As such, we
        /// want to get the same skeleton set for it.  Second, consider the following scenario:
        /// <list type="number">
        /// <item>Project A is referenced by projects B and C (both have a different language than A).</item>
        /// <item>Producing the compilation for 'B' produces the compilation for 'A' which produces the skeleton that 'B' references.</item>
        /// <item>B's compilation is released and then GC'ed.</item> 
        /// <item>Producing the compilation for 'C' needs the skeleton from 'A'</item>
        /// </list>
        /// At this point we would not want to re-emit the assembly metadata for A's compilation.  We already did that
        /// for 'B', and it can be enormously expensive to do so again.  So as long as A's compilation lives, we really
        /// want to keep it's skeleton cache around.
        /// </summary>
        private static readonly ConditionalWeakTable<Compilation, AsyncLazy<SkeletonReferenceSet?>> s_compilationToSkeletonSet = new();
 
        /// <summary>
        /// Lock around <see cref="_version"/> and <see cref="_skeletonReferenceSet"/> to ensure they are updated/read 
        /// in an atomic fashion.
        /// </summary>
        private readonly object _stateGate = new();
 
        /// <summary>
        /// The <see cref="Project.GetDependentSemanticVersionAsync"/> version of the project that the
        /// <see cref="_skeletonReferenceSet"/> corresponds to.
        /// </summary>
        private VersionStamp? _version;
 
        /// <summary>
        /// Mapping from metadata-reference-properties to the actual metadata reference for them.
        /// </summary>
        private SkeletonReferenceSet? _skeletonReferenceSet;
 
        public SkeletonReferenceCache()
            : this(version: null, skeletonReferenceSet: null)
        {
        }
 
        private SkeletonReferenceCache(
            VersionStamp? version,
            SkeletonReferenceSet? skeletonReferenceSet)
        {
            _version = version;
            _skeletonReferenceSet = skeletonReferenceSet;
        }
 
        /// <summary>
        /// Produces a copy of the <see cref="SkeletonReferenceCache"/>, allowing forks of <see cref="ProjectState"/> to
        /// reuse <see cref="MetadataReference"/>s when their dependent semantic version matches ours.  In the case where
        /// the version is different, then the clone will attempt to make a new skeleton reference for that version.  If it
        /// succeeds, it will use that.  If it fails however, it can still use our skeletons.
        /// </summary>
        public SkeletonReferenceCache Clone()
        {
            lock (_stateGate)
            {
                // pass along the best version/reference-set we computed for ourselves.  That way future ProjectStates
                // can use this data if either the version changed, or they weren't able to build a skeleton for themselves.
                // By passing along a copy we ensure that if they have a different version, they'll end up producing a new
                // SkeletonReferenceSet where they'll store their own data in which will not affect prior ProjectStates.
                return new SkeletonReferenceCache(_version, _skeletonReferenceSet);
            }
        }
 
        public MetadataReference? TryGetAlreadyBuiltMetadataReference(MetadataReferenceProperties properties)
            => _skeletonReferenceSet?.GetOrCreateMetadataReference(properties);
 
        public async Task<MetadataReference?> GetOrBuildReferenceAsync(
            ICompilationTracker compilationTracker,
            SolutionState solution,
            MetadataReferenceProperties properties,
            CancellationToken cancellationToken)
        {
            var version = await compilationTracker.GetDependentSemanticVersionAsync(solution, cancellationToken).ConfigureAwait(false);
            var referenceSet = await TryGetOrCreateReferenceSetAsync(
                compilationTracker, solution, version, cancellationToken).ConfigureAwait(false);
            if (referenceSet == null)
                return null;
 
            return referenceSet.GetOrCreateMetadataReference(properties);
        }
 
        private async Task<SkeletonReferenceSet?> TryGetOrCreateReferenceSetAsync(
            ICompilationTracker compilationTracker,
            SolutionState solution,
            VersionStamp version,
            CancellationToken cancellationToken)
        {
            // First, just see if we have cached a reference set that is complimentary with the version of the project
            // being passed in.  If so, we can just reuse what we already computed before.
            if (TryReadSkeletonReferenceSetAtThisVersion(version, out var referenceSet))
                return referenceSet;
 
            // okay, we don't have anything cached with this version. so create one now.
 
            var currentSkeletonReferenceSet = await CreateSkeletonReferenceSetAsync(compilationTracker, solution, cancellationToken).ConfigureAwait(false);
 
            lock (_stateGate)
            {
                // If we successfully created the metadata storage, then create the new set that points to it.
                // if we didn't, that's ok too, we'll just say that for this requested version, that we can
                // return any prior computed reference set (including 'null' if we've never successfully made
                // a skeleton).
                if (currentSkeletonReferenceSet != null)
                    _skeletonReferenceSet = currentSkeletonReferenceSet;
 
                _version = version;
 
                return _skeletonReferenceSet;
            }
        }
 
        private static async Task<SkeletonReferenceSet?> CreateSkeletonReferenceSetAsync(
            ICompilationTracker compilationTracker,
            SolutionState solution,
            CancellationToken cancellationToken)
        {
            // It's acceptable for this computation to be something that multiple calling threads may hit at once.  The
            // implementation inside the compilation tracker does an async-wait on a an internal semaphore to ensure 
            // only one thread actually does the computation and the rest wait.
            var compilation = await compilationTracker.GetCompilationAsync(solution, cancellationToken).ConfigureAwait(false);
            var services = solution.Services;
 
            // note: computing the assembly metadata is actually synchronous.  However, this ensures we don't have N
            // threads blocking on a lazy to compute the work.  Instead, we'll only occupy one thread, while any
            // concurrent requests asynchronously wait for that work to be done.
 
            var lazy = s_compilationToSkeletonSet.GetValue(compilation,
                compilation => new AsyncLazy<SkeletonReferenceSet?>(
                    cancellationToken => Task.FromResult(CreateSkeletonSet(services, compilation, cancellationToken)),
                    cacheResult: true));
 
            return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
        }
 
        private static SkeletonReferenceSet? CreateSkeletonSet(
            SolutionServices services, Compilation compilation, CancellationToken cancellationToken)
        {
            var storage = TryCreateMetadataStorage(services, compilation, cancellationToken);
            if (storage == null)
                return null;
 
            var metadata = AssemblyMetadata.CreateFromStream(storage.ReadStream(cancellationToken), leaveOpen: false);
 
            // read in the stream and pass ownership of it to the metadata object.  When it is disposed it will dispose
            // the stream as well.
            return new SkeletonReferenceSet(
                metadata,
                compilation.AssemblyName,
                new DeferredDocumentationProvider(compilation));
        }
 
        private bool TryReadSkeletonReferenceSetAtThisVersion(VersionStamp version, out SkeletonReferenceSet? result)
        {
            lock (_stateGate)
            {
                // if we're asking about the same version as we've cached, then return whatever have (regardless of
                // whether it succeeded or not.
                if (version == _version)
                {
                    result = _skeletonReferenceSet;
                    return true;
                }
            }
 
            result = null;
            return false;
        }
 
        private static ITemporaryStreamStorageInternal? TryCreateMetadataStorage(SolutionServices services, Compilation compilation, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var logger = services.GetService<IWorkspaceTestLogger>();
 
            try
            {
                logger?.Log($"Beginning to create a skeleton assembly for {compilation.AssemblyName}...");
 
                using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_EmitMetadataOnlyImage, cancellationToken))
                {
                    using var stream = SerializableBytes.CreateWritableStream();
 
                    var emitResult = compilation.Emit(stream, options: s_metadataOnlyEmitOptions, cancellationToken: cancellationToken);
 
                    if (emitResult.Success)
                    {
                        logger?.Log($"Successfully emitted a skeleton assembly for {compilation.AssemblyName}");
 
                        var temporaryStorageService = services.GetRequiredService<ITemporaryStorageServiceInternal>();
                        var storage = temporaryStorageService.CreateTemporaryStreamStorage();
 
                        stream.Position = 0;
                        storage.WriteStream(stream, cancellationToken);
 
                        return storage;
                    }
 
                    if (logger != null)
                    {
                        logger.Log($"Failed to create a skeleton assembly for {compilation.AssemblyName}:");
 
                        foreach (var diagnostic in emitResult.Diagnostics)
                        {
                            logger.Log("  " + diagnostic.GetMessage());
                        }
                    }
 
                    // log emit failures so that we can improve most common cases
                    Logger.Log(FunctionId.MetadataOnlyImage_EmitFailure, KeyValueLogMessage.Create(m =>
                    {
                        // log errors in the format of
                        // CS0001:1;CS002:10;...
                        var groups = emitResult.Diagnostics.GroupBy(d => d.Id).Select(g => $"{g.Key}:{g.Count()}");
                        m["Errors"] = string.Join(";", groups);
                    }));
 
                    return null;
                }
            }
            finally
            {
                logger?.Log($"Done trying to create a skeleton assembly for {compilation.AssemblyName}");
            }
        }
    }
}