Skip to content

Commit e8f8e9d

Browse files
authored
Further reduce thread switches during package load. (#77602)
Further reduce thread switches during package load. The Rolsyn package load sequence is quite confusing, and currently does quite a few thread switches. Trying re-order statements is difficult, and fraught with danger if one of the calls is entered or returns on an unexpected thread. For example, one component created during load, the ColorSchemeApplier, needs to be initialized during this sequence, but does up to three thread switches during it's initialization. Every time work is added to the package load sequence, we run a pretty high risk of adding new thread switches during this critical period. Instead, this change allows packages to add work to main thread or background thread work queues. Items in each of these queues are batched together and can use a single thread switch for the entire batch. These queues can be added to during processing, allowing packages to "discover" new work that needs to be processed as they proceed.
1 parent 4eee7f0 commit e8f8e9d

File tree

9 files changed

+221
-75
lines changed

9 files changed

+221
-75
lines changed

src/VisualStudio/CSharp/Impl/CSharpPackage.cs

+10-3
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,17 @@ internal sealed class CSharpPackage : AbstractPackage<CSharpPackage, CSharpLangu
5858
private ObjectBrowserLibraryManager? _libraryManager;
5959
private uint _libraryManagerCookie;
6060

61-
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
61+
protected override void RegisterInitializationWork(PackageRegistrationTasks packageRegistrationTasks)
62+
{
63+
base.RegisterInitializationWork(packageRegistrationTasks);
64+
65+
packageRegistrationTasks.AddTask(isMainThreadTask: false, task: PackageInitializationBackgroundThreadAsync);
66+
}
67+
68+
private Task PackageInitializationBackgroundThreadAsync(IProgress<ServiceProgressData> progress, PackageRegistrationTasks packageRegistrationTasks, CancellationToken cancellationToken)
6269
{
6370
try
6471
{
65-
await base.InitializeAsync(cancellationToken, progress).ConfigureAwait(false);
66-
6772
this.RegisterService<ICSharpTempPECompilerService>(async ct =>
6873
{
6974
var workspace = this.ComponentModel.GetService<VisualStudioWorkspace>();
@@ -74,6 +79,8 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
7479
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, ErrorSeverity.General))
7580
{
7681
}
82+
83+
return Task.CompletedTask;
7784
}
7885

7986
protected override void RegisterObjectBrowserLibraryManager()

src/VisualStudio/Core/Def/ColorSchemes/ColorSchemeApplier.cs

+8-7
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
using Microsoft.CodeAnalysis.Shared.TestHooks;
1616
using Microsoft.CodeAnalysis.Threading;
1717
using Microsoft.VisualStudio;
18+
using Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
1819
using Microsoft.VisualStudio.Settings;
1920
using Microsoft.VisualStudio.Shell;
2021
using Microsoft.VisualStudio.Shell.Interop;
21-
using Microsoft.VisualStudio.Threading;
2222
using Task = System.Threading.Tasks.Task;
2323

2424
namespace Microsoft.CodeAnalysis.ColorSchemes;
@@ -60,7 +60,7 @@ public ColorSchemeApplier(
6060
threadingContext.DisposalToken);
6161
}
6262

63-
public async Task InitializeAsync(CancellationToken cancellationToken)
63+
public void RegisterInitializationWork(PackageRegistrationTasks packageRegistrationTasks)
6464
{
6565
lock (_gate)
6666
{
@@ -70,15 +70,16 @@ public async Task InitializeAsync(CancellationToken cancellationToken)
7070
_isInitialized = true;
7171
}
7272

73-
// We need to update the theme whenever the Editor Color Scheme setting changes.
74-
await TaskScheduler.Default;
73+
packageRegistrationTasks.AddTask(isMainThreadTask: false, task: PackageInitializationBackgroundThreadAsync);
74+
}
75+
76+
private async Task PackageInitializationBackgroundThreadAsync(IProgress<ServiceProgressData> progress, PackageRegistrationTasks packageRegistrationTasks, CancellationToken cancellationToken)
77+
{
7578
var settingsManager = await _asyncServiceProvider.GetServiceAsync<SVsSettingsPersistenceManager, ISettingsManager>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
7679

77-
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
80+
// We need to update the theme whenever the Editor Color Scheme setting changes.
7881
settingsManager.GetSubset(ColorSchemeOptionsStorage.ColorSchemeSettingKey).SettingChangedAsync += ColorSchemeChangedAsync;
7982

80-
await TaskScheduler.Default;
81-
8283
// Try to migrate the `useEnhancedColorsSetting` to the new `ColorSchemeName` setting.
8384
_settings.MigrateToColorSchemeSetting();
8485

src/VisualStudio/Core/Def/LanguageService/AbstractPackage.cs

+32-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
using System;
66
using System.Threading;
7+
using System.Threading.Tasks;
78
using Microsoft.CodeAnalysis.Options;
89
using Microsoft.VisualStudio.ComponentModelHost;
910
using Microsoft.VisualStudio.Shell;
1011
using Microsoft.VisualStudio.Threading;
11-
using Task = System.Threading.Tasks.Task;
1212

1313
namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
1414

@@ -25,10 +25,38 @@ internal IComponentModel ComponentModel
2525
}
2626
}
2727

28-
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
28+
protected virtual void RegisterInitializationWork(PackageRegistrationTasks packageRegistrationTasks)
2929
{
30-
_componentModel_doNotAccessDirectly = (IComponentModel?)await GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(false);
31-
Assumes.Present(_componentModel_doNotAccessDirectly);
30+
// This treatment of registering work on the bg/main threads is a bit unique as we want the component model initialized at the beginning
31+
// of whichever context is invoked first. The current architecture doesn't execute any of the registered tasks concurrently,
32+
// so that isn't a concern for running calculating or setting _componentModel_doNotAccessDirectly multiple times.
33+
packageRegistrationTasks.AddTask(isMainThreadTask: false, task: EnsureComponentModelAsync);
34+
packageRegistrationTasks.AddTask(isMainThreadTask: true, task: EnsureComponentModelAsync);
35+
36+
async Task EnsureComponentModelAsync(IProgress<ServiceProgressData> progress, PackageRegistrationTasks packageRegistrationTasks, CancellationToken token)
37+
{
38+
if (_componentModel_doNotAccessDirectly == null)
39+
{
40+
_componentModel_doNotAccessDirectly = (IComponentModel?)await GetServiceAsync(typeof(SComponentModel)).ConfigureAwait(false);
41+
Assumes.Present(_componentModel_doNotAccessDirectly);
42+
}
43+
}
44+
}
45+
46+
/// This method is called upon package creation and is the mechanism by which roslyn packages calculate and
47+
/// process all package initialization work. Do not override this sealed method, instead override RegisterInitializationWork
48+
/// to indicate the work your package needs upon initialization.
49+
protected sealed override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
50+
{
51+
var packageRegistrationTasks = new PackageRegistrationTasks(JoinableTaskFactory);
52+
53+
// Request all initially known work, classified into whether it should be processed on the main or
54+
// background thread. These lists can be modified by the work itself to add more work for subsequent processing.
55+
// Requesting this information is useful as it lets us batch up work on these threads, significantly
56+
// reducing thread switches during package load.
57+
RegisterInitializationWork(packageRegistrationTasks);
58+
59+
await packageRegistrationTasks.ProcessTasksAsync(progress, cancellationToken).ConfigureAwait(false);
3260
}
3361

3462
protected override async Task OnAfterPackageLoadedAsync(CancellationToken cancellationToken)

src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs

+20-6
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,21 @@ protected AbstractPackage()
3434
{
3535
}
3636

37-
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
37+
protected override void RegisterInitializationWork(PackageRegistrationTasks packageRegistrationTasks)
3838
{
39-
await base.InitializeAsync(cancellationToken, progress).ConfigureAwait(false);
39+
base.RegisterInitializationWork(packageRegistrationTasks);
4040

41-
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
41+
packageRegistrationTasks.AddTask(isMainThreadTask: true, task: PackageInitializationMainThreadAsync);
42+
}
43+
44+
private async Task PackageInitializationMainThreadAsync(IProgress<ServiceProgressData> progress, PackageRegistrationTasks packageRegistrationTasks, CancellationToken cancellationToken)
45+
{
46+
// This code uses various main thread only services, so it must run completely on the main thread
47+
// (thus the CA(true) usage throughout)
48+
Contract.ThrowIfFalse(JoinableTaskFactory.Context.IsOnMainThread);
4249

4350
var shell = (IVsShell7?)await GetServiceAsync(typeof(SVsShell)).ConfigureAwait(true);
44-
var solution = (IVsSolution?)await GetServiceAsync(typeof(SVsSolution)).ConfigureAwait(true);
4551
Assumes.Present(shell);
46-
Assumes.Present(solution);
4752

4853
_shell = (IVsShell?)shell;
4954
Assumes.Present(_shell);
@@ -66,9 +71,11 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
6671
return _languageService.ComAggregate!;
6772
});
6873

74+
var miscellaneousFilesWorkspace = this.ComponentModel.GetService<MiscellaneousFilesWorkspace>();
75+
76+
// awaiting an IVsTask guarantees to return on the captured context
6977
await shell.LoadPackageAsync(Guids.RoslynPackageId);
7078

71-
var miscellaneousFilesWorkspace = this.ComponentModel.GetService<MiscellaneousFilesWorkspace>();
7279
RegisterMiscellaneousFilesWorkspaceInformation(miscellaneousFilesWorkspace);
7380

7481
if (!_shell.IsInCommandLineMode())
@@ -77,6 +84,13 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
7784
// this is a no op
7885
RegisterObjectBrowserLibraryManager();
7986
}
87+
}
88+
89+
protected override async Task OnAfterPackageLoadedAsync(CancellationToken cancellationToken)
90+
{
91+
await base.OnAfterPackageLoadedAsync(cancellationToken).ConfigureAwait(false);
92+
93+
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
8094

8195
LoadComponentsInUIContextOnceSolutionFullyLoadedAsync(cancellationToken).Forget();
8296
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.VisualStudio.Shell;
10+
using Microsoft.VisualStudio.Threading;
11+
12+
namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService;
13+
14+
using WorkTask = Func<IProgress<ServiceProgressData>, PackageRegistrationTasks, CancellationToken, Task>;
15+
16+
/// <summary>
17+
/// Provides a mechanism for registering work to be done during package initialization. Work is registered
18+
/// as either main thread or background thread appropriate. This allows processing of these work items
19+
/// in a batched manner, reducing the number of thread switches required during the performance sensitive
20+
/// package loading timeframe.
21+
///
22+
/// Note that currently the processing of these tasks isn't done concurrently. A future optimization may
23+
/// allow parallel background thread task execution, or even concurrent main and background thread work.
24+
/// </summary>
25+
internal sealed class PackageRegistrationTasks(JoinableTaskFactory jtf)
26+
{
27+
private readonly ConcurrentQueue<WorkTask> _backgroundThreadWorkTasks = [];
28+
private readonly ConcurrentQueue<WorkTask> _mainThreadWorkTasks = [];
29+
private readonly JoinableTaskFactory _jtf = jtf;
30+
31+
public void AddTask(bool isMainThreadTask, WorkTask task)
32+
{
33+
var workTasks = GetWorkTasks(isMainThreadTask);
34+
workTasks.Enqueue(task);
35+
}
36+
37+
public async Task ProcessTasksAsync(IProgress<ServiceProgressData> progress, CancellationToken cancellationToken)
38+
{
39+
// prime the pump by doing the first group of bg thread work if the initiating thread is not the main thread
40+
if (!_jtf.Context.IsOnMainThread)
41+
await PerformWorkAsync(isMainThreadTask: false, progress, cancellationToken).ConfigureAwait(false);
42+
43+
// Continue processing work until everything is completed, switching between main and bg threads as needed.
44+
while (!_mainThreadWorkTasks.IsEmpty || !_backgroundThreadWorkTasks.IsEmpty)
45+
{
46+
await PerformWorkAsync(isMainThreadTask: true, progress, cancellationToken).ConfigureAwait(false);
47+
await PerformWorkAsync(isMainThreadTask: false, progress, cancellationToken).ConfigureAwait(false);
48+
}
49+
}
50+
51+
private ConcurrentQueue<WorkTask> GetWorkTasks(bool isMainThreadTask)
52+
=> isMainThreadTask ? _mainThreadWorkTasks : _backgroundThreadWorkTasks;
53+
54+
private async Task PerformWorkAsync(bool isMainThreadTask, IProgress<ServiceProgressData> progress, CancellationToken cancellationToken)
55+
{
56+
var workTasks = GetWorkTasks(isMainThreadTask);
57+
if (workTasks.IsEmpty)
58+
return;
59+
60+
// Ensure we're invoking the task on the right thread
61+
if (isMainThreadTask)
62+
await _jtf.SwitchToMainThreadAsync(cancellationToken);
63+
else if (_jtf.Context.IsOnMainThread)
64+
await TaskScheduler.Default;
65+
66+
while (workTasks.TryDequeue(out var work))
67+
{
68+
// CA(true) is important here, as we want to ensure that each iteration is done in the same
69+
// captured context. Thus, even poorly behaving tasks (ie, those that do their own thread switching)
70+
// don't effect the next loop iteration.
71+
await work(progress, this, cancellationToken).ConfigureAwait(true);
72+
}
73+
}
74+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ internal sealed class VisualStudioSettingsOptionPersister
3535
= ImmutableDictionary<string, (OptionKey2, string)>.Empty;
3636

3737
/// <remarks>
38-
/// We make sure this code is from the UI by asking for all <see cref="IOptionPersister"/> in <see cref="RoslynPackage.InitializeAsync"/>
38+
/// We make sure this code is from the UI by asking for all <see cref="IOptionPersister"/> in <see cref="RoslynPackage.OnAfterPackageLoadedAsync"/>
3939
/// </remarks>
4040
public VisualStudioSettingsOptionPersister(Action<OptionKey2, object?> refreshOption, ImmutableDictionary<string, Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>> readFallbacks, ISettingsManager settingsManager)
4141
{

src/VisualStudio/Core/Def/ProjectSystem/MiscellaneousFilesWorkspace.cs

-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ public MiscellaneousFilesWorkspace(
7474

7575
public async Task InitializeAsync()
7676
{
77-
await TaskScheduler.Default;
7877
_textManager = await _textManagerService.GetValueAsync().ConfigureAwait(false);
7978
}
8079

0 commit comments

Comments
 (0)