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.Collections.Generic;
using System.Collections.Immutable;
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 set of GeneratorDriver instances that have been created for the keyed project in the solution. Any time we create a GeneratorDriver the first time for
        /// a project, we'll put it into this map. 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, so the results are still correct.
        /// 
        /// Since these entries are going to be holding onto non-trivial amounts of state, we get rid of the cached entries once there's a belief that we won't be
        /// creating further GeneratorDrivers for a given project. See uses of <see cref="EmptyCacheForProjectsThatHaveGeneratorDriversInSolution"/>
        /// for details.
        /// 
        /// Any additions/removals to this map must be done via ImmutableInterlocked methods.
        /// </summary>
        private ImmutableDictionary<ProjectId, AsyncLazy<GeneratorDriver>> _driverCache = ImmutableDictionary<ProjectId, AsyncLazy<GeneratorDriver>>.Empty;
 
        public async Task<GeneratorDriver> CreateAndRunGeneratorDriverAsync(
            ProjectState projectState,
            Compilation compilation,
            Func<GeneratorFilterContext, bool> generatorFilter,
            CancellationToken cancellationToken)
        {
            // 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 added 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 = ImmutableInterlocked.GetOrAdd(ref _driverCache, projectState.Id, static (_, created) => created, createdAsyncLazy);
 
            if (asyncLazy == createdAsyncLazy)
            {
                // 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(() => asyncLazy.GetValueAsync(CancellationToken.None));
 
                return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
            }
            else
            {
                var driver = await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
 
                driver = UpdateGeneratorDriverToMatchState(driver, projectState);
 
                return driver.RunGenerators(compilation, generatorFilter, cancellationToken);
            }
 
            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);
            }
        }
 
        public void EmptyCacheForProjectsThatHaveGeneratorDriversInSolution(SolutionCompilationState state)
        {
            // If we don't have any cached drivers, then just return before we loop through all the projects
            // in the solution. This is to ensure that once we hit a steady-state case of a Workspace's CurrentSolution
            // having generators for all projects, we won't need to keep anything further in our cache since the cache
            // will never be used -- any running of generators in the future will use the GeneratorDrivers already held by
            // the Solutions.
            //
            // This doesn't need to be synchronized against other mutations to _driverCache. If we see it as empty when
            // in reality something was just being added, we'll just do the cleanup the next time this method is called.
            if (_driverCache.IsEmpty)
                return;
 
            foreach (var (projectId, tracker) in state._projectIdToTrackerMap)
            {
                if (tracker.GeneratorDriver is not null)
                    EmptyCacheForProject(projectId);
            }
        }
 
        public void EmptyCacheForProject(ProjectId projectId)
        {
            ImmutableInterlocked.TryRemove(ref _driverCache, projectId, out _);
        }
    }
}