diff --git a/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs b/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs index 9febf4218e7e5..12dd252c7bec2 100644 --- a/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs +++ b/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs @@ -60,6 +60,9 @@ public SolutionChecksumUpdater( listener, shutdownToken); + // Use an equality comparer here as we will commonly get lots of change notifications that will all be + // associated with the same cancellation token controlling that batch of work. No need to enqueue the same + // token a huge number of times when we only need the single value of it when doing the work. _synchronizeWorkspaceQueue = new AsyncBatchingWorkQueue( DelayTimeSpan.NearImmediate, SynchronizePrimaryWorkspaceAsync, @@ -139,15 +142,17 @@ private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) private async ValueTask SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken) { + var solution = _workspace.CurrentSolution; var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); if (client == null) return; using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken)) { + var workspaceVersion = solution.WorkspaceVersion; await client.TryInvokeAsync( - _workspace.CurrentSolution, - (service, solution, cancellationToken) => service.SynchronizePrimaryWorkspaceAsync(solution, cancellationToken), + solution, + (service, solution, cancellationToken) => service.SynchronizePrimaryWorkspaceAsync(solution, workspaceVersion, cancellationToken), cancellationToken).ConfigureAwait(false); } } diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index 9ba50f5f37516..30022d8d436a0 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -137,7 +137,7 @@ public async Task TestDesignerAttributes() // Ensure remote workspace is in sync with normal workspace. var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, solution.WorkspaceVersion, CancellationToken.None); var callback = new DesignerAttributeComputerCallback(); @@ -188,7 +188,7 @@ public async Task TestDesignerAttributesUnsupportedLanguage() // Ensure remote workspace is in sync with normal workspace. var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, solution.WorkspaceVersion, CancellationToken.None); var callback = new DesignerAttributeComputerCallback(); @@ -359,8 +359,9 @@ public async Task TestRemoteWorkspaceCircularReferences() using var remoteWorkspace = new RemoteWorkspace(FeaturesTestCompositions.RemoteHost.GetHostServices()); // this shouldn't throw exception - var solution = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync( - remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo)); + var (solution, updated) = await remoteWorkspace.GetTestAccessor().TryUpdateWorkspaceCurrentSolutionAsync( + remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo), workspaceVersion: 1); + Assert.True(updated); Assert.NotNull(solution); } @@ -827,9 +828,10 @@ private static (Project project, ImmutableArray documents) GetProjectA private static async Task UpdatePrimaryWorkspace(RemoteHostClient client, Solution solution) { + var workspaceVersion = solution.WorkspaceVersion; await client.TryInvokeAsync( solution, - async (service, solutionInfo, cancellationToken) => await service.SynchronizePrimaryWorkspaceAsync(solutionInfo, cancellationToken), + async (service, solutionInfo, cancellationToken) => await service.SynchronizePrimaryWorkspaceAsync(solutionInfo, workspaceVersion, cancellationToken), CancellationToken.None); } diff --git a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs index 3e43f88c1d6c3..3f6703acfb73d 100644 --- a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs @@ -48,7 +48,7 @@ public async Task TestCreation() var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); } @@ -66,7 +66,7 @@ public async Task TestGetSolutionWithPrimaryFlag(bool updatePrimaryBranch) var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, cancellationToken: CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, solution.WorkspaceVersion, cancellationToken: CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); Assert.Equal(WorkspaceKind.RemoteWorkspace, synched.WorkspaceKind); @@ -88,7 +88,7 @@ public async Task TestStrongNameProvider() var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); - var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); var compilationOptions = solution.Projects.First().CompilationOptions; @@ -117,7 +117,7 @@ public async Task TestStrongNameProviderEmpty() var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); - var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); var compilationOptions = solution.Projects.First().CompilationOptions; @@ -141,8 +141,8 @@ public async Task TestCache() var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var first = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - var second = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var first = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); + var second = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); // same instance from cache Assert.True(object.ReferenceEquals(first, second)); @@ -344,49 +344,54 @@ public async Task TestRemoteWorkspace() var remoteSolution1 = await GetInitialOOPSolutionAsync(remoteWorkspace, assetProvider, solution1); - await Verify(remoteWorkspace, solution1, remoteSolution1); + await Verify(remoteWorkspace, solution1, remoteSolution1, expectRemoteSolutionToCurrent: true); + var version = solution1.WorkspaceVersion; // update remote workspace var currentSolution = remoteSolution1.WithDocumentText(remoteSolution1.Projects.First().Documents.First().Id, SourceText.From(code + " class Test2 { }")); - var oopSolution2 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); + var (oopSolution2, _) = await remoteWorkspace.GetTestAccessor().TryUpdateWorkspaceCurrentSolutionAsync(currentSolution, ++version); - await Verify(remoteWorkspace, currentSolution, oopSolution2); + await Verify(remoteWorkspace, currentSolution, oopSolution2, expectRemoteSolutionToCurrent: true); // move backward - await Verify(remoteWorkspace, remoteSolution1, await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(remoteSolution1)); + await Verify(remoteWorkspace, remoteSolution1, (await remoteWorkspace.GetTestAccessor().TryUpdateWorkspaceCurrentSolutionAsync(remoteSolution1, solution1.WorkspaceVersion)).solution, expectRemoteSolutionToCurrent: false); // move forward currentSolution = oopSolution2.WithDocumentText(oopSolution2.Projects.First().Documents.First().Id, SourceText.From(code + " class Test3 { }")); - var remoteSolution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); + var remoteSolution3 = (await remoteWorkspace.GetTestAccessor().TryUpdateWorkspaceCurrentSolutionAsync(currentSolution, ++version)).solution; - await Verify(remoteWorkspace, currentSolution, remoteSolution3); + await Verify(remoteWorkspace, currentSolution, remoteSolution3, expectRemoteSolutionToCurrent: true); // move to new solution backward var solutionInfo2 = await assetProvider.CreateSolutionInfoAsync(await solution1.CompilationState.GetChecksumAsync(CancellationToken.None), CancellationToken.None); var solution2 = remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo2); + Assert.False((await remoteWorkspace.GetTestAccessor().TryUpdateWorkspaceCurrentSolutionAsync( + solution2, solution1.WorkspaceVersion)).updated); // move to new solution forward - var solution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(solution2); + var (solution3, updated3) = await remoteWorkspace.GetTestAccessor().TryUpdateWorkspaceCurrentSolutionAsync( + solution2, ++version); Assert.NotNull(solution3); - await Verify(remoteWorkspace, solution1, solution3); + Assert.True(updated3); + await Verify(remoteWorkspace, solution1, solution3, expectRemoteSolutionToCurrent: true); static async Task GetInitialOOPSolutionAsync(RemoteWorkspace remoteWorkspace, AssetProvider assetProvider, Solution solution) { // set up initial solution var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, solution.WorkspaceVersion, CancellationToken.None); // get solution in remote host - return await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + return await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); } - static async Task Verify(RemoteWorkspace remoteWorkspace, Solution givenSolution, Solution remoteSolution) + static async Task Verify(RemoteWorkspace remoteWorkspace, Solution givenSolution, Solution remoteSolution, bool expectRemoteSolutionToCurrent) { // verify we got solution expected Assert.Equal(await givenSolution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // verify remote workspace got updated - Assert.Equal(remoteSolution, remoteWorkspace.CurrentSolution); + Assert.True(expectRemoteSolutionToCurrent == (remoteSolution == remoteWorkspace.CurrentSolution)); } } @@ -402,7 +407,7 @@ public async Task TestAddingProjectsWithExplicitOptions(bool useDefaultOptionVal solution = solution.RemoveProject(solution.ProjectIds.Single()); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: 0, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); // Add a C# project and a VB project, set some options, and check again @@ -424,7 +429,7 @@ public async Task TestAddingProjectsWithExplicitOptions(bool useDefaultOptionVal assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: 2, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); } @@ -442,7 +447,7 @@ public async Task TestFrozenSourceGeneratedDocument() // First sync the solution over that has a generator var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: 0, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); // Now freeze with some content @@ -452,7 +457,7 @@ public async Task TestFrozenSourceGeneratedDocument() assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution1); solutionChecksum = await frozenSolution1.CompilationState.GetChecksumAsync(CancellationToken.None); - synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: 1, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); // Try freezing with some different content from the original solution @@ -461,7 +466,7 @@ public async Task TestFrozenSourceGeneratedDocument() assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution2); solutionChecksum = await frozenSolution2.CompilationState.GetChecksumAsync(CancellationToken.None); - synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: 2, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); } @@ -488,7 +493,7 @@ public async Task TestPartialProjectSync_GetSolutionFirst() await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: solution.WorkspaceVersion, CancellationToken.None); Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); Assert.Equal(2, syncedFullSolution.Projects.Count()); @@ -496,13 +501,13 @@ public async Task TestPartialProjectSync_GetSolutionFirst() // Syncing project1 should do nothing as syncing the solution already synced it over. var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project1SyncedSolution.Projects.Count()); // Syncing project2 should do nothing as syncing the solution already synced it over. var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); } @@ -528,20 +533,20 @@ public async Task TestPartialProjectSync_GetSolutionLast() // Syncing project 1 should just since it over. await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project1SyncedSolution.Projects.Count()); Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); // Syncing project 2 should end up with only p2 synced over. await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project2SyncedSolution.Projects.Count()); // then syncing the whole project should now copy both over. await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: solution.WorkspaceVersion, CancellationToken.None); Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); Assert.Equal(2, syncedFullSolution.Projects.Count()); @@ -569,14 +574,14 @@ public async Task TestPartialProjectSync_GetDependentProjects1() await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project2SyncedSolution.Projects.Count()); Assert.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); // syncing project 3 should sync project 2 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project3SyncedSolution.Projects.Count()); } @@ -603,20 +608,20 @@ public async Task TestPartialProjectSync_GetDependentProjects2() // syncing P3 should since project P2 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project3SyncedSolution.Projects.Count()); // if we then sync just P2, we should still have only P2 in the synced cone await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project2SyncedSolution.Projects.Count()); AssertEx.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); // if we then sync just P1, we should only have it in its own cone. await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project1SyncedSolution.Projects.Count()); AssertEx.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); } @@ -645,19 +650,19 @@ public async Task TestPartialProjectSync_GetDependentProjects3() // syncing project3 should since project2 and project1 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(3, project3SyncedSolution.Projects.Count()); // syncing project2 should only have it and project 1. await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); // syncing project1 should only be itself await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project1SyncedSolution.Projects.Count()); } @@ -685,19 +690,19 @@ public async Task TestPartialProjectSync_GetDependentProjects4() // syncing project3 should since project2 and project1 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(3, project3SyncedSolution.Projects.Count()); // Syncing project2 should only have a cone with itself. await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project2SyncedSolution.Projects.Count()); // Syncing project1 should only have a cone with itself. await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project1SyncedSolution.Projects.Count()); } @@ -723,14 +728,14 @@ public async Task TestPartialProjectSync_Options1() // Syncing over project1 should give us 1 set of options on the OOP side. await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project1SyncedSolution.Projects.Count()); Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); // Syncing over project2 should also only be one set of options. await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(1, project2SyncedSolution.Projects.Count()); } @@ -756,7 +761,7 @@ public async Task TestPartialProjectSync_DoesNotSeeChangesOutsideOfCone() // Do the initial full sync await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: solution.WorkspaceVersion, CancellationToken.None); Assert.Equal(2, fullSyncedSolution.Projects.Count()); // Mutate both projects to each have a document in it. @@ -768,7 +773,7 @@ public async Task TestPartialProjectSync_DoesNotSeeChangesOutsideOfCone() { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project1SyncedSolution.Projects.Count()); var csharpProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); var vbProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); @@ -780,7 +785,7 @@ public async Task TestPartialProjectSync_DoesNotSeeChangesOutsideOfCone() { await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); var csharpProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); var vbProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); @@ -811,7 +816,7 @@ public async Task TestPartialProjectSync_AddP2PRef() // Do the initial full sync await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, workspaceVersion: solution.WorkspaceVersion, CancellationToken.None); Assert.Equal(2, fullSyncedSolution.Projects.Count()); // Mutate both projects to have a document in it, and add a p2p ref from project1 to project2 @@ -824,7 +829,7 @@ public async Task TestPartialProjectSync_AddP2PRef() { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(2, project1SyncedSolution.Projects.Count()); var project1Synced = project1SyncedSolution.GetRequiredProject(project1.Id); var project2Synced = project1SyncedSolution.GetRequiredProject(project2.Id); @@ -880,8 +885,8 @@ private static async Task VerifySolutionUpdate( var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); // update primary workspace - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); - var recoveredSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, solution.WorkspaceVersion, CancellationToken.None); + var recoveredSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); oldSolutionValidator?.Invoke(recoveredSolution); Assert.Equal(WorkspaceKind.RemoteWorkspace, recoveredSolution.WorkspaceKind); @@ -893,13 +898,13 @@ private static async Task VerifySolutionUpdate( await newSolution.AppendAssetMapAsync(map, CancellationToken.None); // get solution without updating primary workspace - var recoveredNewSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var recoveredNewSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(newSolutionChecksum, await recoveredNewSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // do same once updating primary workspace - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, newSolutionChecksum, CancellationToken.None); - var third = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, newSolutionChecksum, solution.WorkspaceVersion + 1, CancellationToken.None); + var third = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, workspaceVersion: -1, CancellationToken.None); Assert.Equal(newSolutionChecksum, await third.CompilationState.GetChecksumAsync(CancellationToken.None)); diff --git a/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs b/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs index 455c83c4177ed..0b3c0f3a0bc77 100644 --- a/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs +++ b/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs @@ -7,14 +7,15 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.CodeAnalysis.Remote; - -internal interface IRemoteAssetSynchronizationService +namespace Microsoft.CodeAnalysis.Remote { - /// - /// Synchronize data to OOP proactively so that the corresponding solution is often already available when features - /// call into it. - /// - ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, CancellationToken cancellationToken); - ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextChecksum, IEnumerable textChanges, CancellationToken cancellationToken); + internal interface IRemoteAssetSynchronizationService + { + /// + /// Synchronize data to OOP proactively so that the corresponding solution is often already available when + /// features call into it. + /// + ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, int workspaceVersion, CancellationToken cancellationToken); + ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextChecksum, IEnumerable textChanges, CancellationToken cancellationToken); + } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs index 3ac0f34a4bf9e..0471dfcde019e 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs @@ -25,6 +25,12 @@ internal sealed partial class RemoteWorkspace : Workspace /// private readonly SemaphoreSlim _gate = new(initialCount: 1); + /// + /// Used to make sure we never move remote workspace backward. this version is the WorkspaceVersion of primary + /// solution in client (VS) we are currently caching. + /// + private int _currentRemoteWorkspaceVersion = -1; + // internal for testing purposes. internal RemoteWorkspace(HostServices hostServices) : base(hostServices, WorkspaceKind.RemoteWorkspace) @@ -46,7 +52,7 @@ public AssetProvider CreateAssetProvider(Checksum solutionChecksum, SolutionAsse /// them to be pre-populated for feature requests that come in soon after this call completes. /// public async Task UpdatePrimaryBranchSolutionAsync( - AssetProvider assetProvider, Checksum solutionChecksum, CancellationToken cancellationToken) + AssetProvider assetProvider, Checksum solutionChecksum, int workspaceVersion, CancellationToken cancellationToken) { // See if the current snapshot we're pointing at is the same one the host wants us to sync to. If so, we // don't need to do anything. @@ -59,6 +65,7 @@ public async Task UpdatePrimaryBranchSolutionAsync( await RunWithSolutionAsync( assetProvider, solutionChecksum, + workspaceVersion, updatePrimaryBranch: true, implementation: static _ => ValueTaskFactory.FromResult(false), cancellationToken).ConfigureAwait(false); @@ -83,12 +90,13 @@ await RunWithSolutionAsync( Func> implementation, CancellationToken cancellationToken) { - return RunWithSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, implementation, cancellationToken); + return RunWithSolutionAsync(assetProvider, solutionChecksum, workspaceVersion: -1, updatePrimaryBranch: false, implementation, cancellationToken); } private async ValueTask<(Solution solution, T result)> RunWithSolutionAsync( AssetProvider assetProvider, Checksum solutionChecksum, + int workspaceVersion, bool updatePrimaryBranch, Func> implementation, CancellationToken cancellationToken) @@ -123,7 +131,7 @@ await RunWithSolutionAsync( try { inFlightSolution = GetOrCreateSolutionAndAddInFlightCount_NoLock( - assetProvider, solutionChecksum, updatePrimaryBranch); + assetProvider, solutionChecksum, workspaceVersion, updatePrimaryBranch); solutionTask = inFlightSolution.PreferredSolutionTask_NoLock; // We must have at least 1 for the in-flight-count (representing this current in-flight call). @@ -260,15 +268,34 @@ private Solution CreateSolutionFromInfo(SolutionInfo solutionInfo) } /// - /// Updates this workspace with the given . The solution returned is the actual - /// one the workspace now points to. + /// Attempts to update this workspace with the given . If this succeeds, will be returned in the tuple result as well as the actual solution that the workspace is + /// updated to point at. If we cannot update this workspace, then will be returned, + /// along with the solution passed in. The only time the solution can not be updated is if it would move backwards. /// - private async Task UpdateWorkspaceCurrentSolutionAsync( + private async Task TryUpdateWorkspaceCurrentSolutionAsync( + int workspaceVersion, + Solution newSolution, + CancellationToken cancellationToken) + { + var (solution, _) = await TryUpdateWorkspaceCurrentSolutionWorkerAsync(workspaceVersion, newSolution, cancellationToken).ConfigureAwait(false); + return solution; + } + + private async ValueTask<(Solution solution, bool updated)> TryUpdateWorkspaceCurrentSolutionWorkerAsync( + int workspaceVersion, Solution newSolution, CancellationToken cancellationToken) { using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) { + // Never move workspace backward + if (workspaceVersion <= _currentRemoteWorkspaceVersion) + return (newSolution, updated: false); + + _currentRemoteWorkspaceVersion = workspaceVersion; + // if either solution id or file path changed, then we consider it as new solution. Otherwise, // update the current solution in place. @@ -289,7 +316,7 @@ private async Task UpdateWorkspaceCurrentSolutionAsync( } }); - return newSolution; + return (newSolution, updated: true); } static bool IsAddingSolution(Solution oldSolution, Solution newSolution) @@ -311,17 +338,18 @@ public TestAccessor(RemoteWorkspace remoteWorkspace) public Solution CreateSolutionFromInfo(SolutionInfo solutionInfo) => _remoteWorkspace.CreateSolutionFromInfo(solutionInfo); - public Task UpdateWorkspaceCurrentSolutionAsync(Solution newSolution) - => _remoteWorkspace.UpdateWorkspaceCurrentSolutionAsync(newSolution, CancellationToken.None); + public ValueTask<(Solution solution, bool updated)> TryUpdateWorkspaceCurrentSolutionAsync(Solution newSolution, int workspaceVersion) + => _remoteWorkspace.TryUpdateWorkspaceCurrentSolutionWorkerAsync(workspaceVersion, newSolution, CancellationToken.None); public async ValueTask GetSolutionAsync( AssetProvider assetProvider, Checksum solutionChecksum, bool updatePrimaryBranch, + int workspaceVersion, CancellationToken cancellationToken) { var (solution, _) = await _remoteWorkspace.RunWithSolutionAsync( - assetProvider, solutionChecksum, updatePrimaryBranch, _ => ValueTaskFactory.FromResult(false), cancellationToken).ConfigureAwait(false); + assetProvider, solutionChecksum, workspaceVersion, updatePrimaryBranch, _ => ValueTaskFactory.FromResult(false), cancellationToken).ConfigureAwait(false); return solution; } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace_SolutionCaching.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace_SolutionCaching.cs index edfb339611d09..d6c82aee29f98 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace_SolutionCaching.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace_SolutionCaching.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; +using Microsoft.CodeAnalysis.Internal.Log; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Remote { + internal sealed partial class RemoteWorkspace { /// @@ -38,6 +40,7 @@ internal sealed partial class RemoteWorkspace private InFlightSolution GetOrCreateSolutionAndAddInFlightCount_NoLock( AssetProvider assetProvider, Checksum solutionChecksum, + int workspaceVersion, bool updatePrimaryBranch) { Contract.ThrowIfFalse(_gate.CurrentCount == 0); @@ -53,7 +56,8 @@ private InFlightSolution GetOrCreateSolutionAndAddInFlightCount_NoLock( // to compute the primary branch as well, let it know so it can start that now. if (updatePrimaryBranch) { - solution.TryKickOffPrimaryBranchWork_NoLock(this.UpdateWorkspaceCurrentSolutionAsync); + solution.TryKickOffPrimaryBranchWork_NoLock((disconnectedSolution, cancellationToken) => + this.TryUpdateWorkspaceCurrentSolutionAsync(workspaceVersion, disconnectedSolution, cancellationToken)); } CheckCacheInvariants_NoLock(); diff --git a/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs b/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs index 3f5c2974b6b0b..ea25f6948ee99 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs @@ -30,7 +30,7 @@ public RemoteAssetSynchronizationService(in ServiceConstructionArguments argumen { } - public ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, CancellationToken cancellationToken) + public ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, int workspaceVersion, CancellationToken cancellationToken) { return RunServiceAsync(async cancellationToken => { @@ -38,7 +38,7 @@ public ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, Can { var workspace = GetWorkspace(); var assetProvider = workspace.CreateAssetProvider(solutionChecksum, WorkspaceManager.SolutionAssetCache, SolutionAssetSource); - await workspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, cancellationToken).ConfigureAwait(false); + await workspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, workspaceVersion, cancellationToken).ConfigureAwait(false); } }, cancellationToken); }