Repository: dotnet/roslyn
Pull request #77920: Reduce main thread switches in VisualStudioProjectFactory.CreateAndAddToWorkspaceAsync
Author: login: ToddGrun
Labels: name: Area-IDE
name: untriaged
description: Issues and PRs which have not yet been triaged by a lead
Created: 2025-03-31T20:14:43Z | Merged: 2025-04-04T00:37:44Z

This method is called once for each project in the solution during solution open, and each call was essentially switching to the main thread twice (once explicitly, once via call to _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync). For my testing of opening Roslyn.sln, this removed over 600 main thread switches.

Instead:

1) Move the first main thread switch to just occur once in an initialization task. This task is joined when CreateAndAddToWorkspaceAsync is invoked to ensure that that initialization is completed.
2) Move the second main thread switch to instead only occur when the known uicontext state would have it's active state changed. This essentially only happens when opening the first or closing the last project for a language.


Diff:
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
index 92da315daac29..3b069ba9b6b24 100644
--- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
+++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
@@ -14,6 +14,7 @@
 using Microsoft.CodeAnalysis.Host.Mef;
 using Microsoft.CodeAnalysis.Workspaces.AnalyzerRedirecting;
 using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
+using Microsoft.Internal.VisualStudio.Shell.Interop;
 using Microsoft.VisualStudio.LanguageServices.ExternalAccess.VSTypeScript.Api;
 using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
 using Microsoft.VisualStudio.Shell;
@@ -35,7 +36,9 @@ internal sealed class VisualStudioProjectFactory : IVsTypeScriptVisualStudioProj
     private readonly ImmutableArray<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> _dynamicFileInfoProviders;
     private readonly IVisualStudioDiagnosticAnalyzerProviderFactory _vsixAnalyzerProviderFactory;
     private readonly ImmutableArray<IAnalyzerAssemblyRedirector> _analyzerAssemblyRedirectors;
-    private readonly IVsService<SVsSolution, IVsSolution2> _solution2;
+    private readonly IVsService<SVsBackgroundSolution, IVsBackgroundSolution> _solution;
+
+    private readonly JoinableTask<VisualStudioDiagnosticAnalyzerProvider> _initializationTask;
 
     [ImportingConstructor]
     [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
@@ -45,14 +48,33 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsBackgroundSolution, IVsBackgroundSolution> solution)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
-        _solution2 = solution2;
+        _solution = solution;
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                // yield if on the main thread, as the VisualStudioMetadataReferenceManager construction can be fairly expensive
+                // and we don't want the case where VisualStudioProjectFactory is constructed on the main thread to block on that.
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                return await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
     }
 
     public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
@@ -61,24 +83,7 @@ public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSys
     public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         string projectSystemName, string language, VisualStudioProjectCreationInfo creationInfo, CancellationToken cancellationToken)
     {
-        // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
-        // moved off we'll need to fix up it's constructor to be free-threaded.
-
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
-        _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
-
-        _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
-        _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
-
-#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
-        // Since we're on the UI thread here anyways, use that as an opportunity to grab the
-        // IVsSolution object and solution file path.
-        var solution = await _solution2.GetValueOrNullAsync(cancellationToken);
-        var solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
-            ? filePath
-            : null;
-
-        var vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(false);
+        var vsixAnalyzerProvider = await _initializationTask.JoinAsync(cancellationToken).ConfigureAwait(false);
 
         // The rest of this method can be ran off the UI thread. We'll only switch though if the UI thread isn't already blocked -- the legacy project
         // system creates project synchronously, and during solution load we've seen traces where the thread pool is sufficiently saturated that this
@@ -89,27 +94,24 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
             await TaskScheduler.Default;
         }
 
+        var solution = await _solution.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+
         // From this point on, we start mutating the solution.  So make us non cancellable.
-#pragma warning disable IDE0059 // Unnecessary assignment of a value
         cancellationToken = CancellationToken.None;
-#pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solution?.SolutionFileName;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
         var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, cancellationToken).ConfigureAwait(true);
 
         return project;
 
-#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
-
         static Guid GetSolutionSessionId()
         {
             var dataModelTelemetrySession = TelemetryService.DefaultSession;
diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
index 07d8fb1b5b2e8..10c5420385dd4 100644
--- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
+++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
@@ -92,9 +92,15 @@ internal abstract partial class VisualStudioWorkspaceImpl : VisualStudioWorkspac
     private readonly Dictionary<string, List<ProjectSystemProject>> _projectSystemNameToProjectsMap = [];
 
     /// <summary>
-    /// Only safe to use on the UI thread.
+    /// Mapping from language name to an existing UIContext's active state.
+    /// Only access when holding <see cref="_gate"/>
     /// </summary>
-    private readonly Dictionary<string, UIContext?> _languageToProjectExistsUIContext = [];
+    private readonly Dictionary<string, bool> _languageToProjectExistsUIContextState = [];
+
+    /// <summary>
+    /// Joinable task collection to await to ensure language ui contexts are updated.
+    /// </summary>
+    private readonly JoinableTaskCollection _updateUIContextJoinableTasks;
 
     private OpenFileTracker? _openFileTracker;
     internal IFileChangeWatcher FileChangeWatcher { get; }
@@ -131,6 +137,8 @@ public VisualStudioWorkspaceImpl(ExportProvider exportProvider, IAsyncServicePro
             exportProvider.GetExportedValue<ExternalErrorDiagnosticUpdateSource>(),
             isThreadSafe: true);
 
+        _updateUIContextJoinableTasks = new JoinableTaskCollection(_threadingContext.JoinableTaskContext);
+
         _workspaceListener = Services.GetRequiredService<IWorkspaceAsynchronousOperationListenerProvider>().GetListener();
     }
 
@@ -1562,12 +1570,35 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
     }
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
+    {
+        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
+        {
+            var isContextActive = _languageToProjectExistsUIContextState.GetOrAdd(language, false);
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage != isContextActive)
+            {
+                _languageToProjectExistsUIContextState[language] = projectExistsWithLanguage;
+
+                // Create a task to update the UI context, and add it to the task collection that all callers to 
+                // this method will wait on before returning.
+                var joinableTask = _threadingContext.JoinableTaskFactory.RunAsync(() => UpdateUIContextAsync(language, cancellationToken));
+
+                _updateUIContextJoinableTasks.Add(joinableTask);
+            }
+        }
+
+        // Ensure any pending ui context updates have occurred before returning
+        await _updateUIContextJoinableTasks.JoinTillEmptyAsync(cancellationToken).ConfigureAwait(false);
+    }
+
+    private async Task UpdateUIContextAsync(string language, CancellationToken cancellationToken)
     {
         await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        var uiContext = Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext();
 
         // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in
         // other scenarios.
@@ -1578,7 +1609,9 @@ internal async Task RefreshProjectExistsUIContextForLanguageAsync(string languag
         // thread, so that acts as a natural ordering mechanism here.  If, say, a BG piece of work was mutating this
         // solution (either adding or removing a project) then that work will also have enqueued the next refresh
         // operation on the UI thread.  So we'll always eventually reach a fixed point where the task for that
-        // language will check the latest CurrentSolution we have and will set the IsActive bit accordingly.
+        // language will check the latest CurrentSolution we have and will set the IsActive bit accordingly. We
+        // don't use the isContextActive value here specifically for this case as it may not reflect the desired
+        // value after the main thread switch.
         uiContext.IsActive = this.CurrentSolution.Projects.Any(p => p.Language == language);
     }
 }


Reviews:
user: login: CyrusNajmabadi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:27:39Z
user: login: CyrusNajmabadi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:27:52Z
user: login: CyrusNajmabadi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:28:48Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:38:08Z
user: login: jasonmalinowski
body: There's a bug around loading a different solution.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-31T20:40:31Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:43:53Z
user: login: CyrusNajmabadi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:44:16Z
user: login: CyrusNajmabadi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:45:15Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:50:38Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:50:59Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T20:58:45Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:40:01Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:42:06Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:51:29Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:51:48Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T21:57:51Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T22:26:09Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T23:27:56Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T23:29:25Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T23:39:19Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T16:36:32Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T19:45:51Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T19:56:13Z
user: login: jasonmalinowski
body: Looks good, but let's double check that UIContext is safe to grab off the UI thread.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T20:05:27Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T20:05:45Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T20:06:12Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T21:31:03Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T21:39:34Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:36:29Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:37:56Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:41:40Z
user: login: jasonmalinowski
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T21:12:41Z
user: login: jasonmalinowski
body: :shipit:
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T22:27:13Z
user: login: jasonmalinowski
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T22:29:12Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T22:35:21Z
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T22:35:50Z

Review comments:
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: This will only run once, which will break things badly if the user closes one solution and opens another. That said though, in the last few releases a bunch of the solution-loading mechanics in the shell were rewritten to be managed code, so maybe there's a free-threaded way to get this now. Or this could subscribe to an event and update it whenever that event fires.
created_at: 2025-03-31T20:26:14Z
author_association: MEMBER
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: If this is the only state that leaves this function (after you fix that issue about solution paths) then consider making the task return it and getting rid of the field, so nullable analysis gets cleaner.
created_at: 2025-03-31T20:27:23Z
author_association: MEMBER
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: CyrusNajmabadi
body: does RunAsync take a CT?
created_at: 2025-03-31T20:27:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: CyrusNajmabadi
body: any specific reason for alwaysYield?
created_at: 2025-03-31T20:27:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
+    }
 
-    public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
-        string projectSystemName, string language, VisualStudioProjectCreationInfo creationInfo, CancellationToken cancellationToken)
+    private async ValueTask UpdateUIContextsAsync(ImmutableSegmentedList<string> list, CancellationToken cancellationToken)
     {
-        // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
-        // moved off we'll need to fix up it's constructor to be free-threaded.
+        // This is not cancellable as we have already mutated the solution.
+        cancellationToken = CancellationToken.None;
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: Could the batching queue shouldn't be creating cancellable tokens here in the first place, right? Otherwise I'd be worried the queue might internally cancel work and that'd create problems.
created_at: 2025-03-31T20:28:07Z
author_association: MEMBER
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
+    }
 
-    public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
-        string projectSystemName, string language, VisualStudioProjectCreationInfo creationInfo, CancellationToken cancellationToken)
+    private async ValueTask UpdateUIContextsAsync(ImmutableSegmentedList<string> list, CancellationToken cancellationToken)
     {
-        // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
-        // moved off we'll need to fix up it's constructor to be free-threaded.
+        // This is not cancellable as we have already mutated the solution.
+        cancellationToken = CancellationToken.None;
 
         await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
-        _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
 
-        _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
-        _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+        foreach (var language in list)
+            await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, cancellationToken).ConfigureAwait(false);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: CyrusNajmabadi
body: odd that this is CA(false) since you just switched to the UI thread.
created_at: 2025-03-31T20:28:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
+    }
 
-    public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
-        string projectSystemName, string language, VisualStudioProjectCreationInfo creationInfo, CancellationToken cancellationToken)
+    private async ValueTask UpdateUIContextsAsync(ImmutableSegmentedList<string> list, CancellationToken cancellationToken)
     {
-        // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
-        // moved off we'll need to fix up it's constructor to be free-threaded.
+        // This is not cancellable as we have already mutated the solution.
+        cancellationToken = CancellationToken.None;
 
         await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
-        _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
 
-        _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
-        _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+        foreach (var language in list)
+            await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, cancellationToken).ConfigureAwait(false);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: Odd indeed!
created_at: 2025-03-31T20:38:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: I'm not sure if asynchronously updating this will cause issues, or whether we had a (soft) guarantee that when we return the context would be set. But there might be an even better approach here: we only need to update the contexts when the number of projects will go from zero to non-zero during a load. Which in our case of loading Roslyn as a benchmark would be a grand total of two. So maybe a small tweak to simply have it realize the update isn't necessary in the first place might be even better.
created_at: 2025-03-31T20:40:20Z
author_association: MEMBER
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: I didn't know about the costs of the code following it (specifically getting the VisualStudioMetadataReferenceManager) and didn't want to block construction of VisualStudioProjectFactory if it were called on the main thread.
created_at: 2025-03-31T20:43:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
+    }
 
-    public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
-        string projectSystemName, string language, VisualStudioProjectCreationInfo creationInfo, CancellationToken cancellationToken)
+    private async ValueTask UpdateUIContextsAsync(ImmutableSegmentedList<string> list, CancellationToken cancellationToken)
     {
-        // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
-        // moved off we'll need to fix up it's constructor to be free-threaded.
+        // This is not cancellable as we have already mutated the solution.
+        cancellationToken = CancellationToken.None;
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: CyrusNajmabadi
body: the batching queue always supports cancellation.  But it will only actually cancel in one of two cases:

1. The CT that it was passed in its constructor gets canceled.  That'st he threading context disposal token.  So it would be ok in that case.
2. someone does `.AddWork(x, cancelExistingWork: true)`.  This doesn't happen in this code afaict.  So you'd never be triggered by that.

Based on this, i think it's fine to actually check the CT and not do the CT.None bit.  You should only get canceled here during shutdown.  In which case you *do* want to stop working.
created_at: 2025-03-31T20:44:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: CyrusNajmabadi
body: that's fine.  please at least doc the concern in the code.  It can even say "we're not certain, but here's at least why we have this currently".  
created_at: 2025-03-31T20:45:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
+            });
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: I'm not seeing one
created_at: 2025-03-31T20:50:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: I'm curious if this worked before, I'll test it out.
created_at: 2025-03-31T20:50:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: The GetSolutionInfo call would differ after the solution changed. Need to handle that better.
created_at: 2025-03-31T20:58:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: It feels like the only way to know whether the update is necessary is to check the state of the UIContext, which would need to be done on the UI thread. It doesn't seem like just keeping track if we go to/from zero projects with the language name would handle cases where other code could change that UIContext's state.
created_at: 2025-03-31T21:40:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: IVsBackgroundSolution seems to work with the project types I've tested which allows obtaining the solution path off the main thread.
created_at: 2025-03-31T21:42:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                // Since we're on the UI thread here anyways, use that as an opportunity to grab the
+                // IVsSolution object and solution file path.
+                var solution = await _solution2.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true);
+                _solutionFilePath = solution != null && ErrorHandler.Succeeded(solution.GetSolutionInfo(out _, out var filePath, out _))
+                    ? filePath
+                    : null;
+
+                _vsixAnalyzerProvider = await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: Done!
created_at: 2025-03-31T21:51:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,40 +53,65 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsSolution, IVsSolution2> solution2,
+        IAsynchronousOperationListenerProvider listenerProvider)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
         _solution2 = solution2;
-    }
 
-    public Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(string projectSystemName, string language, CancellationToken cancellationToken)
-        => CreateAndAddToWorkspaceAsync(projectSystemName, language, new VisualStudioProjectCreationInfo(), cancellationToken);
+        _uiContextUpdateWorkQueue = new AsyncBatchingWorkQueue<string>(
+            DelayTimeSpan.NearImmediate,
+            UpdateUIContextsAsync,
+            StringComparer.OrdinalIgnoreCase,
+            listenerProvider.GetListener(FeatureAttribute.Workspace),
+            _threadingContext.DisposalToken);
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: Done!
created_at: 2025-03-31T21:51:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: I'm not seeing where this is even used (other than tests). Is there somewhere with IVT that is using these flags?
created_at: 2025-03-31T21:57:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: @jasonmalinowski -- do you have a preference between:

1) Loosen the requirement that uicontext.IsActive is set during CreateAndAddToWorkspaceAsync execution. Change the integration tests that look like they might rely on this to instead do a WaitForAllAsyncOperationsAsync.

2) Change RefreshProjectExistsUIContextForLanguageAsync to assume that no one else changes the UIContext.IsActive state and have it set the value when the number of projects for a language switches to/from zero.

3) Something else

I personally prefer 1 since I can't find any other users of these UIContexts, but I'm not too opinionated.
created_at: 2025-03-31T22:26:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: The UI contexts are used for things like enabling/disabling commands, and also to help with other components that want to be able to ask "is there a C# project even there before I load up all my Roslyn support?" for things like test explorer. We're the only one who should be setting it so this assumption:

>  to assume that no one else changes the UIContext.IsActive state

Is completely fine to assume I'd say.

I'd be worried about weakening the support because it could have downstream ramifications -- package loads and such can trigger on UIContexts being changed, and delaying that on some time delay could break integration tests, including divisional tests.
created_at: 2025-03-31T23:27:56Z
author_association: MEMBER
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: That said, if 2 is hard to write, maybe 1 is just the easier option.
created_at: 2025-03-31T23:29:25Z
author_association: MEMBER
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: I missed this question:

> Is there somewhere with IVT that is using these flags?

Anything can use the GUID for the context to ask if that context is active. I'm not sure if the GUIDs got formally documented but any other repo could have it.
created_at: 2025-03-31T23:39:19Z
author_association: MEMBER
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: Tried out 2), let me know if you were thinking along a different line for it. 

Note that only calls to CreateAndAddToWorkspaceAsync that will change the UIContext state wait for it to update before returning. Is this acceptable?
created_at: 2025-04-01T16:36:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,14 +48,33 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsBackgroundSolution, IVsBackgroundSolution> solution)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
-        _solution2 = solution2;
+        _solution = solution;
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                // yield if on the main thread, as the VisualStudioMetadataReferenceManager construction can be fairly expensive
+                // and we don't want the case where VisualStudioProjectFactory is constructed on the main thread to block on that.
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                return await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: You could switch this to ConfigureAwait(false), as otherwise you're going to pay for a switch back to the UI thread just to do the return.
created_at: 2025-04-02T19:45:51Z
author_association: MEMBER
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: Is fetching the UI context itself free-threaded? I feel like when this was originally written that wasn't the case.
created_at: 2025-04-02T19:56:12Z
author_association: MEMBER
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
 
-        // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in
-        // other scenarios.
-        if (uiContext == null || uiContext.IsZombie)
-            return;
+            // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in
+            // other scenarios.
+            if (uiContext == null || uiContext.IsZombie)
+                return;
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage == isContextActive)
+                return;
+
+            _languageToProjectExistsUIContext[language] = (uiContext, projectExistsWithLanguage);
+        }
+
+        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
         // Note: it's safe to read CurrentSolution here outside of any sort of lock.  We do all work here on the UI
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: It might be worth rephrasing this comment to make it clear it's not using the isContextActive boolean that was inside the lock, since there's no guarantee that if one logical thread goes from false -> true and another goes from true -> false that this last bit will run in the "right order". But since you're _not_ using it...it's fine.
created_at: 2025-04-02T20:05:08Z
author_association: MEMBER
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: (and if it's not, the fix is simple to just defer that bit to the UI thread...)
created_at: 2025-04-02T20:05:45Z
author_association: MEMBER
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: And also frankly now since we're avoiding unnecessary updates, you could just remove the UIContext from the map entirely, since we're only going to fetch it 'rarely'.
created_at: 2025-04-02T20:06:12Z
author_association: MEMBER
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: ToddGrun
body: I thought so because of this comment:

[https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/env/shell/PackageFramework/Framework/UIContextImpl.cs&version=GBmain&_a=contents&line=63&lineStyle=plain&lineEnd=70&lineStartColumn=1&lineEndColumn=1](https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/env/shell/PackageFramework/Framework/UIContextImpl.cs&version=GBmain&_a=contents&line=63&lineStyle=plain&lineEnd=70&lineStartColumn=1&lineEndColumn=1)
created_at: 2025-04-02T21:31:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,14 +48,33 @@ public VisualStudioProjectFactory(
         [ImportMany] IEnumerable<Lazy<IDynamicFileInfoProvider, FileExtensionsMetadata>> fileInfoProviders,
         IVisualStudioDiagnosticAnalyzerProviderFactory vsixAnalyzerProviderFactory,
         [ImportMany] IEnumerable<IAnalyzerAssemblyRedirector> analyzerAssemblyRedirectors,
-        IVsService<SVsSolution, IVsSolution2> solution2)
+        IVsService<SVsBackgroundSolution, IVsBackgroundSolution> solution)
     {
         _threadingContext = threadingContext;
         _visualStudioWorkspaceImpl = visualStudioWorkspaceImpl;
         _dynamicFileInfoProviders = fileInfoProviders.AsImmutableOrEmpty();
         _vsixAnalyzerProviderFactory = vsixAnalyzerProviderFactory;
         _analyzerAssemblyRedirectors = analyzerAssemblyRedirectors.AsImmutableOrEmpty();
-        _solution2 = solution2;
+        _solution = solution;
+
+        _initializationTask = _threadingContext.JoinableTaskFactory.RunAsync(
+            async () =>
+            {
+                var cancellationToken = _threadingContext.DisposalToken;
+
+                // HACK: Fetch this service to ensure it's still created on the UI thread; once this is
+                // moved off we'll need to fix up it's constructor to be free-threaded.
+
+                // yield if on the main thread, as the VisualStudioMetadataReferenceManager construction can be fairly expensive
+                // and we don't want the case where VisualStudioProjectFactory is constructed on the main thread to block on that.
+                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+                _visualStudioWorkspaceImpl.Services.GetRequiredService<VisualStudioMetadataReferenceManager>();
+
+                _visualStudioWorkspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
+                _visualStudioWorkspaceImpl.SubscribeToSourceGeneratorImpactingEvents();
+
+                return await _vsixAnalyzerProviderFactory.GetOrCreateProviderAsync(cancellationToken).ConfigureAwait(true);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: full disclosure: I have a tendency to get CA calls wrong. 

Given that, and that GetOrCreateProviderAsync does all it's work on the main thread, isn't a CA(true) here going to prevent a thread switch back to the thread pool? If it was CA(false), then it would be a switch back to the thread pool for sure, right?

created_at: 2025-04-02T21:39:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
 
-        // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in
-        // other scenarios.
-        if (uiContext == null || uiContext.IsZombie)
-            return;
+            // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in
+            // other scenarios.
+            if (uiContext == null || uiContext.IsZombie)
+                return;
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage == isContextActive)
+                return;
+
+            _languageToProjectExistsUIContext[language] = (uiContext, projectExistsWithLanguage);
+        }
+
+        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
         // Note: it's safe to read CurrentSolution here outside of any sort of lock.  We do all work here on the UI
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: ToddGrun
body: Appended to the comment.
created_at: 2025-04-03T16:36:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1563,16 +1564,29 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
     {
-        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
+        UIContext? uiContext;
 
-        var uiContext = _languageToProjectExistsUIContext.GetOrAdd(
-            language,
-            language => Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext());
+        using (_gate.DisposableWait(cancellationToken))
+        {
+            (uiContext, var isContextActive) = _languageToProjectExistsUIContext.GetOrAdd(
+                language,
+                language => (Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext(), false));
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: ToddGrun
body: Although I do believe this is safe because of the comment linked to above, I do prefer your idea of just retrieving the UIContext while on the main thread, instead of caching it. It simplifies the data structures, and the removes the dependency that creating the ui context is free threaded.
created_at: 2025-04-03T16:37:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: ToddGrun
body: > Note that only calls to CreateAndAddToWorkspaceAsync that will change the UIContext state wait for it to update before returning. Is this acceptable?

@jasonmalinowski -- this is my only remaining concern. Is this acceptable?
created_at: 2025-04-03T16:41:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,9 +92,10 @@ internal abstract partial class VisualStudioWorkspaceImpl : VisualStudioWorkspac
     private readonly Dictionary<string, List<ProjectSystemProject>> _projectSystemNameToProjectsMap = [];
 
     /// <summary>
-    /// Only safe to use on the UI thread.
+    /// Mapping from language name to an existing UIContext's active state.
+    /// Only access when holding _gate
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: Consider using a cref.
created_at: 2025-04-03T20:08:17Z
author_association: MEMBER
diff_hunk: @@ -92,9 +92,10 @@ internal abstract partial class VisualStudioWorkspaceImpl : VisualStudioWorkspac
     private readonly Dictionary<string, List<ProjectSystemProject>> _projectSystemNameToProjectsMap = [];
 
     /// <summary>
-    /// Only safe to use on the UI thread.
+    /// Mapping from language name to an existing UIContext's active state.
+    /// Only access when holding _gate
     /// </summary>
-    private readonly Dictionary<string, UIContext?> _languageToProjectExistsUIContext = [];
+    private readonly Dictionary<string, bool> _languageToProjectExistsUIContext = [];
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: ```suggestion
    private readonly Dictionary<string, bool> _languageToProjectExistsUIContextState = [];
```
created_at: 2025-04-03T20:08:24Z
author_association: MEMBER
diff_hunk: @@ -94,17 +127,16 @@ public async Task<ProjectSystemProject> CreateAndAddToWorkspaceAsync(
         cancellationToken = CancellationToken.None;
 #pragma warning restore IDE0059 // Unnecessary assignment of a value
 
-        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solutionFilePath;
+        _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = _solutionFilePath;
         _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId();
 
-        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, vsixAnalyzerProvider, _analyzerAssemblyRedirectors);
-        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo);
+        var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _vsixAnalyzerProvider!, _analyzerAssemblyRedirectors);
+        var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true);
 
         _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName);
 
         // Ensure that other VS contexts get accurate information that the UIContext for this language is now active.
-        // This is not cancellable as we have already mutated the solution.
-        await _visualStudioWorkspaceImpl.RefreshProjectExistsUIContextForLanguageAsync(language, CancellationToken.None);
+        _uiContextUpdateWorkQueue.AddWork(language);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
user: login: jasonmalinowski
body: My apologies I had missed that question.

That is a potential concern. My worry about this was mostly if this could introduce flakiness to integration tests -- some integration test loads a project, and then does something that implicitly checks the UI context and then fails if this hasn't been updated yet.

What might work, instead of just doing SwitchToMainThread outside the lock, instead have a JoinableTaskCollection where we can add a task to; everything always waits for that collection to be empty before returning.
created_at: 2025-04-03T20:35:18Z
author_association: MEMBER
diff_hunk: @@ -1562,12 +1570,32 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
     }
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
+    {
+        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
+        {
+            var isContextActive = _languageToProjectExistsUIContextState.GetOrAdd(language, false);
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage != isContextActive)
+            {
+                _languageToProjectExistsUIContextState[language] = projectExistsWithLanguage;
+
+                var joinableTask = _threadingContext.JoinableTaskFactory.RunAsync(() => UpdateUIContextAsync(language, cancellationToken));
+
+                _updateUIContextJoinableTasks.Add(joinableTask);
+            }
+        }
+
+        await _updateUIContextJoinableTasks.JoinTillEmptyAsync(cancellationToken).ConfigureAwait(false);
+    }
+
+    private async Task UpdateUIContextAsync(string language, CancellationToken cancellationToken)
     {
         await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: Do we still want/need the alwaysYield: true? It does mean this will never run inline in RefreshProjectExistsUIContextForLanguageAsync, but does mean that we'll take an extra transition if we might have already been on the UI thread....
created_at: 2025-04-03T22:27:07Z
author_association: MEMBER
diff_hunk: @@ -1562,12 +1570,32 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
     }
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
+    {
+        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
+        {
+            var isContextActive = _languageToProjectExistsUIContextState.GetOrAdd(language, false);
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage != isContextActive)
+            {
+                _languageToProjectExistsUIContextState[language] = projectExistsWithLanguage;
+
+                var joinableTask = _threadingContext.JoinableTaskFactory.RunAsync(() => UpdateUIContextAsync(language, cancellationToken));
+
+                _updateUIContextJoinableTasks.Add(joinableTask);
+            }
+        }
+
+        await _updateUIContextJoinableTasks.JoinTillEmptyAsync(cancellationToken).ConfigureAwait(false);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: jasonmalinowski
body: We might want to add a comment here just to explain we're trying to make a guarantee that when the project is added, we do get this context updated.
created_at: 2025-04-03T22:29:07Z
author_association: MEMBER
diff_hunk: @@ -1562,12 +1570,32 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
     }
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
+    {
+        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
+        {
+            var isContextActive = _languageToProjectExistsUIContextState.GetOrAdd(language, false);
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage != isContextActive)
+            {
+                _languageToProjectExistsUIContextState[language] = projectExistsWithLanguage;
+
+                var joinableTask = _threadingContext.JoinableTaskFactory.RunAsync(() => UpdateUIContextAsync(language, cancellationToken));
+
+                _updateUIContextJoinableTasks.Add(joinableTask);
+            }
+        }
+
+        await _updateUIContextJoinableTasks.JoinTillEmptyAsync(cancellationToken).ConfigureAwait(false);
+    }
+
+    private async Task UpdateUIContextAsync(string language, CancellationToken cancellationToken)
     {
         await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: ToddGrun
body: I'd prefer to keep it. I see this method always entered on a bg thread. I'd like to limit the amount of time that I'm holding onto _gate.
created_at: 2025-04-03T22:35:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1562,12 +1570,32 @@ internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
     }
 
     internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
+    {
+        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
+        {
+            var isContextActive = _languageToProjectExistsUIContextState.GetOrAdd(language, false);
+
+            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
+            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
+            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
+            if (projectExistsWithLanguage != isContextActive)
+            {
+                _languageToProjectExistsUIContextState[language] = projectExistsWithLanguage;
+
+                var joinableTask = _threadingContext.JoinableTaskFactory.RunAsync(() => UpdateUIContextAsync(language, cancellationToken));
+
+                _updateUIContextJoinableTasks.Add(joinableTask);
+            }
+        }
+
+        await _updateUIContextJoinableTasks.JoinTillEmptyAsync(cancellationToken).ConfigureAwait(false);
path: src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs
user: login: ToddGrun
body: Done!
created_at: 2025-04-03T22:35:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: ToddGrun
created_at: 2025-04-03T16:38:32Z
body: Changed to no longer cache the UIContext and just grab it while on the UI thread and it's needed.

---
In reply to: [2737437422](https://github.com/dotnet/roslyn/pull/77920#issuecomment-2737437422) [](http://example.com/codeflow?ancestors=2737437422)
author_association: CONTRIBUTOR
user: login: CyrusNajmabadi
created_at: 2025-04-04T01:21:34Z
body: i thought we weren't using dev17.15 anymore?
author_association: CONTRIBUTOR
user: login: ToddGrun
created_at: 2025-04-04T01:28:08Z
body: > i thought we weren't using dev17.15 anymore?

whoops! Here's the PR for it to main: https://github.com/dotnet/roslyn/pull/77998
author_association: CONTRIBUTOR