-
Notifications
You must be signed in to change notification settings - Fork 230
Collapse Reset then Add to Update in project state synchronizer #10330
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
Conversation
| Configuration file path: '{configurationFilePath}' | ||
| """); | ||
| } | ||
| var projectKey = ProjectKey.FromString(FilePathNormalizer.GetNormalizedDirectoryName(args.ConfigurationFilePath)); |
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.
Could do a ProjectKey.From(ProjectConfigurationFileChangeEventArgs) overload?
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.
It seems a bit cumbersome to make the ProjectKey API dependent on ProjectConfigurationFileChangeEventArgs. I'd rather see a ProjectConfigurationFileChangeEventArgs.ToProjectKey() method.
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.
Good idea, I should have thought of that!
| { | ||
| [Fact] | ||
| public async Task ProjectConfigurationFileChanged_Removed_UnknownDocumentNoops() | ||
| public async Task ProjectConfigurationFileChanged_Removed_UntrackedProject_CallsUpdate() |
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 change to the logic of the synchronizer itself, where it will call Update, but Update will no-op for a non-existent project, so holistically the behaviour is the same
|
|
||
| [Fact] | ||
| public async Task ProjectConfigurationFileChanged_Changed_UntrackedProject_Noops() | ||
| public async Task ProjectConfigurationFileChanged_Changed_UntrackedProject_CallsUpdate() |
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.
As above, holistically the same
| public async Task ProjectConfigurationFileChanged_RemoveThenAdd_OnlyAdds() | ||
| public async Task ProjectConfigurationFileChanged_RemoveThenAdd_Updates() |
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 test didn't do what its name implied it did. It actually verified that if you Add then Change, you get an Add then Update.
The test now does what its name implies, which is actually testing the restored functionality of collapsing removes and adds into a single Update operation.
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.
Thank you! These tests made my eyes cross. 😄
DustinCampbell
left a comment
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 definitely like this approach better than my PR!
...Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs
Outdated
Show resolved
Hide resolved
| // and then we can process the most recent unique items as normal | ||
| for (var i = _indicesToSkip.Count - 1; i >= 0; i--) | ||
| { | ||
| itemsToProcess.RemoveAt(_indicesToSkip[i]); |
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.
❔Can we just skip indices in the loop below? That would avoid all of the array copying 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.
The problem is that we want to skip indicies before collapsing. So for example, if we create a new bin file, then update it, this method will see an Add, a Remove, then an Add. We need to convert the second Add to an Update, and skip index 1. If we do that in the loop that calls GetMostRecentUniqueItems though, we'll be actually processing a list that contains Remove then Add, because the first Add will have been de-duped. Skiping index 1 there is not what we want.
Your points about copying are totally fair though. I guess I can inline the logic from GetMostRecentUniqueItems into the the loop above this, and do it all in one backwards loop through the set.
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.
Okay this was a big BIG pain, so I haven't quite got there, but see what you think.
I did remove a hashset, and remove the most egregious array resizing that this RemoveAt method causes. I also fixed a bug in the previous code that I didn't realise I had :)
Also, and you might not like this, but I swapped from records to classes. We don't actually want value semantics for the work items, and in fact we don't get it anyway (RazorProjectInfo doesn't provide an equality comparer), and I'm not entirely convinced that the tests were even valid. They are reusing the same project info for all work items, but in the real world, for different project info instances, I think the generated GetHashCode would have returned different values, resulting in no actual de-duping at runtime, because the HashSet Contains checked wouldn't have even called the Equals method in the comparer.
...Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs
Outdated
Show resolved
Hide resolved
...Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectConfigurationStateSynchronizer.cs
Outdated
Show resolved
Hide resolved
| Configuration file path: '{configurationFilePath}' | ||
| """); | ||
| } | ||
| var projectKey = ProjectKey.FromString(FilePathNormalizer.GetNormalizedDirectoryName(args.ConfigurationFilePath)); |
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.
It seems a bit cumbersome to make the ProjectKey API dependent on ProjectConfigurationFileChangeEventArgs. I'd rather see a ProjectConfigurationFileChangeEventArgs.ToProjectKey() method.
| // end up. All creation logic is here in one place to ensure this is consistent. | ||
| public static ProjectKey From(HostProject hostProject) => new(hostProject.IntermediateOutputPath); | ||
| public static ProjectKey From(IProjectSnapshot project) => new(project.IntermediateOutputPath); | ||
| public static ProjectKey From(RazorProjectInfo project) => new(FilePathNormalizer.GetNormalizedDirectoryName(project.SerializedFilePath)); |
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.
Hey! You found a use for SerializedFilePath! I hope it's accurate. 😄
| public async Task ProjectConfigurationFileChanged_RemoveThenAdd_OnlyAdds() | ||
| public async Task ProjectConfigurationFileChanged_RemoveThenAdd_Updates() |
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.
Thank you! These tests made my eyes cross. 😄
|
|
||
| private TestProjectConfigurationStateSynchronizer GetSynchronizer(IRazorProjectService razorProjectService) | ||
| => new(razorProjectService, LoggerFactory, TestLanguageServerFeatureOptions.Instance, TimeSpan.FromMilliseconds(5)); | ||
| => new(razorProjectService, LoggerFactory, TestLanguageServerFeatureOptions.Instance, TimeSpan.FromMilliseconds(50)); |
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.
Why does this need to be 50 milliseconds? Is there a race?
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 race that leads to a bug, but on my laptop on battery the test where I add half a dozen changed events would sometimes end up processing two batches, which means UpdateProjectAsync would be called one more time than we expect. Though to be fair, the number of changed events I'm adding is a little pathological :)
Could just directly test the process method and avoid the queue for some tests though, since the event handler code is simpler now anyway, if you prefer?
…#10335) Fixes #10306 This change takes a different approach than #10308 and #10330 and #10332 because life was meant to be interesting. The original problem we were trying to solve is that when a Reset then Add came in, we would migrate all of the documents out of the project, then migrate them all back in again. The various solutions in the other PRs you can read about at your leisure, this one takes a rather simple approach: 1. Ignore everything but the most recent update for a specific project 2. If we need to update a project, but it doesn't exist, add it In the long run we should remove `IRazorProjectService.AddProjectAsync` entirely, and rename `IRazorProjectService.UpdateProjectAsync` to `ResetProjectAsync`, but doing that now would just conflict with other open PRs.
Fixes #10306
Alternative to #10308
This change keeps the AsyncBatchingWorkQueue in the project state synchronizer, but still collapses a Reset then Add to be an Update, so that we don't migrate every document onto the misc files project, and then back on to the real project, every time a new component is added.