From 0b44805e9c0163d5ec0edb96dd0f51fe09623036 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Wed, 8 Oct 2025 15:26:11 -0700 Subject: [PATCH 1/4] Remove an eager loading of IGlobalOptionService The implementation now imports all the persisters lazily, so this won't even have any benefit. --- src/VisualStudio/Core/Def/RoslynPackage.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 0e6ab971ef613..3c3d40aece3f9 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -12,7 +12,6 @@ using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Remote.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.EditorConfigSettings; using Microsoft.VisualStudio.LanguageServices.ExternalAccess.UnitTesting; @@ -91,19 +90,10 @@ protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks a { base.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks); - afterPackageLoadedTasks.AddTask(isMainThreadTask: false, task: OnAfterPackageLoadedBackgroundThreadAsync); afterPackageLoadedTasks.AddTask(isMainThreadTask: true, task: OnAfterPackageLoadedMainThreadAsync); return; - Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoadedTasks, CancellationToken cancellationToken) - { - // Ensure the options persisters are loaded since we have to fetch options from the shell - _ = ComponentModel.GetService(); - - return Task.CompletedTask; - } - Task OnAfterPackageLoadedMainThreadAsync(PackageLoadTasks afterPackageLoadedTasks, CancellationToken cancellationToken) { // load some services that have to be loaded in UI thread From ee06fd9cb877726cd606d6bcdc56f40d5b60b87f Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Wed, 8 Oct 2025 15:39:34 -0700 Subject: [PATCH 2/4] Register editor factories asynchronously This can be done off the UI thread now. --- .../EditorConfigSettings/SettingsEditorFactory.cs | 10 ---------- .../Core/Def/LanguageService/AbstractPackage.cs | 14 ++++++++++++++ .../Core/Def/LanguageService/AbstractPackage`2.cs | 12 +++++------- src/VisualStudio/Core/Def/RoslynPackage.cs | 14 +++----------- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorFactory.cs b/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorFactory.cs index 9bddad6c4169f..81287b2f62a29 100644 --- a/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorFactory.cs +++ b/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorFactory.cs @@ -23,21 +23,11 @@ namespace Microsoft.VisualStudio.LanguageServices.EditorConfigSettings; [Guid(SettingsEditorFactoryGuidString)] internal sealed class SettingsEditorFactory() : IVsEditorFactory, IVsEditorFactory4 { - private static SettingsEditorFactory? s_instance; - public static readonly Guid SettingsEditorFactoryGuid = new(SettingsEditorFactoryGuidString); public const string SettingsEditorFactoryGuidString = "68b46364-d378-42f2-9e72-37d86c5f4468"; public const string Extension = ".editorconfig"; private ServiceProvider? _vsServiceProvider; - - public static SettingsEditorFactory GetInstance() - { - s_instance ??= new SettingsEditorFactory(); - - return s_instance; - } - public int CreateEditorInstance(uint grfCreateDoc, string filePath, string pszPhysicalView, diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs index 1b39a2ae14a0c..6befd17d6f1a2 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService; @@ -71,6 +72,19 @@ protected virtual void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks af { } + /// + /// Registers an editor factory. This is the same as except it fetches the service async. + /// + protected async Task RegisterEditorFactoryAsync(IVsEditorFactory editorFactory, CancellationToken cancellationToken) + { + // Call with ConfigureAwait(true): if we're off the UI thread we will stay that way, but a synchronous load of our package should continue to use the UI thread + // since the UI thread is otherwise blocked waiting for us. This method is called under JTF rules so that's fine. + var registerEditors = await GetServiceAsync(throwOnFailure: true, cancellationToken).ConfigureAwait(true); + Assumes.Present(registerEditors); + + ErrorHandler.ThrowOnFailure(registerEditors.RegisterEditor(editorFactory.GetType().GUID, editorFactory, out _)); + } + protected async Task LoadComponentsInUIContextOnceSolutionFullyLoadedAsync(CancellationToken cancellationToken) { // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in other scenarios. diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs index cca33bb4458d1..46362388f0096 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs @@ -57,16 +57,11 @@ private async Task PackageInitializationMainThreadAsync(PackageLoadTasks package _shell = (IVsShell?)shell; Assumes.Present(_shell); - foreach (var editorFactory in CreateEditorFactories()) - { - RegisterEditorFactory(editorFactory); - } - // awaiting an IVsTask guarantees to return on the captured context await shell.LoadPackageAsync(Guids.RoslynPackageId); } - private Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks packageInitializationTasks, CancellationToken cancellationToken) + private async Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks packageInitializationTasks, CancellationToken cancellationToken) { AddService(typeof(TLanguageService), async (_, cancellationToken, _) => { @@ -103,7 +98,10 @@ private Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks package RegisterMiscellaneousFilesWorkspaceInformation(miscellaneousFilesWorkspace); - return Task.CompletedTask; + foreach (var editorFactory in CreateEditorFactories()) + { + await RegisterEditorFactoryAsync(editorFactory, cancellationToken).ConfigureAwait(true); + } } protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks afterPackageLoadedTasks) diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 3c3d40aece3f9..8c26bfde24b1b 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -68,21 +68,13 @@ protected override void RegisterInitializeAsyncWork(PackageLoadTasks packageInit base.RegisterInitializeAsyncWork(packageInitializationTasks); packageInitializationTasks.AddTask(isMainThreadTask: false, task: PackageInitializationBackgroundThreadAsync); - packageInitializationTasks.AddTask(isMainThreadTask: true, task: PackageInitializationMainThreadAsync); return; - Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks packageInitializationTasks, CancellationToken cancellationToken) + async Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks packageInitializationTasks, CancellationToken cancellationToken) { - return ProfferServiceBrokerServicesAsync(cancellationToken); - } - - Task PackageInitializationMainThreadAsync(PackageLoadTasks packageInitializationTasks, CancellationToken cancellationToken) - { - var settingsEditorFactory = SettingsEditorFactory.GetInstance(); - RegisterEditorFactory(settingsEditorFactory); - - return Task.CompletedTask; + await RegisterEditorFactoryAsync(new SettingsEditorFactory(), cancellationToken).ConfigureAwait(true); + await ProfferServiceBrokerServicesAsync(cancellationToken).ConfigureAwait(true); } } From a0353c962227e42d1549645b73692a80cda007ae Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Wed, 8 Oct 2025 16:00:57 -0700 Subject: [PATCH 3/4] Switch to getting the command line state from IVsAppId This is free-threaded compared to asking IVsShell. --- .../Def/LanguageService/AbstractPackage`2.cs | 27 ++++---- .../Core/Def/Utilities/CommandLineMode.cs | 63 +++++++++++++++++++ .../Core/Def/Utilities/IVsShellExtensions.cs | 41 ------------ 3 files changed, 74 insertions(+), 57 deletions(-) create mode 100644 src/VisualStudio/Core/Def/Utilities/CommandLineMode.cs delete mode 100644 src/VisualStudio/Core/Def/Utilities/IVsShellExtensions.cs diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs index 46362388f0096..8816b3e3eab3e 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs @@ -26,13 +26,14 @@ internal abstract partial class AbstractPackage : Ab { private PackageInstallerService? _packageInstallerService; private VisualStudioSymbolSearchService? _symbolSearchService; - private IVsShell? _shell; /// /// Set to 1 if we've already preloaded project system components. Should be updated with /// private int _projectSystemComponentsPreloaded; + private bool _objectBrowserLibraryManagerRegistered = false; + protected AbstractPackage() { } @@ -47,17 +48,10 @@ protected override void RegisterInitializeAsyncWork(PackageLoadTasks packageInit private async Task PackageInitializationMainThreadAsync(PackageLoadTasks packageInitializationTasks, CancellationToken cancellationToken) { - // This code uses various main thread only services, so it must run completely on the main thread - // (thus the CA(true) usage throughout) - Contract.ThrowIfFalse(JoinableTaskFactory.Context.IsOnMainThread); - - var shell = (IVsShell7?)await GetServiceAsync(typeof(SVsShell)).ConfigureAwait(true); + // We still need to ensure the RoslynPackage is loaded, since it's OnAfterPackageLoaded will hook up event handlers in RoslynPackage.LoadComponentsAsync. + // Once that method has been replaced, then this package load can be removed. + var shell = await GetServiceAsync(throwOnFailure: true, cancellationToken).ConfigureAwait(true); Assumes.Present(shell); - - _shell = (IVsShell?)shell; - Assumes.Present(_shell); - - // awaiting an IVsTask guarantees to return on the captured context await shell.LoadPackageAsync(Guids.RoslynPackageId); } @@ -110,18 +104,18 @@ protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks a afterPackageLoadedTasks.AddTask( isMainThreadTask: true, - task: (packageLoadedTasks, cancellationToken) => + task: async (packageLoadedTasks, cancellationToken) => { - if (_shell != null && !_shell.IsInCommandLineMode()) + if (!await CommandLineMode.IsInCommandLineModeAsync(AsyncServiceProvider.GlobalProvider, cancellationToken).ConfigureAwait(true)) { // not every derived package support object browser and for those languages // this is a no op RegisterObjectBrowserLibraryManager(); + + _objectBrowserLibraryManagerRegistered = true; } LoadComponentsInUIContextOnceSolutionFullyLoadedAsync(cancellationToken).Forget(); - - return Task.CompletedTask; }); } @@ -158,7 +152,8 @@ protected override void Dispose(bool disposing) { // Per VS core team, Package.Dispose is called on the UI thread. Contract.ThrowIfFalse(JoinableTaskFactory.Context.IsOnMainThread); - if (_shell != null && !_shell.IsInCommandLineMode()) + + if (_objectBrowserLibraryManagerRegistered) { UnregisterObjectBrowserLibraryManager(); } diff --git a/src/VisualStudio/Core/Def/Utilities/CommandLineMode.cs b/src/VisualStudio/Core/Def/Utilities/CommandLineMode.cs new file mode 100644 index 0000000000000..d535f85c7367c --- /dev/null +++ b/src/VisualStudio/Core/Def/Utilities/CommandLineMode.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; + +internal static class CommandLineMode +{ + // tri-state: uninitialized (0), devenv is in command line mode (1), devenv is not in command line mode (-1) + private static volatile int s_isInCommandLineMode; + + /// + /// Returns true if devenv is invoked in command line mode for build, e.g. devenv /rebuild MySolution.sln + /// + public static async Task IsInCommandLineModeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken) + { + if (s_isInCommandLineMode == 0) + { + var appId = await serviceProvider.GetServiceAsync(cancellationToken).ConfigureAwait(true); + + s_isInCommandLineMode = + ErrorHandler.Succeeded(appId.GetProperty(VSAPROPID_IsInCommandLineMode, out var result)) && + (bool)result ? 1 : -1; + } + + return s_isInCommandLineMode == 1; + } + + // Copied from https://github.com/dotnet/project-system/blob/698c90fc016a24fd5b0b2b73df2c68299e04bd66/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Interop/IVsAppId.cs + [Guid("1EAA526A-0898-11d3-B868-00C04F79F802"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IVsAppId + { + [PreserveSig] + int SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider pSP); + + [PreserveSig] + int GetProperty(int propid, // VSAPROPID + [MarshalAs(UnmanagedType.Struct)] out object pvar); + + [PreserveSig] + int SetProperty(int propid, //[in] VSAPROPID + [MarshalAs(UnmanagedType.Struct)] object var); + + [PreserveSig] + int GetGuidProperty(int propid, // VSAPROPID + out Guid guid); + + [PreserveSig] + int SetGuidProperty(int propid, // [in] VSAPROPID + ref Guid rguid); + + [PreserveSig] + int Initialize(); // called after main initialization and before command executing and entering main loop + } + + private const int VSAPROPID_IsInCommandLineMode = -8660; +} diff --git a/src/VisualStudio/Core/Def/Utilities/IVsShellExtensions.cs b/src/VisualStudio/Core/Def/Utilities/IVsShellExtensions.cs deleted file mode 100644 index 167641cbcdb2a..0000000000000 --- a/src/VisualStudio/Core/Def/Utilities/IVsShellExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using Microsoft.VisualStudio.Shell.Interop; - -namespace Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; - -internal static class IVsShellExtensions -{ - // tri-state: uninitialized (0), devenv is in command line mode (1), devenv is not in command line mode (-1) - private static volatile int s_isInCommandLineMode; - - /// - /// Returns true if devenv is invoked in command line mode for build, e.g. devenv /rebuild MySolution.sln - /// - public static bool IsInCommandLineMode(this IVsShell shell) - { - if (s_isInCommandLineMode == 0) - { - s_isInCommandLineMode = - ErrorHandler.Succeeded(shell.GetProperty((int)__VSSPROPID.VSSPROPID_IsInCommandLineMode, out var result)) && - (bool)result ? 1 : -1; - } - - return s_isInCommandLineMode == 1; - } - - public static bool TryGetPropertyValue(this IVsShell shell, __VSSPROPID id, out IntPtr value) - { - if (ErrorHandler.Succeeded(shell.GetProperty((int)id, out var objValue)) && objValue != null) - { - value = (IntPtr.Size == 4) ? (IntPtr)(int)objValue : (IntPtr)(long)objValue; - return true; - } - - value = default; - return false; - } -} From 1b2c755c3625a72601fd2972767ae330f7addae5 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Wed, 8 Oct 2025 16:16:53 -0700 Subject: [PATCH 4/4] Remove the EnsureComponentModelAsync from the main thread queue This isn't needed anymore since nothing on the main thread is still consuming the ComponentModel. --- .../Core/Def/LanguageService/AbstractPackage.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs index 6befd17d6f1a2..2f1671eaa406f 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs @@ -52,19 +52,14 @@ private Task RegisterAndProcessTasksAsync(Action registerTasks protected virtual void RegisterInitializeAsyncWork(PackageLoadTasks packageInitializationTasks) { - // This treatment of registering work on the bg/main threads is a bit unique as we want the component model initialized at the beginning - // of whichever context is invoked first. The current architecture doesn't execute any of the registered tasks concurrently, - // so that isn't a concern for calculating or setting _componentModel_doNotAccessDirectly multiple times. + // We register this task so our ComponentModel property is available during other parts of package initialization and OnAfterPackageLoaded work. The + // expectation at this point is no work scheduled to the UI thread needs the ComponentModel, so we only schedule it for the background thread. packageInitializationTasks.AddTask(isMainThreadTask: false, task: EnsureComponentModelAsync); - packageInitializationTasks.AddTask(isMainThreadTask: true, task: EnsureComponentModelAsync); async Task EnsureComponentModelAsync(PackageLoadTasks packageInitializationTasks, CancellationToken token) { - if (_componentModel_doNotAccessDirectly == null) - { - _componentModel_doNotAccessDirectly = (IComponentModel?)await GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(false); - Assumes.Present(_componentModel_doNotAccessDirectly); - } + _componentModel_doNotAccessDirectly = (IComponentModel?)await GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(false); + Assumes.Present(_componentModel_doNotAccessDirectly); } }