Skip to content

Commit 9c09627

Browse files
authored
Make GlobalOptionService initialization synchronous. (#77823)
As Tomas pointed out in an earlier PR, 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.
1 parent 5d80619 commit 9c09627

File tree

5 files changed

+34
-108
lines changed

5 files changed

+34
-108
lines changed

src/VisualStudio/Core/Def/Options/VisualStudioOptionPersisterProvider.cs

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.ComponentModel.Composition;
9-
using System.Threading;
10-
using System.Threading.Tasks;
119
using Microsoft.CodeAnalysis;
12-
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
1310
using Microsoft.CodeAnalysis.ErrorReporting;
1411
using Microsoft.CodeAnalysis.Host.Mef;
1512
using Microsoft.CodeAnalysis.Options;
@@ -24,46 +21,41 @@ namespace Microsoft.VisualStudio.LanguageServices.Options;
2421
[Export(typeof(VisualStudioOptionPersisterProvider))]
2522
internal sealed class VisualStudioOptionPersisterProvider : IOptionPersisterProvider
2623
{
27-
private readonly IAsyncServiceProvider _serviceProvider;
28-
private readonly ILegacyGlobalOptionService _legacyGlobalOptions;
24+
private readonly IServiceProvider _serviceProvider;
25+
private readonly Lazy<ILegacyGlobalOptionService> _legacyGlobalOptions;
2926

3027
// maps config name to a read fallback:
3128
private readonly ImmutableDictionary<string, Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>> _readFallbacks;
3229

33-
// Use vs-threading's JTF-aware AsyncLazy<T>. Ensure only one persister instance is created (even in the face of
34-
// parallel requests for the value) because the constructor registers global event handler callbacks.
35-
private readonly Threading.AsyncLazy<IOptionPersister> _lazyPersister;
30+
// Ensure only one persister instance is created (even in the face of parallel requests for the value)
31+
// because the constructor registers global event handler callbacks.
32+
private readonly Lazy<IOptionPersister> _lazyPersister;
3633

3734
[ImportingConstructor]
3835
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
3936
public VisualStudioOptionPersisterProvider(
40-
[Import(typeof(SAsyncServiceProvider))] IAsyncServiceProvider serviceProvider,
37+
[Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
4138
[ImportMany] IEnumerable<Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>> readFallbacks,
42-
IThreadingContext threadingContext,
43-
ILegacyGlobalOptionService legacyGlobalOptions)
39+
Lazy<ILegacyGlobalOptionService> legacyGlobalOptions)
4440
{
4541
_serviceProvider = serviceProvider;
4642
_legacyGlobalOptions = legacyGlobalOptions;
4743
_readFallbacks = readFallbacks.ToImmutableDictionary(item => item.Metadata.ConfigName, item => item);
48-
_lazyPersister = new Threading.AsyncLazy<IOptionPersister>(() => CreatePersisterAsync(threadingContext.DisposalToken), threadingContext.JoinableTaskFactory);
44+
_lazyPersister = new Lazy<IOptionPersister>(() => CreatePersister());
4945
}
5046

51-
public ValueTask<IOptionPersister> GetOrCreatePersisterAsync(CancellationToken cancellationToken)
52-
=> new(_lazyPersister.GetValueAsync(cancellationToken));
47+
public IOptionPersister GetOrCreatePersister()
48+
=> _lazyPersister.Value;
5349

54-
private async Task<IOptionPersister> CreatePersisterAsync(CancellationToken cancellationToken)
50+
private IOptionPersister CreatePersister()
5551
{
56-
// Obtain services before creating instances. This avoids state corruption in the event cancellation is
57-
// requested (some of the constructors register event handlers that could leak if cancellation occurred
58-
// in the middle of construction).
59-
var settingsManager = await GetFreeThreadedServiceAsync<SVsSettingsPersistenceManager, ISettingsManager>().ConfigureAwait(false);
52+
var settingsManager = GetFreeThreadedService<SVsSettingsPersistenceManager, ISettingsManager>();
6053
Assumes.Present(settingsManager);
61-
var localRegistry = await GetFreeThreadedServiceAsync<SLocalRegistry, ILocalRegistry4>().ConfigureAwait(false);
54+
55+
var localRegistry = GetFreeThreadedService<SLocalRegistry, ILocalRegistry4>();
6256
Assumes.Present(localRegistry);
63-
var featureFlags = await GetFreeThreadedServiceAsync<SVsFeatureFlags, IVsFeatureFlags>().ConfigureAwait(false);
6457

65-
// Cancellation is not allowed after this point
66-
cancellationToken = CancellationToken.None;
58+
var featureFlags = GetFreeThreadedService<SVsFeatureFlags, IVsFeatureFlags>();
6759

6860
return new VisualStudioOptionPersister(
6961
new VisualStudioSettingsOptionPersister(RefreshOption, _readFallbacks, settingsManager),
@@ -73,23 +65,23 @@ private async Task<IOptionPersister> CreatePersisterAsync(CancellationToken canc
7365

7466
private void RefreshOption(OptionKey2 optionKey, object? newValue)
7567
{
76-
if (_legacyGlobalOptions.GlobalOptions.RefreshOption(optionKey, newValue))
68+
if (_legacyGlobalOptions.Value.GlobalOptions.RefreshOption(optionKey, newValue))
7769
{
7870
// We may be updating the values of internally defined public options.
7971
// Update solution snapshots of all workspaces to reflect the new values.
80-
_legacyGlobalOptions.UpdateRegisteredWorkspaces();
72+
_legacyGlobalOptions.Value.UpdateRegisteredWorkspaces();
8173
}
8274
}
8375

8476
/// <summary>
8577
/// 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
8678
/// well-understood to be castable off the UI thread, either because they are managed or free-threaded COM.
8779
/// </summary>
88-
private async ValueTask<I?> GetFreeThreadedServiceAsync<T, I>() where I : class
80+
private I? GetFreeThreadedService<T, I>() where I : class
8981
{
9082
try
9183
{
92-
return (I?)await _serviceProvider.GetServiceAsync(typeof(T)).ConfigureAwait(false);
84+
return (I?)_serviceProvider.GetService(typeof(T));
9385
}
9486
catch (Exception e) when (FatalError.ReportAndPropagate(e))
9587
{

src/VisualStudio/Core/Def/RoslynPackage.cs

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Collections.Immutable;
76
using System.ComponentModel.Design;
87
using System.Runtime.InteropServices;
98
using System.Threading;
@@ -18,12 +17,9 @@
1817
using Microsoft.CodeAnalysis.Notification;
1918
using Microsoft.CodeAnalysis.Options;
2019
using Microsoft.CodeAnalysis.Remote.ProjectSystem;
21-
using Microsoft.CodeAnalysis.Shared.TestHooks;
22-
using Microsoft.VisualStudio.ComponentModelHost;
2320
using Microsoft.VisualStudio.LanguageServices.EditorConfigSettings;
2421
using Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
2522
using Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
26-
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
2723
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.RuleSets;
2824
using Microsoft.VisualStudio.LanguageServices.Implementation.Suppression;
2925
using Microsoft.VisualStudio.LanguageServices.Implementation.SyncNamespaces;
@@ -106,6 +102,9 @@ protected override void RegisterOnAfterPackageLoadedAsyncWork(PackageLoadTasks a
106102

107103
Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoadedTasks, CancellationToken cancellationToken)
108104
{
105+
// Ensure the options persisters are loaded since we have to fetch options from the shell
106+
_ = ComponentModel.GetService<IGlobalOptionService>();
107+
109108
var colorSchemeApplier = ComponentModel.GetService<ColorSchemeApplier>();
110109
colorSchemeApplier.RegisterInitializationWork(afterPackageLoadedTasks);
111110

@@ -115,9 +114,6 @@ Task OnAfterPackageLoadedBackgroundThreadAsync(PackageLoadTasks afterPackageLoad
115114
_solutionEventMonitor = new SolutionEventMonitor(globalNotificationService);
116115
TrackBulkFileOperations(globalNotificationService);
117116

118-
// Ensure the options persisters are loaded since we have to fetch options from the shell
119-
LoadOptionPersistersAsync(this.ComponentModel, cancellationToken).Forget();
120-
121117
return Task.CompletedTask;
122118
}
123119

@@ -145,21 +141,6 @@ private async Task ProfferServiceBrokerServicesAsync(CancellationToken cancellat
145141
(_, _, _, _) => ValueTaskFactory.FromResult<object?>(new ManagedEditAndContinueLanguageServiceBridge(this.ComponentModel.GetService<EditAndContinueLanguageService>())));
146142
}
147143

148-
private async Task LoadOptionPersistersAsync(IComponentModel componentModel, CancellationToken cancellationToken)
149-
{
150-
// Ensure on a background thread to ensure assembly loads don't show up as UI delays attributed to
151-
// InitializeAsync.
152-
Contract.ThrowIfTrue(JoinableTaskFactory.Context.IsOnMainThread);
153-
154-
var listenerProvider = componentModel.GetService<IAsynchronousOperationListenerProvider>();
155-
using var token = listenerProvider.GetListener(FeatureAttribute.Workspace).BeginAsyncOperation(nameof(LoadOptionPersistersAsync));
156-
157-
var persisterProviders = componentModel.GetExtensions<IOptionPersisterProvider>().ToImmutableArray();
158-
159-
foreach (var provider in persisterProviders)
160-
await provider.GetOrCreatePersisterAsync(cancellationToken).ConfigureAwait(true);
161-
}
162-
163144
protected override async Task LoadComponentsAsync(CancellationToken cancellationToken)
164145
{
165146
await TaskScheduler.Default;

src/VisualStudio/IntegrationTest/New.IntegrationTests/Options/GlobalOptionsTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task ValidateAllOptions()
2929
{
3030
var globalOptions = (GlobalOptionService)await TestServices.Shell.GetComponentModelServiceAsync<IGlobalOptionService>(HangMitigatingCancellationToken);
3131
var provider = await TestServices.Shell.GetComponentModelServiceAsync<VisualStudioOptionPersisterProvider>(HangMitigatingCancellationToken);
32-
var vsSettingsPersister = (VisualStudioOptionPersister)await provider.GetOrCreatePersisterAsync(HangMitigatingCancellationToken);
32+
var vsSettingsPersister = (VisualStudioOptionPersister)provider.GetOrCreatePersister();
3333

3434
var optionsInfo = OptionsTestInfo.CollectOptions(Path.GetDirectoryName(typeof(GlobalOptionsTest).Assembly.Location!));
3535
var allLanguages = new[] { LanguageNames.CSharp, LanguageNames.VisualBasic };

src/Workspaces/Core/Portable/Options/GlobalOptionService.cs

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@
77
using System.Collections.Immutable;
88
using System.Composition;
99
using System.Diagnostics;
10-
using System.Threading;
11-
using System.Threading.Tasks;
1210
using Microsoft.CodeAnalysis.Host.Mef;
1311
using Microsoft.CodeAnalysis.PooledObjects;
1412
using Microsoft.CodeAnalysis.Shared.Collections;
15-
using Microsoft.CodeAnalysis.Shared.Utilities;
1613
using Roslyn.Utilities;
1714

1815
namespace Microsoft.CodeAnalysis.Options;
@@ -21,62 +18,23 @@ namespace Microsoft.CodeAnalysis.Options;
2118
[method: ImportingConstructor]
2219
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
2320
internal sealed class GlobalOptionService(
24-
[Import(AllowDefault = true)] IWorkspaceThreadingService? workspaceThreadingService,
25-
[ImportMany] IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisters) : IGlobalOptionService
21+
[ImportMany] IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisterProviders) : IGlobalOptionService
2622
{
27-
private readonly ImmutableArray<Lazy<IOptionPersisterProvider>> _optionPersisterProviders = [.. optionPersisters];
28-
2923
private readonly object _gate = new();
3024

3125
#region Guarded by _gate
3226

33-
private ImmutableArray<IOptionPersister> _lazyOptionPersisters;
27+
private readonly Lazy<ImmutableArray<IOptionPersister>> _optionPersisters = new(() => GetOptionPersisters(optionPersisterProviders));
3428
private ImmutableDictionary<OptionKey2, object?> _currentValues = ImmutableDictionary.Create<OptionKey2, object?>();
3529

3630
#endregion
3731

3832
private readonly WeakEvent<OptionChangedEventArgs> _optionChanged = new();
3933

40-
private ImmutableArray<IOptionPersister> GetOptionPersisters()
34+
private static ImmutableArray<IOptionPersister> GetOptionPersisters(IEnumerable<Lazy<IOptionPersisterProvider>> optionPersisterProviders)
4135
{
42-
if (_lazyOptionPersisters.IsDefault)
43-
{
44-
// Option persisters cannot be initialized while holding the global options lock
45-
// https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1353715
46-
Debug.Assert(!Monitor.IsEntered(_gate));
47-
48-
ImmutableInterlocked.InterlockedInitialize(
49-
ref _lazyOptionPersisters,
50-
GetOptionPersistersSlow(workspaceThreadingService, _optionPersisterProviders, CancellationToken.None));
51-
}
52-
53-
return _lazyOptionPersisters;
54-
55-
// Local functions
56-
static ImmutableArray<IOptionPersister> GetOptionPersistersSlow(
57-
IWorkspaceThreadingService? workspaceThreadingService,
58-
ImmutableArray<Lazy<IOptionPersisterProvider>> persisterProviders,
59-
CancellationToken cancellationToken)
60-
{
61-
if (workspaceThreadingService is not null && workspaceThreadingService.IsOnMainThread)
62-
{
63-
// speedometer tests report jtf.run calls from background threads, so we try to avoid those.
64-
return workspaceThreadingService.Run(() => GetOptionPersistersAsync(persisterProviders, cancellationToken));
65-
}
66-
else
67-
{
68-
return GetOptionPersistersAsync(persisterProviders, cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken);
69-
}
70-
}
71-
72-
static async Task<ImmutableArray<IOptionPersister>> GetOptionPersistersAsync(
73-
ImmutableArray<Lazy<IOptionPersisterProvider>> persisterProviders,
74-
CancellationToken cancellationToken)
75-
{
76-
return await persisterProviders.SelectAsArrayAsync(
77-
static (lazyProvider, cancellationToken) => lazyProvider.Value.GetOrCreatePersisterAsync(cancellationToken),
78-
cancellationToken).ConfigureAwait(false);
79-
}
36+
return optionPersisterProviders.SelectAsArray(
37+
static provider => provider.Value.GetOrCreatePersister());
8038
}
8139

8240
private static object? LoadOptionFromPersisterOrGetDefault(OptionKey2 optionKey, ImmutableArray<IOptionPersister> persisters)
@@ -108,16 +66,15 @@ public T GetOption<T>(PerLanguageOption2<T> option, string language)
10866

10967
public T GetOption<T>(OptionKey2 optionKey)
11068
{
111-
// Ensure the option persisters are available before taking the global lock
112-
var persisters = GetOptionPersisters();
113-
11469
// Performance: This is called very frequently, with the vast majority (> 99%) of calls requesting a previously
11570
// added key. In those cases, we can avoid taking the lock as _currentValues is an immutable structure.
11671
if (_currentValues.TryGetValue(optionKey, out var value))
11772
{
11873
return (T)value!;
11974
}
12075

76+
// Ensure the option persisters are available before taking the global lock
77+
var persisters = _optionPersisters.Value;
12178
lock (_gate)
12279
{
12380
return (T)GetOption_NoLock(ref _currentValues, optionKey, persisters)!;
@@ -126,8 +83,6 @@ public T GetOption<T>(OptionKey2 optionKey)
12683

12784
public ImmutableArray<object?> GetOptions(ImmutableArray<OptionKey2> optionKeys)
12885
{
129-
// Ensure the option persisters are available before taking the global lock
130-
var persisters = GetOptionPersisters();
13186
using var values = TemporaryArray<object?>.Empty;
13287

13388
// 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)
146101

147102
if (values.Count != optionKeys.Length)
148103
{
104+
// Ensure the option persisters are available before taking the global lock
105+
var persisters = _optionPersisters.Value;
149106
lock (_gate)
150107
{
151108
foreach (var optionKey in optionKeys)
@@ -190,7 +147,7 @@ public bool SetGlobalOptions(ImmutableArray<KeyValuePair<OptionKey2, object?>> o
190147
private bool SetGlobalOptions(OneOrMany<KeyValuePair<OptionKey2, object?>> options)
191148
{
192149
using var _ = ArrayBuilder<(OptionKey2, object?)>.GetInstance(options.Count, out var changedOptions);
193-
var persisters = GetOptionPersisters();
150+
var persisters = _optionPersisters.Value;
194151

195152
lock (_gate)
196153
{

src/Workspaces/Core/Portable/Options/IOptionPersisterProvider.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Threading;
6-
using System.Threading.Tasks;
7-
85
namespace Microsoft.CodeAnalysis.Options;
96

107
internal interface IOptionPersisterProvider
@@ -16,7 +13,6 @@ internal interface IOptionPersisterProvider
1613
/// This method is safe for concurrent use from any thread. No guarantees are made regarding the use of the UI
1714
/// thread.
1815
/// </remarks>
19-
/// <param name="cancellationToken">A cancellation token the operation may observe.</param>
2016
/// <returns>The option persister.</returns>
21-
ValueTask<IOptionPersister> GetOrCreatePersisterAsync(CancellationToken cancellationToken);
17+
IOptionPersister GetOrCreatePersister();
2218
}

0 commit comments

Comments
 (0)