From 651e59c525f5f94c9442cb855c61016894ca56e3 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 11:08:05 +1000 Subject: [PATCH 01/11] Give action block a name to assist debugging --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 2968f2a91af..99152e8e733 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -132,7 +132,8 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation target: DataflowBlockFactory.CreateActionBlock>( async update => await ExecuteUnderLockAsync(cancellationToken => OnSlicesChanged(update, cancellationToken)), _unconfiguredProject, - ProjectFaultSeverity.LimitedFunctionality), + ProjectFaultSeverity.LimitedFunctionality, + "LanguageServiceHostSlices {0}"), linkOptions: DataflowOption.PropagateCompletion, cancellationToken: cancellationToken), From 1b226adbfc6b29881c22c9ada30a50a718c41f6c Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 11:08:22 +1000 Subject: [PATCH 02/11] Add API docs to Workspace --- .../LanguageServices/Workspace.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs index a6082dcc8b6..967019186c0 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/Workspace.cs @@ -134,6 +134,10 @@ protected override Task DisposeCoreUnderLockAsync(bool initialized) return Task.CompletedTask; } + /// + /// Adds an object that will be disposed along with this instance. + /// + /// The object to dispose when this object is disposed. public void ChainDisposal(IDisposable disposable) { Verify.NotDisposed(this); @@ -141,6 +145,23 @@ public void ChainDisposal(IDisposable disposable) _disposableBag.Add(disposable); } + /// + /// Integrates project updates into the workspace. + /// + /// + /// + /// This method must always receive an evaluation update first. After that point, + /// both evaluation and build updates may arrive in any order, so long as values + /// of each type are ordered correctly. + /// + /// + /// Calls must not overlap. This method is not thread-safe. This method is designed + /// to be called from a dataflow ActionBlock, which will serialize calls, so we + /// needn't perform any locking or protection here. + /// + /// + /// The project update to integrate. + /// A task that completes when the update has been integrated. internal async Task OnWorkspaceUpdateAsync(IProjectVersionedValue update) { Verify.NotDisposed(this); From 67565b51ce5748d38d0d0d7da17455bb4a446adf Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 13:50:50 +1000 Subject: [PATCH 03/11] Remove duplicate validation GetPrimaryWorkspace already performs this validation. We don't need to do it twice. --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 99152e8e733..6a2f4d2f526 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -278,8 +278,6 @@ public async Task WriteAsync(Func action, CancellationToken to { token = _tasksService.LinkUnload(token); - await ValidateEnabledAsync(token); - Workspace workspace = await GetPrimaryWorkspaceAsync(token); await workspace.WriteAsync(action, token); @@ -289,8 +287,6 @@ public async Task WriteAsync(Func> action, Cancellatio { token = _tasksService.LinkUnload(token); - await ValidateEnabledAsync(token); - Workspace workspace = await GetPrimaryWorkspaceAsync(token); return await workspace.WriteAsync(action, token); From 88a853b738f9d950f43ae15d4e2290113164f85d Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 13:53:38 +1000 Subject: [PATCH 04/11] Join dataflow when waiting for project load --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 6a2f4d2f526..c2984ae4bc5 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -296,7 +296,10 @@ private async Task GetPrimaryWorkspaceAsync(CancellationToken cancell { await ValidateEnabledAsync(cancellationToken); - await WhenProjectLoaded(cancellationToken); + using (_joinableTaskCollection.Join()) + { + await WhenProjectLoaded(cancellationToken); + } return _primaryWorkspace ?? throw Assumes.Fail("Primary workspace unknown."); } From 3c94e95ada97b311c278ce7136641adacff5c671 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 13:54:26 +1000 Subject: [PATCH 05/11] Join dataflow work during validation Determining whether we are enabled or not may involve a switch to the UI thread. --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index c2984ae4bc5..1878ee824d0 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -266,10 +266,10 @@ public Task IsEnabledAsync(CancellationToken cancellationToken) public async Task WhenInitialized(CancellationToken token) { - await ValidateEnabledAsync(token); - using (_joinableTaskCollection.Join()) { + await ValidateEnabledAsync(token); + await _firstPrimaryWorkspaceSet.Task.WithCancellation(token); } } @@ -294,10 +294,10 @@ public async Task WriteAsync(Func> action, Cancellatio private async Task GetPrimaryWorkspaceAsync(CancellationToken cancellationToken) { - await ValidateEnabledAsync(cancellationToken); - using (_joinableTaskCollection.Join()) { + await ValidateEnabledAsync(cancellationToken); + await WhenProjectLoaded(cancellationToken); } From 709f5e24382a1122fc11d1b768bed6a6c0351282 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 13:55:16 +1000 Subject: [PATCH 06/11] Remove duplicate join WhenInitialized already joins dataflow work. No need to do this twice. --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 1878ee824d0..1e962310e76 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -333,10 +333,7 @@ public async Task AfterLoadInitialConfigurationAsync() // Ensure the project is not considered loaded until our first publication. Task result = _tasksService.PrioritizedProjectLoadedInHostAsync(async () => { - using (_joinableTaskCollection.Join()) - { - await WhenInitialized(_tasksService.UnloadCancellationToken); - } + await WhenInitialized(_tasksService.UnloadCancellationToken); }); // While we want make sure it's loaded before PrioritizedProjectLoadedInHost, From 35f396537c0fefaa62ad29052e54f4d57f37e43a Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 13:55:30 +1000 Subject: [PATCH 07/11] Convert method to local function --- .../LanguageServices/LanguageServiceHost.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 1e962310e76..97a5ababf05 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -298,22 +298,22 @@ private async Task GetPrimaryWorkspaceAsync(CancellationToken cancell { await ValidateEnabledAsync(cancellationToken); - await WhenProjectLoaded(cancellationToken); + await WhenProjectLoaded(); } return _primaryWorkspace ?? throw Assumes.Fail("Primary workspace unknown."); - } - private async Task WhenProjectLoaded(CancellationToken cancellationToken) - { - // The active configuration can change multiple times during initialization in cases where we've incorrectly - // guessed the configuration via our IProjectConfigurationDimensionsProvider3 implementation. - // Wait until that has been determined before we publish the wrong configuration. - await _tasksService.PrioritizedProjectLoadedInHost.WithCancellation(cancellationToken); - - // We block project load on initialization of the primary workspace. - // Therefore by this point we must have set the primary workspace. - System.Diagnostics.Debug.Assert(_firstPrimaryWorkspaceSet.Task is { IsCompleted: true, IsFaulted: false }); + async Task WhenProjectLoaded() + { + // The active configuration can change multiple times during initialization in cases where we've incorrectly + // guessed the configuration via our IProjectConfigurationDimensionsProvider3 implementation. + // Wait until that has been determined before we publish the wrong configuration. + await _tasksService.PrioritizedProjectLoadedInHost.WithCancellation(cancellationToken); + + // We block project load on initialization of the primary workspace. + // Therefore by this point we must have set the primary workspace. + System.Diagnostics.Debug.Assert(_firstPrimaryWorkspaceSet.Task is { IsCompleted: true, IsFaulted: false }); + } } #endregion From 3e8ae2fbcad7c4c6dddc783a7dec89b925bf5101 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 29 Sep 2022 13:56:17 +1000 Subject: [PATCH 08/11] Remove potentially redundant UI switch If `IVsShellServices` has already been initialised, we won't need the UI thread. Avoid the switch before calling it. --- .../VS/LanguageServices/LanguageServiceHostEnvironment.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs index b719b3655ea..4906d062427 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/LanguageServices/LanguageServiceHostEnvironment.cs @@ -19,8 +19,6 @@ public LanguageServiceHostEnvironment(IVsShellServices vsShell, JoinableTaskCont _isEnabled = new( async () => { - await joinableTaskContext.Factory.SwitchToMainThreadAsync(); - // If VS is running in command line mode (e.g. "devenv.exe /build my.sln"), // the language service host is not enabled. The one exception to this is // when we're populating a solution cache via "/populateSolutionCache". From b63e4434025bd4a9889024b6c806bac69607bc3a Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Fri, 30 Sep 2022 09:41:32 +1000 Subject: [PATCH 09/11] Don't lock for dataflow updates The action block serialises updates, so they won't overlap. There's no race condition with the dispose method either, as the dispose logic just tears down the dataflow links. --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 97a5ababf05..90f007f7998 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.LanguageServices; [Export(typeof(IWorkspaceWriter))] [Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] [AppliesTo(ProjectCapability.DotNetLanguageService)] -internal sealed class LanguageServiceHost : OnceInitializedOnceDisposedUnderLockAsync, IProjectDynamicLoadComponent, IWorkspaceWriter +internal sealed class LanguageServiceHost : OnceInitializedOnceDisposedAsync, IProjectDynamicLoadComponent, IWorkspaceWriter { private readonly TaskCompletionSource _firstPrimaryWorkspaceSet = new(); @@ -130,7 +130,7 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation // We track per-slice data via this source. _activeConfigurationGroupSubscriptionService.SourceBlock.SyncLinkOptions(), target: DataflowBlockFactory.CreateActionBlock>( - async update => await ExecuteUnderLockAsync(cancellationToken => OnSlicesChanged(update, cancellationToken)), + update => OnSlicesChanged(update, cancellationToken), _unconfiguredProject, ProjectFaultSeverity.LimitedFunctionality, "LanguageServiceHostSlices {0}"), @@ -341,7 +341,7 @@ public async Task AfterLoadInitialConfigurationAsync() _projectFaultHandler.Forget(result, _unconfiguredProject, ProjectFaultSeverity.LimitedFunctionality); } - protected override Task DisposeCoreUnderLockAsync(bool initialized) + protected override Task DisposeCoreAsync(bool initialized) { _firstPrimaryWorkspaceSet.TrySetCanceled(); From 72738401fffc534801977db53b9f1f28072b6f2d Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Fri, 30 Sep 2022 09:41:57 +1000 Subject: [PATCH 10/11] Ensure we cancel on project unload --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 90f007f7998..78b4daf3905 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -106,6 +106,9 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation return; } + // Ensure we also cancel on project unload + cancellationToken = _tasksService.LinkUnload(cancellationToken); + // We have one "workspace" per "slice". // // - A "workspace" models the project state that Roslyn needs for a specific configuration. From a4f25885a0706f7404a7b67e238a553508f9f64c Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Fri, 30 Sep 2022 09:53:43 +1000 Subject: [PATCH 11/11] Join upstream data sources --- .../ProjectSystem/LanguageServices/LanguageServiceHost.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs index 78b4daf3905..1f7d12c4bf7 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -140,6 +140,8 @@ protected override async Task InitializeCoreAsync(CancellationToken cancellation linkOptions: DataflowOption.PropagateCompletion, cancellationToken: cancellationToken), + ProjectDataSources.JoinUpstreamDataSources(_joinableTaskFactory, _projectFaultHandler, _activeConfiguredProjectProvider, _activeConfigurationGroupSubscriptionService), + new DisposableDelegate(() => { // Dispose all workspaces. Note that this happens within a lock, so we will not race with project updates.