Skip to content

Conversation

@CyrusNajmabadi
Copy link
Member

Followup to #72045

@ghost ghost added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Feb 13, 2024
{
// Otherwise, we're not finalized, or the document has changed. We'll create an in-progress state
// to represent the new state of the project, with all the necessary translation steps to
// incorporate the new document.
Copy link
Member Author

Choose a reason for hiding this comment

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

view with whitespace off.

Copy link
Member Author

Choose a reason for hiding this comment

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

note: teh logic here is very similar to the old logic. just operating on DocStates instead of trees. A lot of the diff is also because i updated the comments to reflect thsi. But i otherwise tried to keep the semantics the same as before.

// produced an empty one, and removed any documents, so inProgressProject.DocumentStates would
// have been empty originally.
pendingActions = [(inProgressProject, new CompilationAndGeneratorDriverTranslationAction.TouchDocumentAction(oldState, docState))];
inProgressProject = inProgressProject.UpdateDocument(docState, contentChanged: true);
Copy link
Member Author

Choose a reason for hiding this comment

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

note: i think i'm passing in inProgressProject property to the pending actions. it represents the state prior to performing the action.

Copy link
Member Author

Choose a reason for hiding this comment

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

and here we see teh core concept change. instead of just manipulating the compilations directly (And thus needing the trees up front), we instead leverage totally normal pending actions, and we create an inprogress state that still has a teeny bit more work to do. That work is going to be the same as today (namely parsing the syntax trees), but that work is now handled in the normal translation-step processing code we already have.

Also, nicely, instead of needing to get the syntax trees for the N linked-file siblings of this document up front, that work can be delayed until someone actually needs it (by asking for syntax trees for them, or for getting compilations for their projects).

Copy link
Member

Choose a reason for hiding this comment

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

(by asking for syntax trees for them, or for getting compilations for their projects).

This is one small thing I do like about this approach: if we have features that grab frozen partials right away, and then later decide they had to do no work, we didn't end up paying the cost up front.

Copy link
Member Author

Choose a reason for hiding this comment

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

just a small thing? :)


// This is likely a bug. It seems possible to pass out a partial compilation state that we don't
// properly record assembly symbols for.
metadataReferenceToProjectId = null;
Copy link
Member Author

Choose a reason for hiding this comment

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

i really liked being able to remove this. as we're now not transitioning directly to a FinalState, we don't need to ahve this computed.

compilationWithoutGeneratedDocuments,
compilationWithoutGeneratedDocuments.AddSyntaxTrees(generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken))));

// Now add in back a consistent set of project references. For project references
Copy link
Member Author

Choose a reason for hiding this comment

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

this glorious code went away. since we already do it when transitioning an inprogress state to a finalstate :)

Copy link
Member

Choose a reason for hiding this comment

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

Wonderful discovery!

CompilationTrackerGeneratorInfo generatorInfo,
Compilation? staleCompilationWithGeneratedDocuments,
ImmutableList<(ProjectState state, CompilationAndGeneratorDriverTranslationAction action)> pendingTranslationSteps)
ImmutableList<(ProjectState oldState, CompilationAndGeneratorDriverTranslationAction action)> pendingTranslationSteps)
Copy link
Member Author

Choose a reason for hiding this comment

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

naming for consistency.

HasSuccessfullyLoaded = hasSuccessfullyLoaded;

// As a policy, all partial-state projects are said to have incomplete references, since the
// state has no guarantees.
Copy link
Member Author

Choose a reason for hiding this comment

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

moved from a different location here, so that we're consistent on what this bit means wrt frozen states.

{
var allDocumentIds = @this.SolutionState.GetRelatedDocumentIds(documentId);
using var _ = ArrayBuilder<(DocumentState, SyntaxTree)>.GetInstance(allDocumentIds.Length, out var statesAndTrees);
using var _ = ArrayBuilder<DocumentState>.GetInstance(allDocumentIds.Length, out var documentStates);
Copy link
Member Author

Choose a reason for hiding this comment

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

much cleaner now. we just grab the document states we want to force to be in the frozen solution snapshot.

{
Contract.ThrowIfTrue(tree != null);
var frozenFinalState = finalState.WithIsFrozen();
return new CompilationTracker(this.ProjectState, frozenFinalState, this.SkeletonReferenceCache.Clone());
Copy link
Member Author

Choose a reason for hiding this comment

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

note: this logic is technically optional. i could just always make an inprogressstate here with no translation actions attached. however, we have tests that validate you get the same compilation instance back in that case and it felt good to preserve.

Specifically.

  1. GetCompilation from normal solution.
  2. Get document from that solution.
  3. freeze that document (without any changes)
  4. GetCompilation from that frozen document's project.

Tests validate you get teh exact same instance as before. So this maintains that invariant. Note: you will still get a frozen state, so further mutations will not run generators.

Copy link
Member

Choose a reason for hiding this comment

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

we have tests that validate you get the same compilation instance back in that case and it felt good to preserve.

I agree that seems worth preserving, but it seems a bit surprising to me that returning an InProgressState with no further translation actions and it frozen would have resulted in a different compilation being made. That feels like we have a bug in the finalization step that's recreating a compilation even if it doesn't need to.

But this is small enough to preserve that I don't object to this logic here.

}
// Transition us to a frozen in progress state. With the best compilations up to this point, and the
// pending actions we'll need to perform on it to get to the final state.
var inProgressState = InProgressState.Create(
Copy link
Member Author

Choose a reason for hiding this comment

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

this is core part of the change. Instead of going to be in a final state, where we manually did all the steps ourselves, we go back to a normal frozen in-progress-state where there are still 1-2 pending translations to perform in the standard fashion.

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review February 14, 2024 18:21
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner February 14, 2024 18:21
@CyrusNajmabadi
Copy link
Member Author

@ToddGrun @jasonmalinowski ptal.

}

public FinalCompilationTrackerState WithIsFrozen()
=> new(isFrozen: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

new

no check to return this if already frozen?

Copy link
Member Author

Choose a reason for hiding this comment

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

fair. i can do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

put into: #72111

// any future forks keep things frozen.
if (state is FinalCompilationTrackerState finalState)
{
var frozenFinalState = finalState.WithIsFrozen();
Copy link
Contributor

Choose a reason for hiding this comment

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

finalState

I assume we can't just return this here too if finalstate is already frozen?

Copy link
Member Author

Choose a reason for hiding this comment

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

put into: #72111

@CyrusNajmabadi
Copy link
Member Author

@ToddGrun can i make those changes in followup?

@ToddGrun
Copy link
Contributor

LGTM, but I'm going to defer approval to @jasonmalinowski as the metadataReference part of the change wasn't clear to me


metadataReferenceToProjectId = [];

foreach (var projectReference in this.ProjectState.ProjectReferences)
Copy link
Member Author

Choose a reason for hiding this comment

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

@ToddGrun So this code simply attempts to ensure that the p2p refs in the frozen view of the world point at something. because it's partial and we want to be fast, a p2p ref will point at the compilation without generated docs for the linked project.

previously, we did this work ehre. However, because we're now just making ordinary (albeit frozen) inprogressstates, we can get rid of this. Instead, when we then do the similar pass when transitioning an inprogressstate to a finalstate, we do this same work (deviating only slightly to preserve the isfrozen speedup).

}
else
{
var metadataReference = await compilationState.GetMetadataReferenceAsync(
Copy link
Member Author

Choose a reason for hiding this comment

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

@ToddGrun this loop (if you look upwards to the containing for-loop) is what is setting up p2p references. It stays the same for normal states, but is tweaked for the frozen case. In the frozen case we just use the compilation-without-generated-docs (which is why we can be fast and not async here).

Copy link
Contributor

@ToddGrun ToddGrun left a comment

Choose a reason for hiding this comment

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

:shipit:

var frozenFinalState = finalState.WithIsFrozen();
return new CompilationTracker(this.ProjectState, frozenFinalState, this.SkeletonReferenceCache.Clone());
}
else
Copy link
Contributor

Choose a reason for hiding this comment

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

else

Out of curiosity, why did you add this in the last commit?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just a style preference. I preferred the indentation

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

This turned out far more awesome than I anticipated, especially with your realization we can ditch all the duplicate code messing with references. Nice job!

compilationWithoutGeneratedDocuments,
compilationWithoutGeneratedDocuments.AddSyntaxTrees(generatorInfo.Documents.States.Values.Select(state => state.GetSyntaxTree(cancellationToken))));

// Now add in back a consistent set of project references. For project references
Copy link
Member

Choose a reason for hiding this comment

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

Wonderful discovery!

{
Contract.ThrowIfTrue(tree != null);
var frozenFinalState = finalState.WithIsFrozen();
return new CompilationTracker(this.ProjectState, frozenFinalState, this.SkeletonReferenceCache.Clone());
Copy link
Member

Choose a reason for hiding this comment

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

we have tests that validate you get the same compilation instance back in that case and it felt good to preserve.

I agree that seems worth preserving, but it seems a bit surprising to me that returning an InProgressState with no further translation actions and it frozen would have resulted in a different compilation being made. That feels like we have a bug in the finalization step that's recreating a compilation even if it doesn't need to.

But this is small enough to preserve that I don't object to this logic here.

var oldTree = oldState.GetSyntaxTree(cancellationToken);

compilationPair = compilationPair.ReplaceSyntaxTree(oldTree, tree);
pendingActions = [(inProgressProject, new CompilationAndGeneratorDriverTranslationAction.TouchDocumentAction(oldState, docState))];
Copy link
Member

Choose a reason for hiding this comment

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

You may wish to consider adding an assert that says:

  1. The old state had a syntax tree and
  2. That syntax tree is present in compilationPair.

Since if at any point that gets out of sync in the current form we'd know that right away (and it'd be easy to debug) since ReplaceSyntaxTree would have thrown right away. If that invariant were to get screwed up now, we won't know until the later call which is running the translation actions and we might have lost the state here of why we chose that state.

And to be very clear: I think the logic is fine, but just paranoia since bugs we've had in the past here are very hard to debug when the things go wrong only after this did the wrong thing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Paranoia is fine. I will add in followup pr

// produced an empty one, and removed any documents, so inProgressProject.DocumentStates would
// have been empty originally.
pendingActions = [(inProgressProject, new CompilationAndGeneratorDriverTranslationAction.TouchDocumentAction(oldState, docState))];
inProgressProject = inProgressProject.UpdateDocument(docState, contentChanged: true);
Copy link
Member

Choose a reason for hiding this comment

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

(by asking for syntax trees for them, or for getting compilations for their projects).

This is one small thing I do like about this approach: if we have features that grab frozen partials right away, and then later decide they had to do no work, we didn't end up paying the cost up front.

@jasonmalinowski
Copy link
Member

@CyrusNajmabadi If I had one general concern with this change, if there's one thing the old code did well (and I use "well" loosely here!) was the old code always immediately put us into FinalState, so it's obvious that a later call to GetCompilationAsync() won't do anything expensive. Now, we're deferring that a bit further. And that's good! But it does mean there's a risk we might accidentally let something else expensive slip in. Now if we were to regress that we might know easily enough from telemetry (especially @genlu pointing out completion got slower again!), but I wonder if we can add some extra asserts in the code or tests or something. But for example if a test today is calling FreezeWithDocument and then later calling GetCompilationAsync() that we absolutely know that GetCompilationAsync() should never have yielded, and the task returned should be completed. Because yes, there might have been a few translation actions to process, we had to do some work, but that should still never yield.

@CyrusNajmabadi
Copy link
Member Author

Rolling feedback into followup.

@CyrusNajmabadi CyrusNajmabadi merged commit ad97cb5 into dotnet:main Feb 16, 2024
@CyrusNajmabadi CyrusNajmabadi deleted the frozenPartialSyntaxTree branch February 16, 2024 00:19
@ghost ghost added this to the Next milestone Feb 16, 2024
@jjonescz jjonescz modified the milestones: Next, 17.10 P2 Feb 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants