Skip to content

Make LoadComponentsInUIContext async#29560

Merged
JieCarolHu merged 9 commits intodotnet:masterfrom
JieCarolHu:vso639092B
Aug 31, 2018
Merged

Make LoadComponentsInUIContext async#29560
JieCarolHu merged 9 commits intodotnet:masterfrom
JieCarolHu:vso639092B

Conversation

@JieCarolHu
Copy link
Copy Markdown
Contributor

@JieCarolHu JieCarolHu commented Aug 28, 2018

Fixes #29579

@JieCarolHu JieCarolHu added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Aug 28, 2018
@JieCarolHu JieCarolHu requested a review from a team as a code owner August 28, 2018 00:10

[ImportingConstructor]
public VisualStudioDiagnosticListTable(
IVsErrorList errorList,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not all types available through IServiceProvider are available through MEF. This is one of those types. Since you can't make the constructor for VisualStudioDiagnosticListTable asynchronous, there are three primary options here:

  1. Keep the constructor here the same as it was before, but ensure SVsErrorList has already been requested. In other words, update LoadComponentsInUIContextAsync to call GetServiceAsync(typeof(SVsErrorList)) before requesting an instance of this MEF type. This will ensure the interface is readily available when you GetService on the UI thread.
  2. Switch from simple construction to a construction+async initialization approach. After requesting the instance of VisualStudioDiagnosticListTable, immediately await InitializeAsync, and request the instance of SVsErrorList there.
  3. Make SVsErrorList lazily requested by asynchronous methods in the type, so the constructor is just a simple non-blocking constructor.

Option 1 is the easiest, but it's also fragile. I have a feeling there is a good fourth option, where we can use MEF metadata to declare the dependencies and then initialize them.

// placed on the VisualStudioDiagnosticListTable class
[DependsOnService(typeof(SVsErrorList))]

// Pseudo-code for LoadComponentsInUIContextAsync
foreach (var requiredService in GetRequiredServices())
{
  await GetServiceAsync(requiredService);
}

}

private void OnSolutionExistsAndFullyLoadedContext(object sender, UIContextChangedEventArgs e)
private async void OnSolutionExistsAndFullyLoadedContextAsync(object sender, UIContextChangedEventArgs e)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❗️ No async void

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is event handler so I have to use async void

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can make the callback a non-async method which invokes the async method through JoinableTaskFactory, using Run or RunAsync as appropriate.

protected override void LoadComponentsInUIContext(CancellationToken cancellationToken)
protected override async Task LoadComponentsInUIContextAsync(CancellationToken cancellationToken)
{
ForegroundObject.AssertIsForeground();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❗️ Don't call AssertIsForeground in an asynchronous method. Instance, use SwitchToMainThreadAsync to switch to the main thread if needed (the switch is a NOP if the code is already on the main thread).

📝 This appears a few times in this pull request, but I only commented on it here.

protected override async Task LoadComponentsInUIContextAsync(CancellationToken cancellationToken)
{
ForegroundObject.AssertIsForeground();
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❕ Pass cancellationToken

{
await GetServiceAsync(typeof(SVsTaskStatusCenterService));
var b = await GetServiceAsync(typeof(SVsErrorList));
await GetServiceAsync(typeof(SVsErrorList));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 A comment here would be good.

}

private void OnSolutionExistsAndFullyLoadedContext(object sender, UIContextChangedEventArgs e)
private async void OnSolutionExistsAndFullyLoadedContextAsync(object sender, UIContextChangedEventArgs e)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can make the callback a non-async method which invokes the async method through JoinableTaskFactory, using Run or RunAsync as appropriate.

}

LoadComponentsInUIContextOnceSolutionFullyLoaded(cancellationToken);
await LoadComponentsInUIContextOnceSolutionFullyLoadedAsync(cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 I suspect the problem is here. Try changing this to use _ = instead of await (fire and forget)


// load some services that have to be loaded in UI thread
LoadComponentsInUIContextOnceSolutionFullyLoaded(cancellationToken);
await LoadComponentsInUIContextOnceSolutionFullyLoadedAsync(cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 This is another one to call in a fire-and-forget manner.

@JieCarolHu JieCarolHu requested a review from sharwell August 29, 2018 00:46
@JieCarolHu JieCarolHu changed the title for personal review only Make LoadComponentsInUIContext async Aug 29, 2018
@JieCarolHu JieCarolHu removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Aug 29, 2018
_packageInstallerService = Workspace.Services.GetService<IPackageInstallerService>() as PackageInstallerService;
_symbolSearchService = Workspace.Services.GetService<ISymbolSearchService>() as VisualStudioSymbolSearchService;
_packageInstallerService = await GetServiceAsync(typeof(IPackageInstallerService)) as PackageInstallerService;
_symbolSearchService = await GetServiceAsync(typeof(ISymbolSearchService)) as VisualStudioSymbolSearchService;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❓ Did you verify that both of these return non-null?

protected override async Task LoadComponentsInUIContextAsync(CancellationToken cancellationToken)
{
ForegroundObject.AssertIsForeground();
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❗️ Pass cancellationToken

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we switch here, why did the caller also switch?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In this case, the caller does not need to switch because it only invokes an asynchronous method, and does not have any affinity itself.

In general, characteristics of the caller are never a consideration for the switching characteristics of the callee.

}

protected override void LoadComponentsInUIContext(CancellationToken cancellationToken)
protected override async Task LoadComponentsInUIContextAsync(CancellationToken cancellationToken)
Copy link
Copy Markdown
Contributor

@sharwell sharwell Aug 29, 2018

Choose a reason for hiding this comment

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

📝 Let me know what you think of IPreloadService in sharwell/roslyn@ded603f as a way to avoid hard-coding the services here. I don't consider it a perfect solution, so it may or may not be preferable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@sharwell It looks great! When can I use it?

await GetServiceAsync(typeof(SVsSolution));
await GetServiceAsync(typeof(SVsShell));
await GetServiceAsync(typeof(SVsRunningDocumentTable));
await GetServiceAsync(typeof(SVsTextManager));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

are these not cancellable?
no configure-awaiting? can you please put .ConfigureAwait(true) to indicate intent?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no configure-awaiting? can you please put .ConfigureAwait(true) to indicate intent?

This is a JTF aware class, so use of ConfigureAwait is discouraged to improve readability.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a JTF aware class

How does one know that? I ask honestly since i'm trying to figure out the how to assess code during reviews. I can accept that JTF stuff odesn't need this. But i'm curious how one tells the difference when looking at something like this. Thanks!

Copy link
Copy Markdown
Contributor

@sharwell sharwell Aug 29, 2018

Choose a reason for hiding this comment

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

How does one know that?

We don't have a great way to tell at the moment. So far only the package initialization code was migrated to the JTF form at the time we switched to AsyncPackage.

In general, my understanding is the break will occur at the Editor Features layer. Layer there and above can assume JTF is in use, and layers below it cannot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok. And the rule is: in the package code itself it should not configure-await? Does that also apply to any async methods in other services that it calls into?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And, for now, should i just consider this in AsyncPackage, and AbstsractLanguageService and those related classes? or is this just AsyncPackage?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in the package code itself it should not configure-await?

It can, but it's not a necessity and can hurt performance by increasing the number of context switches. This especially applies to the initialization code where things tend to need the main thread.

Does that also apply to any async methods in other services that it calls into?

No, callees can do whatever they want as long as they locally adhere to the threading rules.

And, for now, should i just consider this in AsyncPackage, and AbstsractLanguageService and those related classes? or is this just AsyncPackage?

Everything derived from AsyncPackage, including but not limited to types derived from AbstractPackage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It can, but it's not a necessity and can hurt performance by increasing the number of context switches.

Note: to be clear, i wasn't asking for ConfigureAwait(false). I was specifically just asking about explicit ConfigureAwaits.

The reason i like explicit (regardless of if its true/false) is that it makes it clear that the author thought about this and asked for the right semantics. So even if .ConfigureAwait(true) is technically unnecessary, it helps make it clear the intent of the code without someone having to go: "was that an accidental or intentional omission?".

protected override void LoadComponentsInUIContext(CancellationToken cancellationToken)
protected override async Task LoadComponentsInUIContextAsync(CancellationToken cancellationToken)
{
await GetServiceAsync(typeof(SVsTaskStatusCenterService));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AbstractPackage2 switched to the UI thread as teh first thing. But this code did not. intentional?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

GetServiceAsync is an asynchronous method. The caller doesn't need to switch threads in advance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm looking for some good, clear, rules about the exact patterns we should have for async. Specifically for JTFish code (since i think i have a good grasp on the roslyn rules for non-jtf code). It's hard to review this stuff since i can't tell what is intentional and part of an accepted pattern, vs waht may be a mistake by omission.

Copy link
Copy Markdown
Contributor

@sharwell sharwell Aug 29, 2018

Choose a reason for hiding this comment

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

For now, assume ConfigureAwait(false) should be applied in everything below EditorFeatures layer. There and up, it's used except in the following situations:

  • A method should not combine ConfigureAwait with one of the JTF switching mechanisms (await SwitchToMainThreadAsync or await TaskScheduler.Default). Use one or the other.
    • If an analyzer requires ConfigureAwait, it is preferable to combine ConfigureAwait(true) everywhere with the use of JTF switching mechanisms than to avoid JTF.
  • Async UI code and the package initialization code should use SwitchToMainThreadAsync (if necessary) and omit ConfigureAwait overall. Some legacy code has not transitioned to this pattern yet, which is apparent by the explicit use of ConfigureAwait(true).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we tweak this at all:

Async UI code and the package initialization code should use SwitchToMainThreadAsync (if necessary) and omit ConfigureAwait overall

Could we make that:

Async UI code and the package initialization code should use SwitchToMainThreadAsync (if necessary) and use .ConfigureAwait(true) overall

Having the ConfigureAwait be explicit in teh code makes it clear that the behavior is intentional and not he result of accidental omission. It's the same reason we ask that CancellationToken.None be explicitly passed instead of having that happen because of a default-parameter-value. By being explicit, someone looking at the code (1, 2, 5, years down the line) can reasonable assert: yes, the intent is clear here. there wasn't an accidental omission.

This sort of explicitness has been extremely valuable for us in the past. It has helped catch a bunch of issues, and it makes reviewing much simpler since it takes the guesswork out of things. It does make things more verbose. But i think that's a virtue here as being overly clear is better for these subtle areas.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see vs-threading as the writing on the wall for the eventually elimination of widespread littering of ConfigureAwait. We aren't ready to go down that road yet, but it's the target outcome.

I don't like preemptively changing styles when it's not been guaranteed what the final state will be. In this interim period, for example, it does lead to confusion (i.e. both these threads where i'm still not sure if this code is correct :)).

I'm not trying to hold anything back. I'm literally just trying to figure out what the rules are and how to reason about this code given those rules. Right now it's not at all clear to me (even if i take a stance of: "AbstractPackage is special and goes its own way").

Let's just look at this method on its own. First, i have no idea how to reason about what thread it is actually on. It says LoadComponentsInUIContextAsync. However, there is no assert to actualy verify that it's on the UI context. Then, it performs a bunch of async calls that are themselves not clear to me as to their intent (though i can come aronud around to the idea that if you knwo this is JTF, you can maybe squint and view no-configure-await as being intentional). Then, it runs code that affectively says "if i'm not on the UI thread, bad baddy badness will occur", but i see nothing that ensures that safety.

Overall, i find this pattern highly confusing and very difficult to reason about. Invariants are not made clear, and it seems far too easy to accidentally do the wrong thing.

Copy link
Copy Markdown
Contributor

@sharwell sharwell Aug 29, 2018

Choose a reason for hiding this comment

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

First of all, the discussion is great. It helps highlight things that aren't obvious.

It says LoadComponentsInUIContextAsync.

I renamed the method to LoadComponentsAsync. It's weird because UIContext here is a Visual Studio API concept independent from the concept of "main thread". Asynchronous methods shouldn't use the method name as an indicator of thread affinity, so we should come up with a new name for this method to avoid confusion.

Then, it performs a bunch of async calls that are themselves not clear to me as to their intent

I am very much interested in your opinion of whether the direction of sharwell/roslyn@ded603f makes things clearer (not considering its current lack of documentation).

Then, it runs code that affectively says "if i'm not on the UI thread, bad baddy badness will occur", but i see nothing that ensures that safety.

This is breaking the rules. Affinity should be explicit, not implicit.

... difficult to reason about ... Invariants are not made clear ...

If we add in the explicit SwitchToMainThreadAsync, does it address the confusion within this context?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we add in the explicit SwitchToMainThreadAsync, does it address the confusion within this context?

I need to see the final form. It's really hard to say without reviewing the code, along with the rules, and determining if it's clear :)

Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi Aug 29, 2018

Choose a reason for hiding this comment

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

I am very much interested in your opinion of whether the direction of sharwell/roslyn@ded603f makes things clearer (not considering its current lack of documentation).

Overall, yes :). I prefer declarative dependency declarations. Though i did have some comments on that PR around clarity.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jinujoseph @dotnet/roslyn-ide I think whether we want .ConfigureAwait(true) to be always added or not is debatable. probably better to be discussed in team meeting.

// unsubscribe from it
KnownUIContexts.SolutionExistsAndFullyLoadedContext.UIContextChanged -= OnSolutionExistsAndFullyLoadedContext;
await KnownUIContexts.SolutionExistsAndFullyLoadedContext;
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 This SwitchToMainThreadAsync can be removed, since the implementation of LoadComponentsInUIContextAsync will already be performing the switch if necessary.

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

Minor nit. This is what this thread looks like in my mail client:

image

If possible, even if a PR is for personal review, can it be given a good title? There is an appropriate label that can be used here to indicate it's not ready for reviewing. That would help in terms of mail client organization. Thanks!

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

It's not, it should have an explicit switch before that line.

I see. Ok. Let me wait for the final form of this PR. Hopefully it will make things clearer :) Let me know when it's ready for another pass. Thanks!

Comment thread src/VisualStudio/Core/Def/RoslynPackage.cs
Comment thread src/VisualStudio/Core/Def/RoslynPackage.cs
@JieCarolHu JieCarolHu requested a review from sharwell August 30, 2018 01:09
Comment thread src/VisualStudio/Core/Def/Implementation/LanguageService/AbstractPackage.cs Outdated
@JieCarolHu
Copy link
Copy Markdown
Contributor Author

retest windows_debug_vs-integration_prtest

@JieCarolHu
Copy link
Copy Markdown
Contributor Author

retest windows_release_vs-integration_prtest

@JieCarolHu
Copy link
Copy Markdown
Contributor Author

retest windows_debug_vs-integration_prtest

@JieCarolHu JieCarolHu closed this Aug 31, 2018
@JieCarolHu JieCarolHu reopened this Aug 31, 2018
@JieCarolHu JieCarolHu merged commit e086a1d into dotnet:master Aug 31, 2018
@JieCarolHu JieCarolHu deleted the vso639092B branch August 31, 2018 16:44
@sharwell sharwell added this to the 16.0 milestone Aug 31, 2018
protected override void LoadComponentsInUIContext(CancellationToken cancellationToken)
protected override async Task LoadComponentsAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JieCarolHu @sharwell
is there reason why we still has this method?

if we are going to keep this for whatever reason, is TaskStatusCenter and ErrorList actually require UI thread?

// load components
LoadComponentsInUIContext(CancellationToken.None);
}
await KnownUIContexts.SolutionExistsAndFullyLoadedContext;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not use SAsyncServiceProvider to push initializing some service until it is actually get used?

protected override async Task LoadComponentsAsync(CancellationToken cancellationToken)
{
ForegroundObject.AssertIsForeground();
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess this will be changed to @sharwell new JTF abstraction we will have in Roslyn side once that is checked in?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actually, here it should be just using JoinableTaskFactory (inherited from AsyncPackage). The MEF container is not (necessarily) initialized yet so we can't use IThreadingContext.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

code above already use ComponentHost so, MEF should be available here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants