Skip to content

Conversation

ToddGrun
Copy link
Contributor

@ToddGrun ToddGrun commented Mar 25, 2025

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.

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.
@ToddGrun ToddGrun requested a review from a team as a code owner March 25, 2025 23:30
@ghost ghost added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Mar 25, 2025
@ToddGrun
Copy link
Contributor Author

Hopefully third times a charm

@@ -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>();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ = ComponentModel.GetService();

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.

Copy link
Member

Choose a reason for hiding this comment

The 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.)

@tmat
Copy link
Member

tmat commented Mar 26, 2025

@jasonmalinowski @sharwell ptal

@ToddGrun
Copy link
Contributor Author

@jasonmalinowski @sharwell -- this approach good for you?

@jasonmalinowski
Copy link
Member

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!)

@@ -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;
Copy link
Member

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.

}
}

/// <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
Copy link
Member

Choose a reason for hiding this comment

The 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.

@@ -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>();
Copy link
Member

Choose a reason for hiding this comment

The 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.)

@ToddGrun ToddGrun merged commit 9c09627 into dotnet:release/dev17.15 Mar 27, 2025
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead VSCode
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants