-
Notifications
You must be signed in to change notification settings - Fork 4k
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
base: main
Are you sure you want to change the base?
Respond to cancellation when a generator is running, so we can still report time. #57122
Conversation
chsienki
commented
Oct 13, 2021
•
edited
Loading
edited
- Expose ElapsedTime publicly
- Create a derived type for run results that expresses cancellation
@@ -259,9 +260,17 @@ internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, Diagnos | |||
{ | |||
stateBuilder[i] = SetGeneratorException(MessageProvider, stateBuilder[i], state.Generators[i], ufe.InnerException, diagnosticsBag, generatorTimer.Elapsed); | |||
} | |||
catch (OperationCanceledException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking change. Should we put an option in the GeneratorOptions to enable/disable it?
Alternatively, if we did that, we could remove the 'WasCancelled' flag from the run results. Presumably the caller could just check the cancellation token that was passed in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simpler alternative is to consider the want to know about cancellation: essentially, what generator was running when it occurred. Could we simply stuff the information in the cancellation exception, so the host can extract it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So yes, this is a breaking change for all the people calling it which...might just be you and me? If you know of other callers that are broken then we should think about those.
That all said, even if we don't have people being broken, there's still a "principle of least surprise" here too where if you passed in a cancellation token but don't realize you have to check the flag, you are now getting broken results. My gut is saying make this behavior an opt-in flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably the caller could just check the cancellation token that was passed in?
Depends how you want to handle the case of we requested cancellation but it wasn't observed. If a poorly behaving generator didn't observe it's cancellation request, having a separate flag would let us know whether the result actually was usable, even if we're going to cancel off later on our side. Only checking our own token would mean we can't tell the difference.
It would also mean we better make telemetry and flag generators that we are requesting to cancel but are coming back slowly and not cancelling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we did want that latter telemetry, it'd be ideal if we could know if some generators did cancel and some completed, i.e. move the Cancelled flag to GeneratorRunResult. I'm not sure if that's easy/hard to do though, and if you told me it's the latter I'd absolutely believe it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move the Cancelled flag to GeneratorRunResult.
Hmm this is an interesting point, and fairly simple to do.
It occurs to me that we have a long standing issue that we're not being good cancellation citizens inside the generator driver either (#46528). I think if we did that we'd mostly be out of the 'cooperative' cancellation and no generator should be able to delay it too much.
Do we care much if the generator doesn't respect cancellation if we're able to intercept it? Edit: actually V1 generators running on top of the framework will have quite a big opportunity to stall where we can't intercept. It does seem like it would be useful to know if we successfully cancelled the generator or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For V2 since you have more parallelism does it mean the requests can run on some other thread while you've observed the cancellation on the original thread? Because yeah, those aren't directly impactful to our responsiveness, but it does still mean that there's still CPU cores being tied up doing work we won't ultimately observe the result of; those are still CPU cores that could be doing something else more productive, or at least could be doing nothing and not killing your battery.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simpler alternative is to consider the want to know about cancellation: essentially, what generator was running when it occurred. Could we simply stuff the information in the cancellation exception, so the host can extract it?
@chsienki I missed this earlier -- the framework does do things like this; there's TaskCanceledException that inherits from OperationCancelledException. I'd check with the framework folks though whether they really want others doing this or perhaps regret it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if some generators were running but others were complete, do we have any way to run only the cancelled ones again later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I was imagining just stuffing it in the Data
property bag. Essentially, 'when cancellation occurred, this is the generator that was running' (along with potentially, 'it was cooperative' / 'we had to wait').
With that approach we'd need to re-run the successful generators again. Which actually is a really good point and strongly makes me prefer the approach we're going for.
@@ -237,6 +237,7 @@ internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, Diagnos | |||
} | |||
constantSourcesBuilder.Free(); | |||
|
|||
bool cancelled = false; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
…report time. - Expose ElapsedTime publically - Expose WasCancelled flag
9c714a4
to
4b9d289
Compare
@@ -15,9 +15,18 @@ namespace Microsoft.CodeAnalysis | |||
{ | |||
public readonly IncrementalGeneratorOutputKind DisabledOutputs; | |||
|
|||
public readonly bool EnableGracefulCancellation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBD: naming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"EnableCancellationTiming"?
LastGeneratorRunning = cancelledOn; | ||
} | ||
|
||
public GeneratorRunResult? LastGeneratorRunning { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBD: naming
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't have more than one here if there's potential parallelism? I'm wondering if it makes sense to make this an ImmutableArray<> or something so we can have more down the road without revving the API.
@jasonmalinowski does this look ok for what you need in the IDE? Note we're still going to erroneously assign all the syntax walk time to which ever generator gets there first. Is that going to be a deal breaker we need to fix for the IDE work? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think. There's some small details but nothing blocking and nothing we can't easily do in a mop-up PR.
@@ -70,6 +70,17 @@ public ImmutableArray<SyntaxTree> GeneratedTrees | |||
return _lazyGeneratedTrees; | |||
} | |||
} | |||
|
|||
public sealed class CancelledResult : GeneratorDriverRunResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Want to call this CanceleldGeneratorDriverRunResult or something? "CancelledResult" is a bit vague, but my alternative seems like a mouthful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made it a nested class to avoid the naming issue too much, but happy to change it.
LastGeneratorRunning = cancelledOn; | ||
} | ||
|
||
public GeneratorRunResult? LastGeneratorRunning { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't have more than one here if there's potential parallelism? I'm wondering if it makes sense to make this an ImmutableArray<> or something so we can have more down the road without revving the API.
LastGeneratorRunning = cancelledOn; | ||
} | ||
|
||
public GeneratorRunResult? LastGeneratorRunning { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does null mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we'll always have a value, because we only return a cancelled result if the cancellation occurred during a generator run, but I didn't want to box us in to graceful cancellation at other points too. If we go with the array, then its easier as we can just return an empty one in that case.
@@ -15,9 +15,18 @@ namespace Microsoft.CodeAnalysis | |||
{ | |||
public readonly IncrementalGeneratorOutputKind DisabledOutputs; | |||
|
|||
public readonly bool EnableGracefulCancellation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"EnableCancellationTiming"?
LastGeneratorRunning = cancelledOn; | ||
} | ||
|
||
public GeneratorRunResult? LastGeneratorRunning { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this is a GeneratorRunResult, is there data that's usable other than the time? Will there be whatever results did come back up until the point of cancellation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. The results are empty. This was one of the things I was really unsure about and why I ended up going with a subtype rather than a property on the parent class.
It gets pretty confusing if we return partial results as we either:
- Only return the ones we actually ran
- You get a variable number of results back, and the host has to track which ones came back compared to the last run
- Host has to decide what to do with the 'missing' results.
- We return cached results for the ones that didn't run:
- You need to add more properties to the results, and have the host go through and check them. The cached values may not actually compile now, if the source compilation has changed significantly.
- That is true if we return nothing too, but at least everything is out of date, rather than some strange hybrid situation.
That wouldn't result in any API shape changes, right? I'd say no reason to block -- let's get this merged in so we can start our side of this, and we can bugfix later. |
@dotnet/roslyn-compiler for review please |
@chsienki is there an approved issue for the public API stuff? |