-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Don't break if we fork a snapshot after freezing partial semantics #56890
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
Don't break if we fork a snapshot after freezing partial semantics #56890
Conversation
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationPair.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationPair.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationPair.cs
Show resolved
Hide resolved
The FinalState in the compilation tracker holds onto two compilations: the compilation with generated files (the "real" compilation) and the compilation without generated files, should we need to later re-run generators if any further forking were to happen. When we froze a snapshot with partial semantics, the code was assuming that no further forking would happen, so it incorrectly passed the compilation with generated files into the field that should hold the compilation without generated files; if we later forked, we'd run generators again and that would result in us trying to add generated trees that already existed. The fix is to correctly track both compilations through that path like anything else. To start making that process simpler, I introduce a CompilationPair type that just holds onto both and lets you mutate both at once. There's also an optimization to ensure that if we don't have any generated files we aren't actually forking Compilations twice for no reason, which was a previous guarantee we held. Fixes dotnet#56702
987059c to
1651a33
Compare
src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs
Show resolved
Hide resolved
So if we pass a frozen document to a completion provider, it will trigger the source generator to run at the moment it needs to make additional change and get an update compilation to the frozen document (e.g. trying to figure out the changes for the final commit)? |
We still had a few places where we were decomposing the GeneratorInfo type to pass it into the parameters for FinalizeCompilationAsync; it's easier if we just pass it around directly and process the values in FinalizeCompilationAsync.
This implements a policy change that once we freeze semantics for a document, we won't ever run geneators again, even if we further fork and replace that original document. This implemented by adding another boolean to ur GeneratorInfo: besides having a flag which indicates whether we don't need to rerun generators for this specific state, we also add a flag to say never run even if we do change.
|
@CyrusNajmabadi @genlu: I've pushed another set of changes to this PR to discuss about risk/reward tradeoffs here. Background: override completion, when it is producing the generated method, is forking a document to make the code changes. It is doing this on the frozen partial semantics we generally moved completion onto. My original commit fixed the fact that freeze-and-then-fork was just going to break, but it did mean that on the forked solution generators could run again if further semantics were asked about. Concern: it means we're still potentially going to run generators in a place where we were already concerned about performance. Even if the generator is fast, it's not clear if running it on a partial semantics snapshot is a great idea, since it might be missing trees or cross-project references. New Change: when we freeze semantics, we ensure we never run generators for that project again, even if there's some later forking of that project. If you look at the last commit first, the core change is pretty simple: we already have a bit which is "we have updated documents for this snapshot, we don't need to re-run" -- I'm adding a second bit that means to not rerun ever. Concern: the last commit is pretty simple, but commit before that was a bit less so since I needed to ensure we're actually keeping that bit around. There were various places we decomposed the GeneratorInfo structure. Generally, the cleanup commit is a good thing and I want to keep around, but I'm feeling iffy about wanting to land that in 17.0 just because of the amount of churn. If we're going to take this I'd really appreciate some careful eyes on this one. |
|
@jasonmalinowski @jinujoseph If this intended to go through QB, we should merge it tonight and get things flowing. 🙂 |
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
Outdated
Show resolved
Hide resolved
| GeneratorDriver? driver, | ||
| bool documentsAreFinal) | ||
| bool documentsAreFinal, | ||
| bool documentsAreFinalAndFrozen = 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.
would prefer this not be optional. callers should have to think about this.
...Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.CompilationTrackerState.cs
Show resolved
Hide resolved
...Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.CompilationTrackerState.cs
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
Show resolved
Hide resolved
src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs
Show resolved
Hide resolved
| GeneratorDriver? driver, | ||
| bool documentsAreFinal) | ||
| bool documentsAreFinal, | ||
| bool documentsAreFinalAndFrozen = 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.
Would you consider using a enum here excessive? It seems to me that having something like enum GeneratedDocumentsState with 3 members NotFinal, Final and FinalAndFrozen would make it clearer. i.e. you can only change the state in one direction.
Actually the one-direction comment is 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.
Not a bad idea, OK to defer just to limit churn here?
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.
Sure
Since we can reuse trees from prior runs, that might hide the fact we ran generators but then reused the prior tree anyways, when we really wanted to ensure generators didn't run at all.
Missed this comment; turns out that's unnecessary since even if we did the FinalState constructor itself also forces it to true. |

The FinalState in the compilation tracker holds onto two compilations: the compilation with generated files (the "real" compilation) and the compilation without generated files, should we need to later re-run generators if any further forking were to happen. When we froze a snapshot with partial semantics, the code was assuming that no further forking would happen, so it incorrectly passed the compilation with generated files into the field that should hold the compilation without generated files; if we later forked, we'd run generators again and that would result in us trying to add generated trees that already existed.
The fix is to correctly track both compilations through that path like anything else. To start making that process simpler, I introduce a CompilationPair type that just holds onto both and lets you mutate both at once. There's also an optimization to ensure that if we don't have any generated files we aren't actually forking Compilations twice for no reason, which was a previous guarantee we held.
Fixes #56702