File: Workspace\Solution\SolutionCompilationState.GeneratorDriverInitializationCache.cs
Web Access
Project: src\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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
internal sealed partial class SolutionCompilationState
{
    internal sealed class GeneratorDriverInitializationCache
    {
        /// <summary>
        /// A GeneratorDriver instance that has been created for a project in the solution. Any time we create a GeneratorDriver the first time for
        /// a project, we'll hold it in this field. If other requests come in to get a GeneratorDriver for the same project (but from different Solution snapshots),
        /// well reuse this GeneratorDriver rather than creating a new one. This allows some first time initialization of a generator (like walking metadata references)
        /// to be shared rather than doing that initialization multiple times. In the case we are reusing a GeneratorDriver, we'll still always update the GeneratorDriver with
        /// the current state of the project with a call to <see cref="UpdateGeneratorDriverToMatchState" />, so the results are still correct.
        ///
        /// This object is held by <see cref="SolutionCompilationState._generatorDriverInitializationCaches" />, and is created when a new project is created, to be shared across
        /// all snapshots.
        ///
        /// When a generator run has happened for a Project, this is assigned an updated AsyncLazy holding the most recent GeneratorDriver from the run.
        /// The idea here is if a different fork of the Solution still needs a generator, we have a more recent one. It also ensures that generally the GeneratorDriver
        /// being held is "recent", so that way we're not holding onto generator state from a much older run of the solution.
        /// </summary>
        private AsyncLazy<GeneratorDriver>? _driverCache;
 
        public async Task<GeneratorDriver> CreateAndRunGeneratorDriverAsync(
            ProjectState projectState,
            Compilation compilation,
            Func<GeneratorFilterContext, bool> generatorFilter,
            CancellationToken cancellationToken)
        {
            // If we already have a cached entry setup, just use it; no reason to avoid creating an AsyncLazy we won't use if we can avoid it
            var existingDriverCache = _driverCache;
            if (existingDriverCache is not null)
            {
                return UpdateDriverAndRunGenerators(await existingDriverCache.GetValueAsync(cancellationToken).ConfigureAwait(false));
            }
 
            // The AsyncLazy we create here implicitly creates a GeneratorDriver that will run generators for the compilation passed to this method.
            // If the one that is set to _driverCache is the one we created, then it's ready to go. If the AsyncLazy is one created by some
            // other call, then we'll still need to run generators for the compilation passed.
            var createdAsyncLazy = AsyncLazy.Create(CreateGeneratorDriverAndRunGenerators);
            var asyncLazy = Interlocked.CompareExchange(ref _driverCache, createdAsyncLazy, comparand: null);
 
            if (asyncLazy == null)
            {
                // We want to ensure that the driver is always created and initialized at least once, so we'll ensure that runs even if we cancel the request here.
                // Otherwise the concern is we might keep starting and cancelling the work which is just wasteful to keep doing it over and over again. We do this
                // in a Task.Run() so if the underlying computation were to run on our thread, we're not blocking our caller from observing cancellation
                // if they request it.
                _ = Task.Run(() => createdAsyncLazy.GetValueAsync(CancellationToken.None));
 
                return await createdAsyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
            }
            else
            {
                return UpdateDriverAndRunGenerators(await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false));
            }
 
            GeneratorDriver CreateGeneratorDriverAndRunGenerators(CancellationToken cancellationToken)
            {
                var generatedFilesBaseDirectory = projectState.CompilationOutputInfo.GetEffectiveGeneratedFilesOutputDirectory();
                var additionalTexts = projectState.AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText);
                var compilationFactory = projectState.LanguageServices.GetRequiredService<ICompilationFactoryService>();
 
                var generatorDriver = compilationFactory.CreateGeneratorDriver(
                    projectState.ParseOptions!,
                    GetSourceGenerators(projectState),
                    projectState.ProjectAnalyzerOptions.AnalyzerConfigOptionsProvider,
                    additionalTexts,
                    generatedFilesBaseDirectory);
 
                return generatorDriver.RunGenerators(compilation, generatorFilter, cancellationToken);
            }
 
            GeneratorDriver UpdateDriverAndRunGenerators(GeneratorDriver driver)
            {
                driver = UpdateGeneratorDriverToMatchState(driver, projectState);
 
                return driver.RunGenerators(compilation, generatorFilter, cancellationToken);
            }
        }
 
        public void UpdateCacheWithGeneratorDriver(GeneratorDriver driver)
        {
            _driverCache = AsyncLazy.Create(driver);
        }
    }
}