-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
WIP: First attempt at cleaning up package load to do less thread switching during InitializeAsync #77368
base: main
Are you sure you want to change the base?
WIP: First attempt at cleaning up package load to do less thread switching during InitializeAsync #77368
Conversation
… during InitializeAsync
@@ -27,16 +27,20 @@ internal IComponentModel ComponentModel | |||
|
|||
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress) | |||
{ | |||
await base.InitializeAsync(cancellationToken, progress).ConfigureAwait(true); | |||
// Should only be called from a threadpool. Opinionated, as package load sequence thread switches are impactful. | |||
Contract.ThrowIfTrue(JoinableTaskFactory.Context.IsOnMainThread); |
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.
We inline you on UI thread if sync blocked so this will fire.
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.
This is a base class, the derived classes ensure we aren't on the main thread
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.
In general, we don't want packages to make that decision; we inline you on the UI thread because its not doing anything, if you queue to the thread pool you will now be affected by latency of the thread pool, instead of just immediately running on the thread that is currently blocked on you,
|
||
_componentModel_doNotAccessDirectly = (IComponentModel?)await GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(true); | ||
_componentModel_doNotAccessDirectly = await GetServiceAsync<SComponentModel, IComponentModel>(throwOnFailure: true, cancellationToken).ConfigureAwait(true); |
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 don't need to retrieve this here, your package unneccesarily waits on the MEF cache to read or be rebuilt here, just retrieve at exact moment you need.
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.
I started going down that path, and maybe it's something to pursue, but there are a lot of consumers of the ComponentModel property, and making them async wasn't going to be simple
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.
Took another look, and this is not going to be a simple thing to defer (for long, at least).
For example, in RoslynPackage.InitializeAsync, it calls base.InitializeAsync (which sets _componentModel_doNotAccessDirectly), and then two of the next three statements end up accessing ComponentModel and calling methods on the items returned from the MEF catalog.
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.
Took some stopwatch data. About 500 ms of the 700 ms I see in InitializeAsync is due to obtaining MEF. Need to think on this more.
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.
That's when its up-to-date that's 15 seconds+ on a full rebuild.
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.
Good to know. Let's bite off pieces as we can. I'll be coming back from vacation on Monday and can help Todd out with this.
We'll likely have several PRs to get through the bulk of the work, depending on how invasive the changes are.
@toddgrunke feel free to open an issue to help track upcoming immediate work. Thanks!
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.
Also, we should def be measuring both "warm Mef" and "full Mef rebuild time"
@davekean how possible would it be to have dedicated speedometer tests for both? It would be great for measuring our progress here over time, and especially good for preventing regressions.
If we already have tests here (say... Time to open a basic project, and time to open a single file in a project), could you link is to those?
Thanks!
Assumes.Present(shell); | ||
Assumes.Present(solution); | ||
// Should only be called from a threadpool. Opinionated, as package load sequence thread switches are impactful. | ||
Contract.ThrowIfTrue(JoinableTaskFactory.Context.IsOnMainThread); |
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.
Ditto.
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.
I don't think this can be removed, and is safe to do as the derived classes make sure this assumption is valid.
|
||
var miscellaneousFilesWorkspace = this.ComponentModel.GetService<MiscellaneousFilesWorkspace>(); | ||
RegisterMiscellaneousFilesWorkspaceInformation(miscellaneousFilesWorkspace); | ||
|
||
await shell.LoadPackageAsync(Guids.RoslynPackageId); |
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.
I know we're already doing this, but would want to know why we need to do this.
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.
I think it's necessary, but it's maybe something which could be improved. Is there a way to specify inter-package dependencies other than manually loading like this?
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.
Package loads generally shouldn't have side-effects, so any behavior its depending on should be expressed in a form that is more explicit such as a service retrieval.
2) move getting the MiscellaneousFilesWorkspace to the bg thread 3) Get rid of some useless calls into base class (AsyncPackage)
…rom main thread 2) change call to base.OnAfterPackageLoadedAsync to use ConfigureAwait(true) to ensure we capture the calling context.
remove assumption about LoadComponentsAsync calling thread
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); | ||
|
||
_componentModel_doNotAccessDirectly = (IComponentModel?)await GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(true); | ||
_componentModel_doNotAccessDirectly = await GetServiceAsync<SComponentModel, IComponentModel>(throwOnFailure: true, cancellationToken).ConfigureAwait(true); |
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.
Ok to not do the base initialize?
Assumes.Present(_componentModel_doNotAccessDirectly); | ||
} | ||
|
||
protected override async Task OnAfterPackageLoadedAsync(CancellationToken cancellationToken) | ||
{ | ||
await base.OnAfterPackageLoadedAsync(cancellationToken).ConfigureAwait(false); |
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.
Just trying to understand why this is ok. Is it just known that the base does nothing?
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.
I documented this.
The default implementation of this method does nothing and there is no requirement to call it in an override.
Could you write up what work may remain? Like deferring the work to access Mef? Thanks! |
This is going to be a dev18 PR, just wanted to get something out to start the conversation about what I've currently got.
Manual testing of opening Roslyn.sln with a single file opened in the sln showed the stopwatch time spent in CSharpPackage.InitializeAsync from about 2000 ms without this change to about 700 ms with this change.
Test insertion to see if speedometer shows anything:
commit 6: https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_build/results?buildId=11106739&view=results