-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Make GlobalOptionService initialization synchronous. #77823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
{ | ||
try | ||
{ | ||
return (I?)await _serviceProvider.GetServiceAsync(typeof(T)).ConfigureAwait(false); | ||
return (I?)_serviceProvider.GetService(typeof(T)); | ||
} | ||
catch (Exception e) when (FatalError.ReportAndPropagate(e)) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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.) |
||
|
||
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; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to add a comment we're using IServiceProvider versus the async forms since we know this is a constrained case.