diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs index 4945a10606dc5..4ea35fba34985 100644 --- a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs @@ -2121,5 +2121,138 @@ class C { } driver = driver.RunGenerators(compilation); Assert.Single(referenceList, modifiedRef.Display); } + + [Fact] + public void Generator_Driver_Supports_Graceful_Cancellation() + { + var source = @" +class C { } +"; + var parseOptions = TestOptions.RegularPreview; + Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); + compilation.VerifyDiagnostics(); + Assert.Single(compilation.SyntaxTrees); + + CancellationTokenSource cts = new CancellationTokenSource(); + + var generator = new PipelineCallbackGenerator(ctx => + { + ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => cts.Cancel()); + }); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator.AsSourceGenerator() }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, true)); + driver = driver.RunGenerators(compilation, cts.Token); + + Assert.True(cts.Token.IsCancellationRequested); + + var runResult = driver.GetRunResult(); + Assert.IsType(runResult); + Assert.Empty(runResult.Results); + } + + [Fact] + public void Generator_Driver_Ignores_Cancelled_Results() + { + var source = @" +class C { } +"; + var parseOptions = TestOptions.RegularPreview; + Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); + compilation.VerifyDiagnostics(); + Assert.Single(compilation.SyntaxTrees); + + CancellationTokenSource cts = new CancellationTokenSource(); + + var generator = new PipelineCallbackGenerator(ctx => + { + ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => { context.AddSource("gen", ""); cts.Cancel(); }); + }); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator.AsSourceGenerator() }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, true)); + driver = driver.RunGenerators(compilation, cts.Token); + + Assert.True(cts.Token.IsCancellationRequested); + + var runResult = driver.GetRunResult(); + Assert.IsType(runResult); + Assert.Empty(runResult.Results); + } + + [Fact] + public void Generator_Driver_Does_Not_Run_Generators_After_Cancellation() + { + var source = @" +class C { } +"; + var parseOptions = TestOptions.RegularPreview; + Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); + compilation.VerifyDiagnostics(); + Assert.Single(compilation.SyntaxTrees); + + CancellationTokenSource cts = new CancellationTokenSource(); + + bool invoked = false; + var generator = new PipelineCallbackGenerator(ctx => + { + ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => cts.Cancel()); + }); + + var generator2 = new PipelineCallbackGenerator2(ctx => + { + ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => invoked = true); + }); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator.AsSourceGenerator(), generator2.AsSourceGenerator() }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, true)); + driver = driver.RunGenerators(compilation, cts.Token); + + Assert.True(cts.Token.IsCancellationRequested); + + var runResult = driver.GetRunResult(); + Assert.IsType(runResult); + Assert.Empty(runResult.Results); + Assert.False(invoked); + } + + [Fact] + public void Generator_Driver_Uses_Partial_Results_From_Previous_Cancellation() + { + var source = @" +class C { } +"; + var parseOptions = TestOptions.RegularPreview; + Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions); + compilation.VerifyDiagnostics(); + Assert.Single(compilation.SyntaxTrees); + + CancellationTokenSource cts = new CancellationTokenSource(); + int callCount = 0; + + var generator = new PipelineCallbackGenerator(ctx => + { + ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => { callCount++; context.AddSource("gen1", ""); }); + }); + + var generator2 = new PipelineCallbackGenerator2(ctx => + { + ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => cts.Cancel()); + }); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator.AsSourceGenerator(), generator2.AsSourceGenerator() }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, true)); + driver = driver.RunGenerators(compilation, cts.Token); + + Assert.True(cts.Token.IsCancellationRequested); + + var runResult = driver.GetRunResult(); + Assert.IsType(runResult); + Assert.Empty(runResult.Results); + Assert.Equal(1, callCount); + + // re-run with the partial results and confirm that the cached results are not re-computed, but we still get run results + driver = driver.RunGenerators(compilation, cancellationToken: default); + var runResult2 = driver.GetRunResult(); + + Assert.Equal(2, runResult2.Results.Length); + Assert.Equal(1, callCount); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs index 816d4e3c1783a..87e53b4e3f872 100644 --- a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/StateTableTests.cs @@ -401,15 +401,16 @@ private DriverStateTable.Builder GetBuilder(DriverStateTable previous) { var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10); var c = CSharpCompilation.Create("empty"); - var state = new GeneratorDriverState(options, + var state = new GeneratorDriverState(new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None), + options, CompilerAnalyzerConfigOptionsProvider.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, previous, - disabledOutputs: IncrementalGeneratorOutputKind.None, - runtime: TimeSpan.Zero); + elapsedTime: TimeSpan.Zero, + cancelled: false); return new DriverStateTable.Builder(c, state, ImmutableArray.Empty); } diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 6340112e3119e..e92ffc0382cf7 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -13,9 +13,10 @@ Microsoft.CodeAnalysis.GeneratorDriver.WithUpdatedParseOptions(Microsoft.CodeAna Microsoft.CodeAnalysis.GeneratorDriverOptions Microsoft.CodeAnalysis.GeneratorDriverOptions.GeneratorDriverOptions() -> void Microsoft.CodeAnalysis.GeneratorDriverOptions.GeneratorDriverOptions(Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind disabledOutputs) -> void -Microsoft.CodeAnalysis.GeneratorDriverRunResult.Cancelled.get -> bool +Microsoft.CodeAnalysis.GeneratorDriverOptions.GeneratorDriverOptions(Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind disabledOutputs, bool enableGracefulCancellation) -> void +Microsoft.CodeAnalysis.GeneratorDriverRunResult.CancelledResult +Microsoft.CodeAnalysis.GeneratorDriverRunResult.CancelledResult.LastGeneratorRunning.get -> Microsoft.CodeAnalysis.GeneratorRunResult? Microsoft.CodeAnalysis.GeneratorDriverRunResult.ElapsedTime.get -> System.TimeSpan -Microsoft.CodeAnalysis.GeneratorDriverRunResult.WasCancelled.get -> bool Microsoft.CodeAnalysis.GeneratorExtensions Microsoft.CodeAnalysis.GeneratorRunResult.ElapsedTime.get -> System.TimeSpan Microsoft.CodeAnalysis.IFieldSymbol.FixedSize.get -> int @@ -126,6 +127,7 @@ Microsoft.CodeAnalysis.SyntaxValueProvider.CreateSyntaxProvider(System.Func void override Microsoft.CodeAnalysis.Text.TextChangeRange.ToString() -> string! readonly Microsoft.CodeAnalysis.GeneratorDriverOptions.DisabledOutputs -> Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind +readonly Microsoft.CodeAnalysis.GeneratorDriverOptions.EnableGracefulCancellation -> bool static Microsoft.CodeAnalysis.CaseInsensitiveComparison.Compare(System.ReadOnlySpan left, System.ReadOnlySpan right) -> int static Microsoft.CodeAnalysis.CaseInsensitiveComparison.Equals(System.ReadOnlySpan left, System.ReadOnlySpan right) -> bool override Microsoft.CodeAnalysis.Diagnostics.AnalyzerFileReference.GetGenerators(string! language) -> System.Collections.Immutable.ImmutableArray diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs index a021899860410..c55883eb6bd20 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs @@ -36,7 +36,7 @@ internal GeneratorDriver(GeneratorDriverState state) internal GeneratorDriver(ParseOptions parseOptions, ImmutableArray generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray additionalTexts, GeneratorDriverOptions driverOptions) { (var filteredGenerators, var incrementalGenerators) = GetIncrementalGenerators(generators, SourceExtension); - _state = new GeneratorDriverState(parseOptions, optionsProvider, filteredGenerators, incrementalGenerators, additionalTexts, ImmutableArray.Create(new GeneratorState[filteredGenerators.Length]), DriverStateTable.Empty, driverOptions.DisabledOutputs, elapsedTime: TimeSpan.Zero, cancelled: false); + _state = new GeneratorDriverState(driverOptions, parseOptions, optionsProvider, filteredGenerators, incrementalGenerators, additionalTexts, ImmutableArray.Create(new GeneratorState[filteredGenerators.Length]), DriverStateTable.Empty, elapsedTime: TimeSpan.Zero, cancelled: false); } public GeneratorDriver RunGenerators(Compilation compilation, CancellationToken cancellationToken = default) @@ -129,6 +129,13 @@ public GeneratorDriver WithUpdatedAnalyzerConfigOptions(AnalyzerConfigOptionsPro public GeneratorDriverRunResult GetRunResult() { + if (_state.Cancelled) + { + Debug.Assert(_state.Options.EnableGracefulCancellation); + + return new GeneratorDriverRunResult.CancelledResult(_state.ElapsedTime, getCancelledRunResult(_state)); + } + var results = _state.Generators.ZipAsArray( _state.GeneratorStates, (generator, generatorState) @@ -137,7 +144,7 @@ public GeneratorDriverRunResult GetRunResult() exception: generatorState.Exception, generatedSources: getGeneratorSources(generatorState), elapsedTime: generatorState.ElapsedTime)); - return new GeneratorDriverRunResult(results, _state.ElapsedTime, _state.Cancelled); + return new GeneratorDriverRunResult(results, _state.ElapsedTime); static ImmutableArray getGeneratorSources(GeneratorState generatorState) { @@ -152,6 +159,19 @@ static ImmutableArray getGeneratorSources(GeneratorState } return sources.ToImmutableAndFree(); } + + static GeneratorRunResult? getCancelledRunResult(GeneratorDriverState driverState) + { + for (int i = 0; i < driverState.GeneratorStates.Length; i++) + { + if (driverState.GeneratorStates[i].Cancelled) + { + return new GeneratorRunResult(driverState.Generators[i], ImmutableArray.Empty, ImmutableArray.Empty, exception: null, elapsedTime: driverState.GeneratorStates[i].ElapsedTime); + } + } + + return null; + } } internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, DiagnosticBag? diagnosticsBag, CancellationToken cancellationToken = default) @@ -260,11 +280,11 @@ internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, Diagnos { stateBuilder[i] = SetGeneratorException(MessageProvider, stateBuilder[i], state.Generators[i], ufe.InnerException, diagnosticsBag, generatorTimer.Elapsed); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (state.Options.EnableGracefulCancellation) { // when cancelled, we record the time it spent generating, but don't include anything that was partially generated. - // this allows us to know if a await runnning generator is frequently causing the cacnellation - stateBuilder[i] = new GeneratorState(generatorState.Info, generatorState.PostInitTrees, generatorState.InputNodes, generatorState.OutputNodes, ImmutableArray.Empty, ImmutableArray.Empty, generatorTimer.Elapsed); + // this allows us to know if a runnning generator is frequently causing the cancellation + stateBuilder[i] = new GeneratorState(generatorState.Info, generatorState.PostInitTrees, generatorState.InputNodes, generatorState.OutputNodes, ImmutableArray.Empty, ImmutableArray.Empty, generatorTimer.Elapsed, cancelled: true); cancelled = true; break; } @@ -281,9 +301,10 @@ private IncrementalExecutionContext UpdateOutputs(ImmutableArray sourceGenerators, ImmutableArray incrementalGenerators, ImmutableArray additionalTexts, ImmutableArray generatorStates, DriverStateTable stateTable, - IncrementalGeneratorOutputKind disabledOutputs, TimeSpan elapsedTime, bool cancelled) { @@ -27,9 +27,9 @@ internal GeneratorDriverState(ParseOptions parseOptions, GeneratorStates = generatorStates; AdditionalTexts = additionalTexts; ParseOptions = parseOptions; + Options = options; OptionsProvider = optionsProvider; StateTable = stateTable; - DisabledOutputs = disabledOutputs; ElapsedTime = elapsedTime; Cancelled = cancelled; Debug.Assert(Generators.Length == GeneratorStates.Length); @@ -80,11 +80,6 @@ internal GeneratorDriverState(ParseOptions parseOptions, internal readonly DriverStateTable StateTable; - /// - /// A bit field containing the output kinds that should not be produced by this generator driver. - /// - internal readonly IncrementalGeneratorOutputKind DisabledOutputs; - /// /// The time spent during the pass that created this state. /// @@ -95,6 +90,11 @@ internal GeneratorDriverState(ParseOptions parseOptions, /// internal readonly bool Cancelled; + /// + /// The set of options passed when this driver was created. + /// + internal readonly GeneratorDriverOptions Options; + internal GeneratorDriverState With( ImmutableArray? sourceGenerators = null, ImmutableArray? incrementalGenerators = null, @@ -103,11 +103,11 @@ internal GeneratorDriverState With( DriverStateTable? stateTable = null, ParseOptions? parseOptions = null, AnalyzerConfigOptionsProvider? optionsProvider = null, - IncrementalGeneratorOutputKind? disabledOutputs = null, TimeSpan? elapsedTime = null, bool? cancelled = null) { return new GeneratorDriverState( + Options, parseOptions ?? this.ParseOptions, optionsProvider ?? this.OptionsProvider, sourceGenerators ?? this.Generators, @@ -115,7 +115,6 @@ internal GeneratorDriverState With( additionalTexts ?? this.AdditionalTexts, generatorStates ?? this.GeneratorStates, stateTable ?? this.StateTable, - disabledOutputs ?? this.DisabledOutputs, elapsedTime ?? this.ElapsedTime, cancelled ?? this.Cancelled ); diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorState.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorState.cs index 251c8fd25b1f4..0641b77c785b7 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorState.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorState.cs @@ -32,7 +32,7 @@ public GeneratorState(GeneratorInfo info, ImmutableArray po /// Creates a new generator state that contains information, constant trees and an execution pipeline /// public GeneratorState(GeneratorInfo info, ImmutableArray postInitTrees, ImmutableArray inputNodes, ImmutableArray outputNodes) - : this(info, postInitTrees, inputNodes, outputNodes, ImmutableArray.Empty, ImmutableArray.Empty, exception: null, elapsedTime: TimeSpan.Zero) + : this(info, postInitTrees, inputNodes, outputNodes, ImmutableArray.Empty, ImmutableArray.Empty, exception: null, elapsedTime: TimeSpan.Zero, cancelled: false) { } @@ -40,19 +40,19 @@ public GeneratorState(GeneratorInfo info, ImmutableArray po /// Creates a new generator state that contains an exception and the associated diagnostic /// public GeneratorState(GeneratorInfo info, Exception e, Diagnostic error, TimeSpan elapsedTime) - : this(info, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Create(error), exception: e, elapsedTime) + : this(info, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Create(error), exception: e, elapsedTime, cancelled: false) { } /// /// Creates a generator state that contains results /// - public GeneratorState(GeneratorInfo info, ImmutableArray postInitTrees, ImmutableArray inputNodes, ImmutableArray outputNodes, ImmutableArray generatedTrees, ImmutableArray diagnostics, TimeSpan elapsedTime) - : this(info, postInitTrees, inputNodes, outputNodes, generatedTrees, diagnostics, exception: null, elapsedTime) + public GeneratorState(GeneratorInfo info, ImmutableArray postInitTrees, ImmutableArray inputNodes, ImmutableArray outputNodes, ImmutableArray generatedTrees, ImmutableArray diagnostics, TimeSpan elapsedTime, bool cancelled = false) + : this(info, postInitTrees, inputNodes, outputNodes, generatedTrees, diagnostics, exception: null, elapsedTime, cancelled) { } - private GeneratorState(GeneratorInfo info, ImmutableArray postInitTrees, ImmutableArray inputNodes, ImmutableArray outputNodes, ImmutableArray generatedTrees, ImmutableArray diagnostics, Exception? exception, TimeSpan elapsedTime) + private GeneratorState(GeneratorInfo info, ImmutableArray postInitTrees, ImmutableArray inputNodes, ImmutableArray outputNodes, ImmutableArray generatedTrees, ImmutableArray diagnostics, Exception? exception, TimeSpan elapsedTime, bool cancelled) { this.Initialized = true; this.PostInitTrees = postInitTrees; @@ -63,6 +63,7 @@ private GeneratorState(GeneratorInfo info, ImmutableArray p this.Diagnostics = diagnostics; this.Exception = exception; this.ElapsedTime = elapsedTime; + this.Cancelled = cancelled; } internal bool Initialized { get; } @@ -81,6 +82,8 @@ private GeneratorState(GeneratorInfo info, ImmutableArray p internal TimeSpan ElapsedTime { get; } + internal bool Cancelled { get; } + internal ImmutableArray Diagnostics { get; } } } diff --git a/src/Compilers/Core/Portable/SourceGeneration/RunResults.cs b/src/Compilers/Core/Portable/SourceGeneration/RunResults.cs index 938b6ce11146e..e0e23a58b746e 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/RunResults.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/RunResults.cs @@ -19,11 +19,10 @@ public class GeneratorDriverRunResult private ImmutableArray _lazyGeneratedTrees; - internal GeneratorDriverRunResult(ImmutableArray results, TimeSpan elapsedTime, bool cancelled) + internal GeneratorDriverRunResult(ImmutableArray results, TimeSpan elapsedTime) { this.Results = results; this.ElapsedTime = elapsedTime; - this.WasCancelled = cancelled; } /// @@ -36,16 +35,6 @@ internal GeneratorDriverRunResult(ImmutableArray results, Ti /// public TimeSpan ElapsedTime { get; } - - /// - /// if the generator pass was cancelled before completion. - /// - /// - /// These run results are likely to be incomplete, depending on when the cancellation was triggered, and - /// will only have entries for generators that succesfully ran or any generator that was running during cancellation. - /// - public bool WasCancelled { get; } - /// /// The s produced by all generators run during this generation pass. /// @@ -81,6 +70,17 @@ public ImmutableArray GeneratedTrees return _lazyGeneratedTrees; } } + + public sealed class CancelledResult : GeneratorDriverRunResult + { + internal CancelledResult(TimeSpan elapsedTime, GeneratorRunResult? cancelledOn) + : base(ImmutableArray.Empty, elapsedTime) + { + LastGeneratorRunning = cancelledOn; + } + + public GeneratorRunResult? LastGeneratorRunning { get; } + } } ///