diff --git a/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs b/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs index 11aab0a785c20..64777db3b8094 100644 --- a/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs +++ b/src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs @@ -25,8 +25,8 @@ internal abstract class CSharpCompiler : CommonCompiler private readonly CommandLineDiagnosticFormatter _diagnosticFormatter; private readonly string? _tempDirectory; - protected CSharpCompiler(CSharpCommandLineParser parser, string? responseFile, string[] args, BuildPaths buildPaths, string? additionalReferenceDirectories, IAnalyzerAssemblyLoader assemblyLoader) - : base(parser, responseFile, args, buildPaths, additionalReferenceDirectories, assemblyLoader) + protected CSharpCompiler(CSharpCommandLineParser parser, string? responseFile, string[] args, BuildPaths buildPaths, string? additionalReferenceDirectories, IAnalyzerAssemblyLoader assemblyLoader, GeneratorDriverCache? driverCache = null) + : base(parser, responseFile, args, buildPaths, additionalReferenceDirectories, assemblyLoader, driverCache) { _diagnosticFormatter = new CommandLineDiagnosticFormatter(buildPaths.WorkingDirectory, Arguments.PrintFullPaths, Arguments.ShouldIncludeErrorEndLocation); _tempDirectory = buildPaths.TempDirectory; @@ -372,12 +372,9 @@ protected override void ResolveEmbeddedFilesFromExternalSourceDirectives( } } - private protected override Compilation RunGenerators(Compilation input, ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigProvider, ImmutableArray additionalTexts, DiagnosticBag diagnostics) + private protected override GeneratorDriver CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts) { - var driver = CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, analyzerConfigProvider); - driver.RunGeneratorsAndUpdateCompilation(input, out var compilationOut, out var generatorDiagnostics); - diagnostics.AddRange(generatorDiagnostics); - return compilationOut; + return CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, analyzerConfigOptionsProvider); } } } diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTestBase.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTestBase.cs index 81fdcedb8d1ec..d9788dd37f7aa 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTestBase.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTestBase.cs @@ -54,15 +54,15 @@ internal CSharpCommandLineArguments DefaultParse(IEnumerable args, strin return CSharpCommandLineParser.Default.Parse(args, baseDirectory, sdkDirectory, additionalReferenceDirectories); } - internal MockCSharpCompiler CreateCSharpCompiler(string[] args, ImmutableArray analyzers = default, ImmutableArray generators = default, AnalyzerAssemblyLoader loader = null) + internal MockCSharpCompiler CreateCSharpCompiler(string[] args, ImmutableArray analyzers = default, ImmutableArray generators = default, AnalyzerAssemblyLoader loader = null, GeneratorDriverCache driverCache = null) { - return CreateCSharpCompiler(null, WorkingDirectory, args, analyzers, generators, loader); + return CreateCSharpCompiler(null, WorkingDirectory, args, analyzers, generators, loader, driverCache); } - internal MockCSharpCompiler CreateCSharpCompiler(string responseFile, string workingDirectory, string[] args, ImmutableArray analyzers = default, ImmutableArray generators = default, AnalyzerAssemblyLoader loader = null) + internal MockCSharpCompiler CreateCSharpCompiler(string responseFile, string workingDirectory, string[] args, ImmutableArray analyzers = default, ImmutableArray generators = default, AnalyzerAssemblyLoader loader = null, GeneratorDriverCache driverCache = null) { var buildPaths = RuntimeUtilities.CreateBuildPaths(workingDirectory, sdkDirectory: SdkDirectory); - return new MockCSharpCompiler(responseFile, buildPaths, args, analyzers, generators, loader); + return new MockCSharpCompiler(responseFile, buildPaths, args, analyzers, generators, loader, driverCache); } } } diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs index 05fa00db21656..13c716fe64404 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs @@ -9793,6 +9793,235 @@ string[] compileAndRun(string featureOpt) }; } + [Fact] + public void Compiler_Uses_DriverCache() + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ +}"); + int sourceCallbackCount = 0; + var generator = new PipelineCallbackGenerator((ctx) => + { + ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (spc, po) => + { + sourceCallbackCount++; + }); + }); + + // with no cache, we'll see the callback execute multiple times + RunWithNoCache(); + Assert.Equal(1, sourceCallbackCount); + + RunWithNoCache(); + Assert.Equal(2, sourceCallbackCount); + + RunWithNoCache(); + Assert.Equal(3, sourceCallbackCount); + + // now re-run with a cache + GeneratorDriverCache cache = new GeneratorDriverCache(); + sourceCallbackCount = 0; + + RunWithCache(); + Assert.Equal(1, sourceCallbackCount); + + RunWithCache(); + Assert.Equal(1, sourceCallbackCount); + + RunWithCache(); + Assert.Equal(1, sourceCallbackCount); + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + Directory.Delete(dir.Path, true); + + void RunWithNoCache() => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview" }, generators: new[] { generator.AsSourceGenerator() }, analyzers: null); + + void RunWithCache() => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview" }, generators: new[] { generator.AsSourceGenerator() }, driverCache: cache, analyzers: null); + } + + [Fact] + public void Compiler_Doesnt_Use_Cache_From_Other_Compilation() + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ +}"); + int sourceCallbackCount = 0; + var generator = new PipelineCallbackGenerator((ctx) => + { + ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (spc, po) => + { + sourceCallbackCount++; + }); + }); + + // now re-run with a cache + GeneratorDriverCache cache = new GeneratorDriverCache(); + sourceCallbackCount = 0; + + RunWithCache("1.dll"); + Assert.Equal(1, sourceCallbackCount); + + RunWithCache("1.dll"); + Assert.Equal(1, sourceCallbackCount); + + // now emulate a new compilation, and check we were invoked, but only once + RunWithCache("2.dll"); + Assert.Equal(2, sourceCallbackCount); + + RunWithCache("2.dll"); + Assert.Equal(2, sourceCallbackCount); + + // now re-run our first compilation + RunWithCache("1.dll"); + Assert.Equal(2, sourceCallbackCount); + + // a new one + RunWithCache("3.dll"); + Assert.Equal(3, sourceCallbackCount); + + // and another old one + RunWithCache("2.dll"); + Assert.Equal(3, sourceCallbackCount); + + RunWithCache("1.dll"); + Assert.Equal(3, sourceCallbackCount); + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + Directory.Delete(dir.Path, true); + + void RunWithCache(string outputPath) => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview", "/out:" + outputPath }, generators: new[] { generator.AsSourceGenerator() }, driverCache: cache, analyzers: null); + } + + [Fact] + public void Compiler_Can_Disable_DriverCache() + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ +}"); + int sourceCallbackCount = 0; + var generator = new PipelineCallbackGenerator((ctx) => + { + ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (spc, po) => + { + sourceCallbackCount++; + }); + }); + + // run with the cache + GeneratorDriverCache cache = new GeneratorDriverCache(); + sourceCallbackCount = 0; + + RunWithCache(); + Assert.Equal(1, sourceCallbackCount); + + RunWithCache(); + Assert.Equal(1, sourceCallbackCount); + + RunWithCache(); + Assert.Equal(1, sourceCallbackCount); + + // now re-run with the cache disabled + sourceCallbackCount = 0; + + RunWithCacheDisabled(); + Assert.Equal(1, sourceCallbackCount); + + RunWithCacheDisabled(); + Assert.Equal(2, sourceCallbackCount); + + RunWithCacheDisabled(); + Assert.Equal(3, sourceCallbackCount); + + // now clear the cache as well as disabling, and verify we don't put any entries into it either + cache = new GeneratorDriverCache(); + sourceCallbackCount = 0; + + RunWithCacheDisabled(); + Assert.Equal(1, sourceCallbackCount); + Assert.Equal(0, cache.CacheSize); + + RunWithCacheDisabled(); + Assert.Equal(2, sourceCallbackCount); + Assert.Equal(0, cache.CacheSize); + + RunWithCacheDisabled(); + Assert.Equal(3, sourceCallbackCount); + Assert.Equal(0, cache.CacheSize); + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + Directory.Delete(dir.Path, true); + + void RunWithCache() => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview" }, generators: new[] { generator.AsSourceGenerator() }, driverCache: cache, analyzers: null); + + void RunWithCacheDisabled() => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview", "/features:disable-generator-cache" }, generators: new[] { generator.AsSourceGenerator() }, driverCache: cache, analyzers: null); + } + + [Fact] + public void Adding_Or_Removing_A_Generator_Invalidates_Cache() + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ +}"); + int sourceCallbackCount = 0; + int sourceCallbackCount2 = 0; + var generator = new PipelineCallbackGenerator((ctx) => + { + ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (spc, po) => + { + sourceCallbackCount++; + }); + }); + + var generator2 = new PipelineCallbackGenerator2((ctx) => + { + ctx.RegisterSourceOutput(ctx.ParseOptionsProvider, (spc, po) => + { + sourceCallbackCount2++; + }); + }); + + // run with the cache + GeneratorDriverCache cache = new GeneratorDriverCache(); + + RunWithOneGenerator(); + Assert.Equal(1, sourceCallbackCount); + Assert.Equal(0, sourceCallbackCount2); + + RunWithOneGenerator(); + Assert.Equal(1, sourceCallbackCount); + Assert.Equal(0, sourceCallbackCount2); + + RunWithTwoGenerators(); + Assert.Equal(2, sourceCallbackCount); + Assert.Equal(1, sourceCallbackCount2); + + RunWithTwoGenerators(); + Assert.Equal(2, sourceCallbackCount); + Assert.Equal(1, sourceCallbackCount2); + + // this seems counterintuitive, but when the only thing to change is the generator, we end up back at the state of the project when + // we just ran a single generator. Thus we already have an entry in the cache we can use (the one created by the original call to + // RunWithOneGenerator above) meaning we can use the previously cached results and not run. + RunWithOneGenerator(); + Assert.Equal(2, sourceCallbackCount); + Assert.Equal(1, sourceCallbackCount2); + + void RunWithOneGenerator() => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview" }, generators: new[] { generator.AsSourceGenerator() }, driverCache: cache, analyzers: null); + + void RunWithTwoGenerators() => VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/langversion:preview" }, generators: new[] { generator.AsSourceGenerator(), generator2.AsSourceGenerator() }, driverCache: cache, analyzers: null); + } + private static int OccurrenceCount(string source, string word) { var n = 0; @@ -9814,6 +10043,7 @@ private string VerifyOutput(TempDirectory sourceDir, TempFile sourceFile, int? expectedExitCode = null, bool errorlog = false, IEnumerable generators = null, + GeneratorDriverCache driverCache = null, params DiagnosticAnalyzer[] analyzers) { var args = new[] { @@ -9835,7 +10065,7 @@ private string VerifyOutput(TempDirectory sourceDir, TempFile sourceFile, args = args.Append(additionalFlags); } - var csc = CreateCSharpCompiler(null, sourceDir.Path, args, analyzers: analyzers.ToImmutableArrayOrEmpty(), generators: generators.ToImmutableArrayOrEmpty()); + var csc = CreateCSharpCompiler(null, sourceDir.Path, args, analyzers: analyzers.ToImmutableArrayOrEmpty(), generators: generators.ToImmutableArrayOrEmpty(), driverCache: driverCache); var outWriter = new StringWriter(CultureInfo.InvariantCulture); var exitCode = csc.Run(outWriter); var output = outWriter.ToString(); diff --git a/src/Compilers/CSharp/Test/CommandLine/GeneratorDriverCacheTests.cs b/src/Compilers/CSharp/Test/CommandLine/GeneratorDriverCacheTests.cs new file mode 100644 index 0000000000000..3c7e7eb8fe0c0 --- /dev/null +++ b/src/Compilers/CSharp/Test/CommandLine/GeneratorDriverCacheTests.cs @@ -0,0 +1,101 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.CommandLine.UnitTests +{ + public class GeneratorDriverCacheTests : CommandLineTestBase + { + + [Fact] + public void DriverCache_Returns_Null_For_No_Match() + { + var driverCache = new GeneratorDriverCache(); + var driver = driverCache.TryGetDriver("0"); + + Assert.Null(driver); + } + + [Fact] + public void DriverCache_Returns_Cached_Driver() + { + var drivers = GetDrivers(1); + var driverCache = new GeneratorDriverCache(); + driverCache.CacheGenerator("0", drivers[0]); + + var driver = driverCache.TryGetDriver("0"); + Assert.Same(driver, drivers[0]); + } + + [Fact] + public void DriverCache_Can_Cache_Multiple_Drivers() + { + var drivers = GetDrivers(3); + + var driverCache = new GeneratorDriverCache(); + driverCache.CacheGenerator("0", drivers[0]); + driverCache.CacheGenerator("1", drivers[1]); + driverCache.CacheGenerator("2", drivers[2]); + + var driver = driverCache.TryGetDriver("0"); + Assert.Same(driver, drivers[0]); + + driver = driverCache.TryGetDriver("1"); + Assert.Same(driver, drivers[1]); + + driver = driverCache.TryGetDriver("2"); + Assert.Same(driver, drivers[2]); + } + + [Fact] + public void DriverCache_Evicts_Least_Recently_Used() + { + var drivers = GetDrivers(GeneratorDriverCache.MaxCacheSize + 2); + var driverCache = new GeneratorDriverCache(); + + // put n+1 drivers into the cache + for (int i = 0; i < GeneratorDriverCache.MaxCacheSize + 1; i++) + { + driverCache.CacheGenerator(i.ToString(), drivers[i]); + } + // current cache state is + // (10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + + // now try and retrieve the first driver which should no longer be in the cache + var driver = driverCache.TryGetDriver("0"); + Assert.Null(driver); + + // add it back + driverCache.CacheGenerator("0", drivers[0]); + + // current cache state is + // (0, 10, 9, 8, 7, 6, 5, 4, 3, 2) + + // access some drivers in the middle + driver = driverCache.TryGetDriver("7"); + driver = driverCache.TryGetDriver("4"); + driver = driverCache.TryGetDriver("2"); + + // current cache state is + // (2, 4, 7, 0, 10, 9, 8, 6, 5, 3) + + // try and get a new driver that was never in the cache + driver = driverCache.TryGetDriver("11"); + Assert.Null(driver); + driverCache.CacheGenerator("11", drivers[11]); + + // current cache state is + // (11, 2, 4, 7, 0, 10, 9, 8, 6, 5) + + // get a driver that has been evicted + driver = driverCache.TryGetDriver("3"); + Assert.Null(driver); + } + + private static GeneratorDriver[] GetDrivers(int count) => Enumerable.Range(0, count).Select(i => CSharpGeneratorDriver.Create(Array.Empty())).ToArray(); + } +} diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index 11c8a1220ab40..d18b3650e8e89 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -72,6 +72,7 @@ internal abstract partial class CommonCompiler public CommonMessageProvider MessageProvider { get; } public CommandLineArguments Arguments { get; } public IAnalyzerAssemblyLoader AssemblyLoader { get; private set; } + public GeneratorDriverCache? GeneratorDriverCache { get; } public abstract DiagnosticFormatter DiagnosticFormatter { get; } /// @@ -116,7 +117,7 @@ protected abstract void ResolveAnalyzersFromArguments( out ImmutableArray analyzers, out ImmutableArray generators); - public CommonCompiler(CommandLineParser parser, string? responseFile, string[] args, BuildPaths buildPaths, string? additionalReferenceDirectories, IAnalyzerAssemblyLoader assemblyLoader) + public CommonCompiler(CommandLineParser parser, string? responseFile, string[] args, BuildPaths buildPaths, string? additionalReferenceDirectories, IAnalyzerAssemblyLoader assemblyLoader, GeneratorDriverCache? driverCache) { IEnumerable allArgs = args; @@ -129,6 +130,7 @@ public CommonCompiler(CommandLineParser parser, string? responseFile, string[] a this.Arguments = parser.Parse(allArgs, buildPaths.WorkingDirectory, buildPaths.SdkDirectory, additionalReferenceDirectories); this.MessageProvider = parser.MessageProvider; this.AssemblyLoader = assemblyLoader; + this.GeneratorDriverCache = driverCache; this.EmbeddedSourcePaths = GetEmbeddedSourcePaths(Arguments); if (Arguments.ParseOptions.Features.ContainsKey("debug-determinism")) @@ -734,7 +736,52 @@ public virtual int Run(TextWriter consoleOutput, CancellationToken cancellationT /// Any additional texts that should be passed to the generators when run. /// Any diagnostics that were produced during generation /// A compilation that represents the original compilation with any additional, generated texts added to it. - private protected virtual Compilation RunGenerators(Compilation input, ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts, DiagnosticBag generatorDiagnostics) { return input; } + private protected Compilation RunGenerators(Compilation input, ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts, DiagnosticBag generatorDiagnostics) + { + GeneratorDriver? driver = null; + string cacheKey = string.Empty; + bool disableCache = Arguments.ParseOptions.Features.ContainsKey("disable-generator-cache") || string.IsNullOrWhiteSpace(Arguments.OutputFileName); + if (this.GeneratorDriverCache is object && !disableCache) + { + cacheKey = deriveCacheKey(); + driver = this.GeneratorDriverCache.TryGetDriver(cacheKey); + } + + driver ??= CreateGeneratorDriver(parseOptions, generators, analyzerConfigOptionsProvider, additionalTexts); + driver = driver.RunGeneratorsAndUpdateCompilation(input, out var compilationOut, out var diagnostics); + generatorDiagnostics.AddRange(diagnostics); + + if (!disableCache) + { + this.GeneratorDriverCache?.CacheGenerator(cacheKey, driver); + } + return compilationOut; + + string deriveCacheKey() + { + Debug.Assert(!string.IsNullOrWhiteSpace(Arguments.OutputFileName)); + + // CONSIDER: The only piece of the cache key that is required for correctness is the generators that were used. + // We set up the graph statically based on the generators, so as long as the generator inputs haven't + // changed we can technically run any project against another's cache and still get the correct results. + // Obviously that would remove the point of the cache, so we also key off of the output file name + // and output path so that collisions are unlikely and we'll usually get the correct cache for any + // given compilation. + + PooledStringBuilder sb = PooledStringBuilder.GetInstance(); + sb.Builder.Append(Arguments.GetOutputFilePath(Arguments.OutputFileName)); + foreach (var generator in generators) + { + // append the generator FQN and the MVID of the assembly it came from, so any changes will invalidate the cache + var type = generator.GetGeneratorType(); + sb.Builder.Append(type.AssemblyQualifiedName); + sb.Builder.Append(type.Assembly.ManifestModule.ModuleVersionId.ToString()); + } + return sb.ToStringAndFree(); + } + } + + private protected abstract GeneratorDriver CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray additionalTexts); private int RunCore(TextWriter consoleOutput, ErrorLogger? errorLogger, CancellationToken cancellationToken) { diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverCache.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverCache.cs new file mode 100644 index 0000000000000..679d738779468 --- /dev/null +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriverCache.cs @@ -0,0 +1,67 @@ +// 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; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Microsoft.CodeAnalysis +{ + internal sealed class GeneratorDriverCache + { + internal const int MaxCacheSize = 10; + + private readonly (string cacheKey, GeneratorDriver driver)[] _cachedDrivers = new (string, GeneratorDriver)[MaxCacheSize]; + + private readonly object _cacheLock = new object(); + + private int _cacheSize = 0; + + public GeneratorDriver? TryGetDriver(string cacheKey) => AddOrUpdateMostRecentlyUsed(cacheKey, driver: null); + + public void CacheGenerator(string cacheKey, GeneratorDriver driver) => AddOrUpdateMostRecentlyUsed(cacheKey, driver); + + public int CacheSize => _cacheSize; + + /// + /// Attempts to find a driver based on . If a matching driver is found in the + /// cache, or explicitly passed via , the cache is updated so that it is at the + /// head of the list. + /// + /// The key to lookup the driver by in the cache + /// An optional driver that should be cached, if not already found in the cache + /// + private GeneratorDriver? AddOrUpdateMostRecentlyUsed(string cacheKey, GeneratorDriver? driver) + { + lock (_cacheLock) + { + // try and find the driver if it's present + int i = 0; + for (; i < _cacheSize; i++) + { + if (_cachedDrivers[i].cacheKey == cacheKey) + { + driver ??= _cachedDrivers[i].driver; + break; + } + } + + // if we found it (or were passed a new one), update the cache so its at the head of the list + if (driver is not null) + { + for (i = Math.Min(i, MaxCacheSize - 1); i > 0; i--) + { + _cachedDrivers[i] = _cachedDrivers[i - 1]; + } + _cachedDrivers[0] = (cacheKey, driver); + _cacheSize = Math.Min(MaxCacheSize, _cacheSize + 1); + } + + return driver; + } + } + } +} diff --git a/src/Compilers/Server/VBCSCompiler/CSharpCompilerServer.cs b/src/Compilers/Server/VBCSCompiler/CSharpCompilerServer.cs index fe31e1832f00e..5dc1350149944 100644 --- a/src/Compilers/Server/VBCSCompiler/CSharpCompilerServer.cs +++ b/src/Compilers/Server/VBCSCompiler/CSharpCompilerServer.cs @@ -16,13 +16,13 @@ internal sealed class CSharpCompilerServer : CSharpCompiler { private readonly Func _metadataProvider; - internal CSharpCompilerServer(Func metadataProvider, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader) - : this(metadataProvider, Path.Combine(buildPaths.ClientDirectory, ResponseFileName), args, buildPaths, libDirectory, analyzerLoader) + internal CSharpCompilerServer(Func metadataProvider, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader, GeneratorDriverCache driverCache) + : this(metadataProvider, Path.Combine(buildPaths.ClientDirectory, ResponseFileName), args, buildPaths, libDirectory, analyzerLoader, driverCache) { } - internal CSharpCompilerServer(Func metadataProvider, string? responseFile, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader) - : base(CSharpCommandLineParser.Default, responseFile, args, buildPaths, libDirectory, analyzerLoader) + internal CSharpCompilerServer(Func metadataProvider, string? responseFile, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader, GeneratorDriverCache driverCache) + : base(CSharpCommandLineParser.Default, responseFile, args, buildPaths, libDirectory, analyzerLoader, driverCache) { _metadataProvider = metadataProvider; } diff --git a/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs b/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs index 06dd245c9283f..7ae08e2ad3203 100644 --- a/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs +++ b/src/Compilers/Server/VBCSCompiler/CompilerRequestHandler.cs @@ -62,6 +62,11 @@ internal sealed class CompilerServerHost : ICompilerServerHost public ICompilerServerLogger Logger { get; } + /// + /// A cache that can store generator drivers in order to enable incrementalism across builds for the lifetime of the server. + /// + private readonly GeneratorDriverCache _driverCache = new GeneratorDriverCache(); + internal CompilerServerHost(string clientDirectory, string sdkDirectory, ICompilerServerLogger logger) { ClientDirectory = clientDirectory; @@ -84,7 +89,8 @@ public bool TryCreateCompiler(in RunRequest request, BuildPaths buildPaths, [Not args: request.Arguments, buildPaths: buildPaths, libDirectory: request.LibDirectory, - analyzerLoader: AnalyzerAssemblyLoader); + analyzerLoader: AnalyzerAssemblyLoader, + _driverCache); return true; case LanguageNames.VisualBasic: compiler = new VisualBasicCompilerServer( @@ -92,7 +98,8 @@ public bool TryCreateCompiler(in RunRequest request, BuildPaths buildPaths, [Not args: request.Arguments, buildPaths: buildPaths, libDirectory: request.LibDirectory, - analyzerLoader: AnalyzerAssemblyLoader); + analyzerLoader: AnalyzerAssemblyLoader, + _driverCache); return true; default: compiler = null; diff --git a/src/Compilers/Server/VBCSCompiler/VisualBasicCompilerServer.cs b/src/Compilers/Server/VBCSCompiler/VisualBasicCompilerServer.cs index 3599ad566756d..0be389d526477 100644 --- a/src/Compilers/Server/VBCSCompiler/VisualBasicCompilerServer.cs +++ b/src/Compilers/Server/VBCSCompiler/VisualBasicCompilerServer.cs @@ -18,13 +18,13 @@ internal sealed class VisualBasicCompilerServer : VisualBasicCompiler { private readonly Func _metadataProvider; - internal VisualBasicCompilerServer(Func metadataProvider, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader) - : this(metadataProvider, Path.Combine(buildPaths.ClientDirectory, ResponseFileName), args, buildPaths, libDirectory, analyzerLoader) + internal VisualBasicCompilerServer(Func metadataProvider, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader, GeneratorDriverCache driverCache) + : this(metadataProvider, Path.Combine(buildPaths.ClientDirectory, ResponseFileName), args, buildPaths, libDirectory, analyzerLoader, driverCache) { } - internal VisualBasicCompilerServer(Func metadataProvider, string? responseFile, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader) - : base(VisualBasicCommandLineParser.Default, responseFile, args, buildPaths, libDirectory, analyzerLoader) + internal VisualBasicCompilerServer(Func metadataProvider, string? responseFile, string[] args, BuildPaths buildPaths, string? libDirectory, IAnalyzerAssemblyLoader analyzerLoader, GeneratorDriverCache driverCache) + : base(VisualBasicCommandLineParser.Default, responseFile, args, buildPaths, libDirectory, analyzerLoader, driverCache) { _metadataProvider = metadataProvider; } diff --git a/src/Compilers/Server/VBCSCompilerTests/TouchedFileLoggingTests.cs b/src/Compilers/Server/VBCSCompilerTests/TouchedFileLoggingTests.cs index abafacdbcfbaf..33f3d98b9f2c0 100644 --- a/src/Compilers/Server/VBCSCompilerTests/TouchedFileLoggingTests.cs +++ b/src/Compilers/Server/VBCSCompilerTests/TouchedFileLoggingTests.cs @@ -67,7 +67,8 @@ public void CSharpTrivialMetadataCaching() new[] { "/nologo", "/touchedfiles:" + touchedBase, source1 }, new BuildPaths(clientDirectory, _baseDirectory, RuntimeEnvironment.GetRuntimeDirectory(), Path.GetTempPath()), s_libDirectory, - new TestAnalyzerAssemblyLoader()); + new TestAnalyzerAssemblyLoader(), + driverCache: null); List expectedReads; List expectedWrites; @@ -116,7 +117,8 @@ public void VisualBasicTrivialMetadataCaching() new[] { "/nologo", "/touchedfiles:" + touchedBase, source1 }, new BuildPaths(clientDirectory, _baseDirectory, RuntimeEnvironment.GetRuntimeDirectory(), Path.GetTempPath()), s_libDirectory, - new TestAnalyzerAssemblyLoader()); + new TestAnalyzerAssemblyLoader(), + driverCache: null); List expectedReads; List expectedWrites; diff --git a/src/Compilers/Test/Utilities/CSharp/MockCSharpCompiler.cs b/src/Compilers/Test/Utilities/CSharp/MockCSharpCompiler.cs index 615d39a77a6d2..be90ad0ef1d3b 100644 --- a/src/Compilers/Test/Utilities/CSharp/MockCSharpCompiler.cs +++ b/src/Compilers/Test/Utilities/CSharp/MockCSharpCompiler.cs @@ -26,8 +26,8 @@ public MockCSharpCompiler(string responseFile, string workingDirectory, string[] { } - public MockCSharpCompiler(string responseFile, BuildPaths buildPaths, string[] args, ImmutableArray analyzers = default, ImmutableArray generators = default, AnalyzerAssemblyLoader loader = null) - : base(CSharpCommandLineParser.Default, responseFile, args, buildPaths, Environment.GetEnvironmentVariable("LIB"), loader ?? new DefaultAnalyzerAssemblyLoader()) + public MockCSharpCompiler(string responseFile, BuildPaths buildPaths, string[] args, ImmutableArray analyzers = default, ImmutableArray generators = default, AnalyzerAssemblyLoader loader = null, GeneratorDriverCache driverCache = null) + : base(CSharpCommandLineParser.Default, responseFile, args, buildPaths, Environment.GetEnvironmentVariable("LIB"), loader ?? new DefaultAnalyzerAssemblyLoader(), driverCache) { _analyzers = analyzers.NullToEmpty(); _generators = generators.NullToEmpty(); diff --git a/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb b/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb index 8e05ed7176cba..ecc22ddc8860e 100644 --- a/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb +++ b/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCompiler.vb @@ -22,8 +22,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Private ReadOnly _tempDirectory As String Private _additionalTextFiles As ImmutableArray(Of AdditionalTextFile) - Protected Sub New(parser As VisualBasicCommandLineParser, responseFile As String, args As String(), buildPaths As BuildPaths, additionalReferenceDirectories As String, analyzerLoader As IAnalyzerAssemblyLoader) - MyBase.New(parser, responseFile, args, buildPaths, additionalReferenceDirectories, analyzerLoader) + Protected Sub New(parser As VisualBasicCommandLineParser, responseFile As String, args As String(), buildPaths As BuildPaths, additionalReferenceDirectories As String, analyzerLoader As IAnalyzerAssemblyLoader, Optional driverCache As GeneratorDriverCache = Nothing) + MyBase.New(parser, responseFile, args, buildPaths, additionalReferenceDirectories, analyzerLoader, driverCache) _diagnosticFormatter = New CommandLineDiagnosticFormatter(buildPaths.WorkingDirectory, AddressOf GetAdditionalTextFiles) _additionalTextFiles = Nothing @@ -298,12 +298,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Next End Sub - Private Protected Overrides Function RunGenerators(input As Compilation, parseOptions As ParseOptions, generators As ImmutableArray(Of ISourceGenerator), analyzerConfigOptionsProvider As AnalyzerConfigOptionsProvider, additionalTexts As ImmutableArray(Of AdditionalText), diagnostics As DiagnosticBag) As Compilation - Dim driver = VisualBasicGeneratorDriver.Create(generators, additionalTexts, DirectCast(parseOptions, VisualBasicParseOptions), analyzerConfigOptionsProvider) - Dim compilationOut As Compilation = Nothing, generatorDiagnostics As ImmutableArray(Of Diagnostic) = Nothing - driver.RunGeneratorsAndUpdateCompilation(input, compilationOut, generatorDiagnostics) - diagnostics.AddRange(generatorDiagnostics) - Return compilationOut + Private Protected Overrides Function CreateGeneratorDriver(parseOptions As ParseOptions, generators As ImmutableArray(Of ISourceGenerator), analyzerConfigOptionsProvider As AnalyzerConfigOptionsProvider, additionalTexts As ImmutableArray(Of AdditionalText)) As GeneratorDriver + Return VisualBasicGeneratorDriver.Create(generators, additionalTexts, DirectCast(parseOptions, VisualBasicParseOptions), analyzerConfigOptionsProvider) End Function End Class