Repository: dotnet/roslyn
Pull request #77823: Make GlobalOptionService initialization synchronous.
Author: login: ToddGrun
Labels: name: Area-IDE
name: untriaged
description: Issues and PRs which have not yet been triaged by a lead
name: VSCode
Created: 2025-03-25T23:30:04Z | Merged: 2025-03-27T19:12:02Z

As Tomas pointed out in an [earlier PR](https://github.com/dotnet/roslyn/pull/77808), all the hoops that are jumped through in GlobalOptionService to make it's initialization asynchronous aren't really necessary if the services obtained in VisualStudioOptionPersisterProvider were obtained synchronously. Doing just that allows GlobalOptionService to be quite a bit simpler and to no longer require a potential JTF.Run call when getting the first option.

The concern one could have with obtaining these services synchronously would be whether they were expensive to get and whether the first call would come through on the main thread. The first call to get an option comes through on a background thread (I've seen it come from either the background processing in after package load, or from the workspace initialization call roslyn gets from project system). Additionally, the measurements that I've taken of obtaining those services haven't shown those as expensive to obtain, so this does seem like a better approach than earlier attempts.


Diff:
diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs b/src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs
index 496ddb9e0f5cf..bf2eadad11631 100644
--- a/src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs
+++ b/src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs
@@ -6,10 +6,7 @@
 using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.ComponentModel.Composition;
-using System.Threading;
-using System.Threading.Tasks;
 using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
 using Microsoft.CodeAnalysis.ErrorReporting;
 using Microsoft.CodeAnalysis.Host.Mef;
 using Microsoft.CodeAnalysis.Options;
@@ -24,46 +21,41 @@ namespace Microsoft.VisualStudio.LanguageServices.Options;
 [Export(typeof(VisualStudioOptionPersisterProvider))]
 internal sealed class VisualStudioOptionPersisterProvider : IOptionPersisterProvider
 {
-    private readonly IAsyncServiceProvider _serviceProvider;
-    private readonly ILegacyGlobalOptionService _legacyGlobalOptions;
+    private readonly IServiceProvider _serviceProvider;
+    private readonly Lazy<ILegacyGlobalOptionService> _legacyGlobalOptions;
 
     // maps config name to a read fallback:
     private readonly ImmutableDictionary<string, Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>> _readFallbacks;
 
-    // Use vs-threading's JTF-aware AsyncLazy<T>. Ensure only one persister instance is created (even in the face of
-    // parallel requests for the value) because the constructor registers global event handler callbacks.
-    private readonly Threading.AsyncLazy<IOptionPersister> _lazyPersister;
+    // Ensure only one persister instance is created (even in the face of parallel requests for the value)
+    // because the constructor registers global event handler callbacks.
+    private readonly Lazy<IOptionPersister> _lazyPersister;
 
     [ImportingConstructor]
     [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
     public VisualStudioOptionPersisterProvider(
-        [Import(typeof(SAsyncServiceProvider))] IAsyncServiceProvider serviceProvider,
+        [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
         [ImportMany] IEnumerable<Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>> readFallbacks,
-        IThreadingContext threadingContext,
-        ILegacyGlobalOptionService legacyGlobalOptions)
+        Lazy<ILegacyGlobalOptionService> legacyGlobalOptions)
     {
         _serviceProvider = serviceProvider;
         _legacyGlobalOptions = legacyGlobalOptions;
         _readFallbacks = readFallbacks.ToImmutableDictionary(item => item.Metadata.ConfigName, item => item);
-        _lazyPersister = new Threading.AsyncLazy<IOptionPersister>(() => CreatePersisterAsync(threadingContext.DisposalToken), threadingContext.JoinableTaskFactory);
+        _lazyPersister = new Lazy<IOptionPersister>(() => CreatePersister());
     }
 
-    public ValueTask<IOptionPersister> GetOrCreatePersisterAsync(CancellationToken cancellationToken)
-        => new(_lazyPersister.GetValueAsync(cancellationToken));
+    public IOptionPersister GetOrCreatePersister()
+        => _lazyPersister.Value;
 
-    private async Task<IOptionPersister> CreatePersisterAsync(CancellationToken cancellationToken)
+    private IOptionPersister CreatePersister()
     {
-        // Obtain services before creating instances. This avoids state corruption in the event cancellation is
-        // requested (some of the constructors register event handlers that could leak if cancellation occurred
-        // in the middle of construction).
-        var settingsManager = await GetFreeThreadedServiceAsync<SVsSettingsPersistenceManager, ISettingsManager>().ConfigureAwait(false);
+        var settingsManager = GetFreeThreadedService<SVsSettingsPersistenceManager, ISettingsManager>();
         Assumes.Present(settingsManager);
-        var localRegistry = await GetFreeThreadedServiceAsync<SLocalRegistry, ILocalRegistry4>().ConfigureAwait(false);
+
+        var localRegistry = GetFreeThreadedService<SLocalRegistry, ILocalRegistry4>();
         Assumes.Present(localRegistry);
-        var featureFlags = await GetFreeThreadedServiceAsync<SVsFeatureFlags, IVsFeatureFlags>().ConfigureAwait(false);
 
-        // Cancellation is not allowed after this point
-        cancellationToken = CancellationToken.None;
+        var featureFlags = GetFreeThreadedService<SVsFeatureFlags, IVsFeatureFlags>();
 
         return new VisualStudioOptionPersister(
             new VisualStudioSettingsOptionPersister(RefreshOption, _readFallbacks, settingsManager),
@@ -73,11 +65,11 @@ private async Task<IOptionPersister> CreatePersisterAsync(CancellationToken canc
 
     private void RefreshOption(OptionKey2 optionKey, object? newValue)
     {
-        if (_legacyGlobalOptions.GlobalOptions.RefreshOption(optionKey, newValue))
+        if (_legacyGlobalOptions.Value.GlobalOptions.RefreshOption(optionKey, newValue))
         {
             // We may be updating the values of internally defined public options.
             // Update solution snapshots of all workspaces to reflect the new values.
-            _legacyGlobalOptions.UpdateRegisteredWorkspaces();
+            _legacyGlobalOptions.Value.UpdateRegisteredWorkspaces();
         }
     }
 
@@ -85,11 +77,11 @@ private void RefreshOption(OptionKey2 optionKey, object? newValue)
     /// Returns a service without doing a transition to the UI thread to cast the service to the interface type. This should only be called for services that are
     /// well-understood to be castable off the UI thread, either because they are managed or free-threaded COM.
     /// </summary>
-    private async ValueTask<I?> GetFreeThreadedServiceAsync<T, I>() where I : class
+    private I? GetFreeThreadedService<T, I>() where I : class
     {
         try
         {
-            return (I?)await _serviceProvider.GetServiceAsync(typeof(T)).ConfigureAwait(false);
+            return (I?)_serviceProvider.GetService(typeof(T));
         }
         catch (Exception e) when (FatalError.ReportAndPropagate(e))
         {
diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs
index 464e92740c231..bb3f93c94449a 100644
--- a/src/VisualStudio/Core/Def/RoslynPackage.cs
+++ b/src/VisualStudio/Core/Def/RoslynPackage.cs
@@ -3,7 +3,6 @@
 // See the LICENSE file in the project root for more information.
 
 using System;
-using System.Collections.Immutable;
 using System.ComponentModel.Design;
 using System.Runtime.InteropServices;
 using System.Threading;
@@ -18,12 +17,9 @@
 using Microsoft.CodeAnalysis.Notification;
 using Microsoft.CodeAnalysis.Options;
 using Microsoft.CodeAnalysis.Remote.ProjectSystem;
-using Microsoft.CodeAnalysis.Shared.TestHooks;
-using Microsoft.VisualStudio.ComponentModelHost;
 using Microsoft.VisualStudio.LanguageServices.EditorConfigSettings;
 using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
 using Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
-using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
 using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.RuleSets;
 using Microsoft.VisualStudio.LanguageServices.Implementation.Suppression;
 using Microsoft.VisualStudio.LanguageServices.Implementation.SyncNamespaces;
@@ -106,6 +102,9 @@ protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks a
 
         Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoadedTasks, CancellationToken cancellationToken)
         {
+            // Ensure the options persisters are loaded since we have to fetch options from the shell
+            _ = ComponentModel.GetService<IGlobalOptionService>();
+
             var colorSchemeApplier = ComponentModel.GetService<ColorSchemeApplier>();
             colorSchemeApplier.RegisterInitializationWork(afterPackageLoadedTasks);
 
@@ -115,9 +114,6 @@ Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoad
             _solutionEventMonitor = new SolutionEventMonitor(globalNotificationService);
             TrackBulkFileOperations(globalNotificationService);
 
-            // Ensure the options persisters are loaded since we have to fetch options from the shell
-            LoadOptionPersistersAsync(this.ComponentModel, cancellationToken).Forget();
-
             return Task.CompletedTask;
         }
 
@@ -145,21 +141,6 @@ private async Task ProfferServiceBrokerServicesAsync(CancellationToken cancellat
             (_, _, _, _) => ValueTaskFactory.FromResult<object?>(new ManagedEditAndContinueLanguageServiceBridge(this.ComponentModel.GetService<EditAndContinueLanguageService>())));
     }
 
-    private async Task LoadOptionPersistersAsync(IComponentModel componentModel, CancellationToken cancellationToken)
-    {
-        // Ensure on a background thread to ensure assembly loads don't show up as UI delays attributed to
-        // InitializeAsync.
-        Contract.ThrowIfTrue(JoinableTaskFactory.Context.IsOnMainThread);
-
-        var listenerProvider = componentModel.GetService<IAsynchronousOperationListenerProvider>();
-        using var token = listenerProvider.GetListener(FeatureAttribute.Workspace).BeginAsyncOperation(nameof(LoadOptionPersistersAsync));
-
-        var persisterProviders = componentModel.GetExtensions<IOptionPersisterProvider>().ToImmutableArray();
-
-        foreach (var provider in persisterProviders)
-            await provider.GetOrCreatePersisterAsync(cancellationToken).ConfigureAwait(true);
-    }
-
     protected override async Task LoadComponentsAsync(CancellationToken cancellationToken)
     {
         await TaskScheduler.Default;
diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/Options/GlobalOptionsTest.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/Options/GlobalOptionsTest.cs
index 567f59d37b7e6..9329cdfa856d0 100644
--- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/Options/GlobalOptionsTest.cs
+++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/Options/GlobalOptionsTest.cs
@@ -29,7 +29,7 @@ public async Task ValidateAllOptions()
     {
         var globalOptions = (GlobalOptionService)await TestServices.Shell.GetComponentModelServiceAsync<IGlobalOptionService>(HangMitigatingCancellationToken);
         var provider = await TestServices.Shell.GetComponentModelServiceAsync<VisualStudioOptionPersisterProvider>(HangMitigatingCancellationToken);
-        var vsSettingsPersister = (VisualStudioOptionPersister)await provider.GetOrCreatePersisterAsync(HangMitigatingCancellationToken);
+        var vsSettingsPersister = (VisualStudioOptionPersister)provider.GetOrCreatePersister();
 
         var optionsInfo = OptionsTestInfo.CollectOptions(Path.GetDirectoryName(typeof(GlobalOptionsTest).Assembly.Location!));
         var allLanguages = new[] { LanguageNames.CSharp, LanguageNames.VisualBasic };
diff --git a/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs b/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs
index 465af3bc1ea81..9bd8329165872 100644
--- a/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs
+++ b/src/Workspaces/Core/Portable/Options/GlobalOptionService.cs
@@ -7,12 +7,9 @@
 using System.Collections.Immutable;
 using System.Composition;
 using System.Diagnostics;
-using System.Threading;
-using System.Threading.Tasks;
 using Microsoft.CodeAnalysis.Host.Mef;
 using Microsoft.CodeAnalysis.PooledObjects;
 using Microsoft.CodeAnalysis.Shared.Collections;
-using Microsoft.CodeAnalysis.Shared.Utilities;
 using Roslyn.Utilities;
 
 namespace Microsoft.CodeAnalysis.Options;
@@ -21,62 +18,23 @@ namespace Microsoft.CodeAnalysis.Options;
 [method: ImportingConstructor]
 [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
 internal sealed class GlobalOptionService(
-    [Import(AllowDefault = true)] IWorkspaceThreadingService? workspaceThreadingService,
-    [ImportMany] IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisters) : IGlobalOptionService
+    [ImportMany] IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisterProviders) : IGlobalOptionService
 {
-    private readonly ImmutableArray<Lazy<IOptionPersisterProvider>> _optionPersisterProviders = [.. optionPersisters];
-
     private readonly object _gate = new();
 
     #region Guarded by _gate
 
-    private ImmutableArray<IOptionPersister> _lazyOptionPersisters;
+    private readonly Lazy<ImmutableArray<IOptionPersister>> _optionPersisters = new(() => GetOptionPersisters(optionPersisterProviders));
     private ImmutableDictionary<OptionKey2, object?> _currentValues = ImmutableDictionary.Create<OptionKey2, object?>();
 
     #endregion
 
     private readonly WeakEvent<OptionChangedEventArgs> _optionChanged = new();
 
-    private ImmutableArray<IOptionPersister> GetOptionPersisters()
+    private static ImmutableArray<IOptionPersister> GetOptionPersisters(IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisterProviders)
     {
-        if (_lazyOptionPersisters.IsDefault)
-        {
-            // Option persisters cannot be initialized while holding the global options lock
-            // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1353715
-            Debug.Assert(!Monitor.IsEntered(_gate));
-
-            ImmutableInterlocked.InterlockedInitialize(
-                ref _lazyOptionPersisters,
-                GetOptionPersistersSlow(workspaceThreadingService, _optionPersisterProviders, CancellationToken.None));
-        }
-
-        return _lazyOptionPersisters;
-
-        // Local functions
-        static ImmutableArray<IOptionPersister> GetOptionPersistersSlow(
-            IWorkspaceThreadingService? workspaceThreadingService,
-            ImmutableArray<Lazy<IOptionPersisterProvider>> persisterProviders,
-            CancellationToken cancellationToken)
-        {
-            if (workspaceThreadingService is not null && workspaceThreadingService.IsOnMainThread)
-            {
-                // speedometer tests report jtf.run calls from background threads, so we try to avoid those.
-                return workspaceThreadingService.Run(() => GetOptionPersistersAsync(persisterProviders, cancellationToken));
-            }
-            else
-            {
-                return GetOptionPersistersAsync(persisterProviders, cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken);
-            }
-        }
-
-        static async Task<ImmutableArray<IOptionPersister>> GetOptionPersistersAsync(
-            ImmutableArray<Lazy<IOptionPersisterProvider>> persisterProviders,
-            CancellationToken cancellationToken)
-        {
-            return await persisterProviders.SelectAsArrayAsync(
-                static (lazyProvider, cancellationToken) => lazyProvider.Value.GetOrCreatePersisterAsync(cancellationToken),
-                cancellationToken).ConfigureAwait(false);
-        }
+        return optionPersisterProviders.SelectAsArray(
+            static provider => provider.Value.GetOrCreatePersister());
     }
 
     private static object? LoadOptionFromPersisterOrGetDefault(OptionKey2 optionKey, ImmutableArray<IOptionPersister> persisters)
@@ -108,9 +66,6 @@ public T GetOption<T>(PerLanguageOption2<T> option, string language)
 
     public T GetOption<T>(OptionKey2 optionKey)
     {
-        // Ensure the option persisters are available before taking the global lock
-        var persisters = GetOptionPersisters();
-
         // Performance: This is called very frequently, with the vast majority (> 99%) of calls requesting a previously
         //  added key. In those cases, we can avoid taking the lock as _currentValues is an immutable structure.
         if (_currentValues.TryGetValue(optionKey, out var value))
@@ -118,6 +73,8 @@ public T GetOption<T>(OptionKey2 optionKey)
             return (T)value!;
         }
 
+        // Ensure the option persisters are available before taking the global lock
+        var persisters = _optionPersisters.Value;
         lock (_gate)
         {
             return (T)GetOption_NoLock(ref _currentValues, optionKey, persisters)!;
@@ -126,8 +83,6 @@ public T GetOption<T>(OptionKey2 optionKey)
 
     public ImmutableArray<object?> GetOptions(ImmutableArray<OptionKey2> optionKeys)
     {
-        // Ensure the option persisters are available before taking the global lock
-        var persisters = GetOptionPersisters();
         using var values = TemporaryArray<object?>.Empty;
 
         // Performance: The vast majority of calls are for previously added keys. In those cases, we can avoid taking the lock
@@ -146,6 +101,8 @@ public T GetOption<T>(OptionKey2 optionKey)
 
         if (values.Count != optionKeys.Length)
         {
+            // Ensure the option persisters are available before taking the global lock
+            var persisters = _optionPersisters.Value;
             lock (_gate)
             {
                 foreach (var optionKey in optionKeys)
@@ -190,7 +147,7 @@ public bool SetGlobalOptions(ImmutableArray<KeyValuePair<OptionKey2, object?>> o
     private bool SetGlobalOptions(OneOrMany<KeyValuePair<OptionKey2, object?>> options)
     {
         using var _ = ArrayBuilder<(OptionKey2, object?)>.GetInstance(options.Count, out var changedOptions);
-        var persisters = GetOptionPersisters();
+        var persisters = _optionPersisters.Value;
 
         lock (_gate)
         {
diff --git a/src/Workspaces/Core/Portable/Options/IOptionPersisterProvider.cs b/src/Workspaces/Core/Portable/Options/IOptionPersisterProvider.cs
index 74859173f5399..15918b36e5145 100644
--- a/src/Workspaces/Core/Portable/Options/IOptionPersisterProvider.cs
+++ b/src/Workspaces/Core/Portable/Options/IOptionPersisterProvider.cs
@@ -2,9 +2,6 @@
 // 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.Threading;
-using System.Threading.Tasks;
-
 namespace Microsoft.CodeAnalysis.Options;
 
 internal interface IOptionPersisterProvider
@@ -16,7 +13,6 @@ internal interface IOptionPersisterProvider
     /// This method is safe for concurrent use from any thread. No guarantees are made regarding the use of the UI
     /// thread.
     /// </remarks>
-    /// <param name="cancellationToken">A cancellation token the operation may observe.</param>
     /// <returns>The option persister.</returns>
-    ValueTask<IOptionPersister> GetOrCreatePersisterAsync(CancellationToken cancellationToken);
+    IOptionPersister GetOrCreatePersister();
 }


Reviews:
user: login: ToddGrun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T23:43:16Z
user: login: CyrusNajmabadi
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T17:55:41Z
user: login: CyrusNajmabadi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T17:56:05Z
user: login: jasonmalinowski
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T19:06:51Z

Review comments:
diff_hunk: @@ -106,6 +102,9 @@ protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks a
 
         Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoadedTasks, CancellationToken cancellationToken)
         {
+            // Ensure the options persisters are loaded since we have to fetch options from the shell
+            _ = ComponentModel.GetService<IGlobalOptionService>();
path: src/VisualStudio/Core/Def/RoslynPackage.cs
user: login: ToddGrun
body: >_ = ComponentModel.GetService<IGlobalOptionService>(); [](http://example.com/codeflow?start=12&length=54)

I debated just not having this, but I wanted to keep it similar  to before in that it explicitly ensures the service is at least created by this point.
created_at: 2025-03-25T23:43:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,62 +18,23 @@ namespace Microsoft.CodeAnalysis.Options;
 [method: ImportingConstructor]
 [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
 internal sealed class GlobalOptionService(
-    [Import(AllowDefault = true)] IWorkspaceThreadingService? workspaceThreadingService,
-    [ImportMany] IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisters) : IGlobalOptionService
+    [ImportMany] IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisterProviders) : IGlobalOptionService
 {
-    private readonly ImmutableArray<Lazy<IOptionPersisterProvider>> _optionPersisterProviders = [.. optionPersisters];
-
     private readonly object _gate = new();
 
     #region Guarded by _gate
 
-    private ImmutableArray<IOptionPersister> _lazyOptionPersisters;
+    private readonly Lazy<ImmutableArray<IOptionPersister>> _optionPersisters = new(() => GetOptionPersisters(optionPersisterProviders));
     private ImmutableDictionary<OptionKey2, object?> _currentValues = ImmutableDictionary.Create<OptionKey2, object?>();
 
     #endregion
 
     private readonly WeakEvent<OptionChangedEventArgs> _optionChanged = new();
 
-    private ImmutableArray<IOptionPersister> GetOptionPersisters()
+    private static ImmutableArray<IOptionPersister> GetOptionPersisters(IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisterProviders)
     {
-        if (_lazyOptionPersisters.IsDefault)
-        {
-            // Option persisters cannot be initialized while holding the global options lock
-            // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1353715
-            Debug.Assert(!Monitor.IsEntered(_gate));
-
-            ImmutableInterlocked.InterlockedInitialize(
-                ref _lazyOptionPersisters,
-                GetOptionPersistersSlow(workspaceThreadingService, _optionPersisterProviders, CancellationToken.None));
-        }
-
-        return _lazyOptionPersisters;
-
-        // Local functions
-        static ImmutableArray<IOptionPersister> GetOptionPersistersSlow(
-            IWorkspaceThreadingService? workspaceThreadingService,
-            ImmutableArray<Lazy<IOptionPersisterProvider>> persisterProviders,
-            CancellationToken cancellationToken)
-        {
-            if (workspaceThreadingService is not null && workspaceThreadingService.IsOnMainThread)
-            {
-                // speedometer tests report jtf.run calls from background threads, so we try to avoid those.
-                return workspaceThreadingService.Run(() => GetOptionPersistersAsync(persisterProviders, cancellationToken));
-            }
-            else
-            {
-                return GetOptionPersistersAsync(persisterProviders, cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken);
-            }
-        }
-
-        static async Task<ImmutableArray<IOptionPersister>> GetOptionPersistersAsync(
-            ImmutableArray<Lazy<IOptionPersisterProvider>> persisterProviders,
-            CancellationToken cancellationToken)
-        {
-            return await persisterProviders.SelectAsArrayAsync(
-                static (lazyProvider, cancellationToken) => lazyProvider.Value.GetOrCreatePersisterAsync(cancellationToken),
-                cancellationToken).ConfigureAwait(false);
-        }
+        return optionPersisterProviders.SelectAsArray(
+                static provider => provider.Value.GetOrCreatePersister());
path: src/Workspaces/Core/Portable/Options/GlobalOptionService.cs
user: login: CyrusNajmabadi
body: indentation.
created_at: 2025-03-26T17:56:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +21,41 @@ namespace Microsoft.VisualStudio.LanguageServices.Options;
 [Export(typeof(VisualStudioOptionPersisterProvider))]
 internal sealed class VisualStudioOptionPersisterProvider : IOptionPersisterProvider
 {
-    private readonly IAsyncServiceProvider _serviceProvider;
-    private readonly ILegacyGlobalOptionService _legacyGlobalOptions;
+    private readonly IServiceProvider _serviceProvider;
path: src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs
user: login: jasonmalinowski
body: You might want to add a comment we're using IServiceProvider versus the async forms since we know this is a constrained case.
created_at: 2025-03-27T18:59:25Z
author_association: MEMBER
diff_hunk: @@ -73,23 +65,23 @@ private async Task<IOptionPersister> CreatePersisterAsync(CancellationToken canc
 
     private void RefreshOption(OptionKey2 optionKey, object? newValue)
     {
-        if (_legacyGlobalOptions.GlobalOptions.RefreshOption(optionKey, newValue))
+        if (_legacyGlobalOptions.Value.GlobalOptions.RefreshOption(optionKey, newValue))
         {
             // We may be updating the values of internally defined public options.
             // Update solution snapshots of all workspaces to reflect the new values.
-            _legacyGlobalOptions.UpdateRegisteredWorkspaces();
+            _legacyGlobalOptions.Value.UpdateRegisteredWorkspaces();
         }
     }
 
     /// <summary>
     /// Returns a service without doing a transition to the UI thread to cast the service to the interface type. This should only be called for services that are
     /// well-understood to be castable off the UI thread, either because they are managed or free-threaded COM.
     /// </summary>
-    private async ValueTask<I?> GetFreeThreadedServiceAsync<T, I>() where I : class
+    private I? GetFreeThreadedService<T, I>() where I : class
path: src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs
user: login: jasonmalinowski
body: This can be done as a follow-up, but do we need this extension anymore? If all the underlying helpers from the shell are now smarter, maybe we can delete it.
created_at: 2025-03-27T19:04:16Z
author_association: MEMBER
diff_hunk: @@ -106,6 +102,9 @@ protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks a
 
         Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoadedTasks, CancellationToken cancellationToken)
         {
+            // Ensure the options persisters are loaded since we have to fetch options from the shell
+            _ = ComponentModel.GetService<IGlobalOptionService>();
path: src/VisualStudio/Core/Def/RoslynPackage.cs
user: login: jasonmalinowski
body: At this point we're not eagerly creating any persisters, right? It's just creating the service, but the persisters themselves are still lazy? In that case, I'd toss this. It's not doing anything (but looks like it might be.)
created_at: 2025-03-27T19:05:13Z
author_association: MEMBER

PR comments:
user: login: ToddGrun
created_at: 2025-03-25T23:32:29Z
body: Hopefully third times a charm
author_association: CONTRIBUTOR
user: login: tmat
created_at: 2025-03-26T00:06:17Z
body: @jasonmalinowski @sharwell ptal
author_association: MEMBER
user: login: ToddGrun
created_at: 2025-03-26T21:50:12Z
body: @jasonmalinowski @sharwell -- this approach good for you?
author_association: CONTRIBUTOR
user: login: jasonmalinowski
created_at: 2025-03-27T18:58:37Z
body: I strongly support the approach here. If we had a time machine, then we would just make GetOption async and that'd be fine. That would have been handy in the past too when we were moving stuff OOP and wished that we could go to VS to fetch options lazily (and not block in the ServiceHub process while doing so.)

But lacking a time machine I think this is fine. The primary concern we've had here is deadlock risk -- this code has been troublesome over the years because UI thread transitions would sneak in during the service fetch, and then we'd deadlock since some random part of Roslyn that though it was free-threaded during initialization wasn't. And since the code predated JTF introduction into Roslyn, we weren't following JTF patterns. (And since GetOption wasn't async, it wasn't a great choice anyways....) But if the services are now free-threaded for good, which they just should be, then the deadlock concern goes away.

I'm not concerned at all either about the fact we're using the synchronous GetService rather than GetServiceAsync, because:

1. These services are cached, so as long as they're already loaded, there is going to be no blocking or asynchrony _anyways_.
2. These services are nearly guaranteed to be loaded before any part of Roslyn initialization happens. It's completely improbable that Roslyn will be the first thing to read a setting, since more critical things happened first, like loading settings that control the shell loading. (Or simply which theme the user has enabled!) So again, we're not going to be blocking up a background thread on the creation of a service which itself might be an asynchronous creation. If on the off chance that Roslyn somehow _is_ the first person reading a setting, then as long as we don't deadlock, that's fine. (And if Visual Studio has gotten so asynchronous and lazy loading stuff that this potential block becomes a problem...I'll be overjoyed that we've gotten that far!)
author_association: MEMBER