Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respond to cancellation when a generator is running, so we can still report time. #57122

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2121,5 +2121,197 @@ class C { }
driver = driver.RunGenerators(compilation);
Assert.Single(referenceList, modifiedRef.Display);
}

[Fact]
public void Generator_Driver_Supports_Timed_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, enableCancellationTiming: true));
driver = driver.RunGenerators(compilation, cts.Token);

Assert.True(cts.Token.IsCancellationRequested);

var runResult = driver.GetRunResult();
Assert.IsType<GeneratorDriverRunResult.CanceledResult>(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(); });
}).AsSourceGenerator();

GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, enableCancellationTiming: true));
driver = driver.RunGenerators(compilation, cts.Token);

Assert.True(cts.Token.IsCancellationRequested);

var runResult = driver.GetRunResult();
var canceledRunResult = Assert.IsType<GeneratorDriverRunResult.CanceledResult>(runResult);
var result = Assert.Single(canceledRunResult.GeneratorsRunningAtCancellation);
Assert.Same(generator, result.Generator);
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, enableCancellationTiming: true));
driver = driver.RunGenerators(compilation, cts.Token);

Assert.True(cts.Token.IsCancellationRequested);

var runResult = driver.GetRunResult();
Assert.IsType<GeneratorDriverRunResult.CanceledResult>(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());
}).AsSourceGenerator();

GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator.AsSourceGenerator(), generator2 }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, enableCancellationTiming: true));
driver = driver.RunGenerators(compilation, cts.Token);

Assert.True(cts.Token.IsCancellationRequested);

var runResult = driver.GetRunResult();
var canceledRunResult = Assert.IsType<GeneratorDriverRunResult.CanceledResult>(runResult);
var result = Assert.Single(canceledRunResult.GeneratorsRunningAtCancellation);
Assert.Same(generator2, result.Generator);

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);
}

[Fact]
public void Generator_Driver_With_Multiple_Cancellations()
{
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();
CancellationTokenSource cts2 = new CancellationTokenSource();

var generator = new PipelineCallbackGenerator(ctx =>
{
ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => { cts.Cancel(); context.AddSource("gen1", ""); });
}).AsSourceGenerator();

var generator2 = new PipelineCallbackGenerator2(ctx =>
{
ctx.RegisterSourceOutput(ctx.CompilationProvider, (context, ct) => { cts2.Cancel(); context.AddSource("gen2", ""); });
}).AsSourceGenerator();

var generator3 = new CallbackGenerator(ctx => { }, ctx => ctx.AddSource("gen3", ""));

GeneratorDriver driver = CSharpGeneratorDriver.Create(new ISourceGenerator[] { generator, generator2, generator3 }, parseOptions: parseOptions, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, enableCancellationTiming: true));
driver = driver.RunGenerators(compilation, cts.Token);

Assert.True(cts.Token.IsCancellationRequested);

var runResult = driver.GetRunResult();
var canceledRunResult = Assert.IsType<GeneratorDriverRunResult.CanceledResult>(runResult);
var result = Assert.Single(canceledRunResult.GeneratorsRunningAtCancellation);
Assert.Same(generator, result.Generator);
Assert.Empty(runResult.Results);

// run a second time, with a different generator causing cancellation
driver = driver.RunGenerators(compilation, cts2.Token);
Assert.True(cts2.Token.IsCancellationRequested);

var runResult2 = driver.GetRunResult();
var canceledRunResult2 = Assert.IsType<GeneratorDriverRunResult.CanceledResult>(runResult2);
var result2 = Assert.Single(canceledRunResult2.GeneratorsRunningAtCancellation);
Assert.Same(generator2, result2.Generator);
Assert.Empty(runResult2.Results);

// run a third time with no cancellation
driver = driver.RunGenerators(compilation, cancellationToken: default);

var runResult3 = driver.GetRunResult();
Assert.Equal(3, runResult3.Results.Length);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISourceGenerator>.Empty,
ImmutableArray<IIncrementalGenerator>.Empty,
ImmutableArray<AdditionalText>.Empty,
ImmutableArray<GeneratorState>.Empty,
previous,
disabledOutputs: IncrementalGeneratorOutputKind.None,
runtime: TimeSpan.Zero);
elapsedTime: TimeSpan.Zero,
cancelled: false);

return new DriverStateTable.Builder(c, state, ImmutableArray<ISyntaxInputNode>.Empty);
}
Expand Down
6 changes: 6 additions & 0 deletions src/Compilers/Core/Portable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ 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.GeneratorDriverOptions.GeneratorDriverOptions(Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind disabledOutputs, bool enableCancellationTiming) -> void
Microsoft.CodeAnalysis.GeneratorDriverRunResult.CanceledResult
Microsoft.CodeAnalysis.GeneratorDriverRunResult.CanceledResult.GeneratorsRunningAtCancellation.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.GeneratorRunResult>
Microsoft.CodeAnalysis.GeneratorDriverRunResult.ElapsedTime.get -> System.TimeSpan
Microsoft.CodeAnalysis.GeneratorExtensions
Microsoft.CodeAnalysis.GeneratorRunResult.ElapsedTime.get -> System.TimeSpan
Microsoft.CodeAnalysis.IFieldSymbol.FixedSize.get -> int
Microsoft.CodeAnalysis.IFieldSymbol.IsExplicitlyNamedTupleElement.get -> bool
Microsoft.CodeAnalysis.GeneratorExecutionContext.SyntaxContextReceiver.get -> Microsoft.CodeAnalysis.ISyntaxContextReceiver?
Expand Down Expand Up @@ -122,6 +127,7 @@ Microsoft.CodeAnalysis.SyntaxValueProvider.CreateSyntaxProvider<T>(System.Func<M
Microsoft.CodeAnalysis.SyntaxValueProvider.SyntaxValueProvider() -> void
override Microsoft.CodeAnalysis.Text.TextChangeRange.ToString() -> string!
readonly Microsoft.CodeAnalysis.GeneratorDriverOptions.DisabledOutputs -> Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind
readonly Microsoft.CodeAnalysis.GeneratorDriverOptions.EnableCancellationTiming -> bool
static Microsoft.CodeAnalysis.CaseInsensitiveComparison.Compare(System.ReadOnlySpan<char> left, System.ReadOnlySpan<char> right) -> int
static Microsoft.CodeAnalysis.CaseInsensitiveComparison.Equals(System.ReadOnlySpan<char> left, System.ReadOnlySpan<char> right) -> bool
override Microsoft.CodeAnalysis.Diagnostics.AnalyzerFileReference.GetGenerators(string! language) -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.ISourceGenerator!>
Expand Down
43 changes: 38 additions & 5 deletions src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal GeneratorDriver(GeneratorDriverState state)
internal GeneratorDriver(ParseOptions parseOptions, ImmutableArray<ISourceGenerator> generators, AnalyzerConfigOptionsProvider optionsProvider, ImmutableArray<AdditionalText> 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, runtime: TimeSpan.Zero);
_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)
Expand Down Expand Up @@ -129,6 +129,13 @@ public GeneratorDriver WithUpdatedAnalyzerConfigOptions(AnalyzerConfigOptionsPro

public GeneratorDriverRunResult GetRunResult()
{
if (_state.Cancelled)
{
Debug.Assert(_state.Options.EnableCancellationTiming);

return new GeneratorDriverRunResult.CanceledResult(_state.ElapsedTime, getCanceledRunResult(_state));
}

var results = _state.Generators.ZipAsArray(
_state.GeneratorStates,
(generator, generatorState)
Expand All @@ -137,7 +144,7 @@ public GeneratorDriverRunResult GetRunResult()
exception: generatorState.Exception,
generatedSources: getGeneratorSources(generatorState),
elapsedTime: generatorState.ElapsedTime));
return new GeneratorDriverRunResult(results, _state.RunTime);
return new GeneratorDriverRunResult(results, _state.ElapsedTime);

static ImmutableArray<GeneratedSourceResult> getGeneratorSources(GeneratorState generatorState)
{
Expand All @@ -152,14 +159,30 @@ static ImmutableArray<GeneratedSourceResult> getGeneratorSources(GeneratorState
}
return sources.ToImmutableAndFree();
}

static ImmutableArray<GeneratorRunResult> getCanceledRunResult(GeneratorDriverState driverState)
{
for (int i = 0; i < driverState.GeneratorStates.Length; i++)
{
if (driverState.GeneratorStates[i].Cancelled)
{
// Today we always run in series, so there can only ever be a single cancelled generator.
// In the future we might have multiple generators running at the same time, so want to
// be forward compatible for that case by using an array.
return ImmutableArray.Create(new GeneratorRunResult(driverState.Generators[i], ImmutableArray<GeneratedSourceResult>.Empty, ImmutableArray<Diagnostic>.Empty, exception: null, elapsedTime: driverState.GeneratorStates[i].ElapsedTime));
}
}

return ImmutableArray<GeneratorRunResult>.Empty;
}
}

internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, DiagnosticBag? diagnosticsBag, CancellationToken cancellationToken = default)
{
// with no generators, there is no work to do
if (_state.Generators.IsEmpty)
{
return _state.With(stateTable: DriverStateTable.Empty, runTime: TimeSpan.Zero);
return _state.With(stateTable: DriverStateTable.Empty, elapsedTime: TimeSpan.Zero);
}

// run the actual generation
Expand Down Expand Up @@ -237,6 +260,7 @@ internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, Diagnos
}
constantSourcesBuilder.Free();

bool cancelled = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only track cancellation during driver execution, not during init etc. That means we still throw at those points, but not during execution. That seems... odd? Should we always return success when cancelled, and just have an empty run result?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not terribly worried about people doing terribly long things in init, just because it'd be so hard to do. But as long as nothing in this design boxes us out of changing that later, should be good.

var driverStateBuilder = new DriverStateTable.Builder(compilation, _state, syntaxInputNodes.ToImmutableAndFree(), cancellationToken);
for (int i = 0; i < state.IncrementalGenerators.Length; i++)
{
Expand All @@ -259,9 +283,17 @@ internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, Diagnos
{
stateBuilder[i] = SetGeneratorException(MessageProvider, stateBuilder[i], state.Generators[i], ufe.InnerException, diagnosticsBag, generatorTimer.Elapsed);
}
catch (OperationCanceledException) when (state.Options.EnableCancellationTiming)
{
// 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 runnning generator is frequently causing the cancellation
stateBuilder[i] = new GeneratorState(generatorState.Info, generatorState.PostInitTrees, generatorState.InputNodes, generatorState.OutputNodes, ImmutableArray<GeneratedSyntaxTree>.Empty, ImmutableArray<Diagnostic>.Empty, generatorTimer.Elapsed, cancelled: true);
cancelled = true;
break;
}
}

state = state.With(stateTable: driverStateBuilder.ToImmutable(), generatorStates: stateBuilder.ToImmutableAndFree(), runTime: timer.Elapsed);
state = state.With(stateTable: driverStateBuilder.ToImmutable(), generatorStates: stateBuilder.ToImmutableAndFree(), elapsedTime: timer.Elapsed, cancelled: cancelled);
return state;
}

Expand All @@ -272,9 +304,10 @@ private IncrementalExecutionContext UpdateOutputs(ImmutableArray<IIncrementalGen
foreach (var outputNode in outputNodes)
{
// if we're looking for this output kind, and it has not been explicitly disabled
if (outputKind.HasFlag(outputNode.Kind) && !_state.DisabledOutputs.HasFlag(outputNode.Kind))
if (outputKind.HasFlag(outputNode.Kind) && !_state.Options.DisabledOutputs.HasFlag(outputNode.Kind))
{
outputNode.AppendOutputs(context, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
}
return context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@ public readonly struct GeneratorDriverOptions
{
public readonly IncrementalGeneratorOutputKind DisabledOutputs;

public readonly bool EnableCancellationTiming;

public GeneratorDriverOptions(IncrementalGeneratorOutputKind disabledOutputs)
: this(disabledOutputs, enableCancellationTiming: false)
{
}

public GeneratorDriverOptions(IncrementalGeneratorOutputKind disabledOutputs, bool enableCancellationTiming)
{
DisabledOutputs = disabledOutputs;
EnableCancellationTiming = enableCancellationTiming;
}

}
}
Loading