Skip to content

Commit d3e916a

Browse files
Add the ability to preload parts of the project system
For the csproj and msvbprj project systems, our COM services get called on the UI thread when project information is being sent our way. This has the side effect of creating all our "lazily created" MEF services on that UI thread during that time, which can result in more unnecessary blocking. Changing how those project systems work would be a hugely complicated change, but what we can do is take advantage of the preloading support that exists in VS to ensure we preload what we can off the UI thread before we hit the legacy code.
1 parent f31d317 commit d3e916a

File tree

6 files changed

+66
-2
lines changed

6 files changed

+66
-2
lines changed

src/VisualStudio/CSharp/Impl/CSharpPackage.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ namespace Microsoft.VisualStudio.LanguageServices.CSharp.LanguageService;
5454
[ProvideLanguageEditorOptionPage(typeof(Options.IntelliSenseOptionPage), "CSharp", null, "IntelliSense", pageNameResourceId: "#103", keywordListResourceId: 312)]
5555
[ProvideSettingsManifest(PackageRelativeManifestFile = @"UnifiedSettings\csharpSettings.registration.json")]
5656
[ProvideService(typeof(ICSharpTempPECompilerService), IsAsyncQueryable = false, IsCacheable = true, IsFreeThreaded = true, ServiceName = "C# TempPE Compiler Service")]
57+
// ICSharpProjectHost requests the language service under the covers, and since that needs the UI thread to create a COM aggregation wrapper, this cannot be free-threaded either
58+
[ProvideService(typeof(ICSharpProjectHost), IsAsyncQueryable = true, IsCacheable = true, IsFreeThreaded = false, ServiceName = nameof(ICSharpProjectHost))]
5759
[Guid(Guids.CSharpPackageIdString)]
5860
internal sealed class CSharpPackage : AbstractPackage<CSharpPackage, CSharpLanguageService>, IVsUserSettingsQuery
5961
{
@@ -76,6 +78,16 @@ private Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks package
7678
var workspace = this.ComponentModel.GetService<VisualStudioWorkspace>();
7779
return new TempPECompilerService(workspace.Services.GetService<IMetadataService>());
7880
}, promote: true);
81+
82+
// Historically, the ICSharpProjectHost was fetched by calling QueryService for the CSharpLanguage service,
83+
// and then casting it to ICSharpProjectHost. We keep that mechanism in place, but we now expose ICSharpProjectHost
84+
// as it's own service directly, so that way the request for it is clear and we can do preloading as needed.
85+
AddService(typeof(ICSharpProjectHost), (_, cancellationToken, _) =>
86+
{
87+
PreloadProjectSystemComponents();
88+
return GetServiceAsync(typeof(CSharpLanguageService), swallowExceptions: false);
89+
},
90+
promote: true);
7991
}
8092
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, ErrorSeverity.General))
8193
{

src/VisualStudio/CSharp/Impl/PackageRegistration.pkgdef

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
"Name"="C# Language Service"
3535
"IsAsyncQueryable"=dword:00000001
3636

37+
[$RootKey$\Services\{1F3B9583-A66A-4be1-A15B-901DA4DB4ACF}]
38+
@="{13c3bbb4-f18f-4111-9f54-a0fb010d9194}"
39+
"Name"="ICSharpProjectHost"
40+
"IsAsyncQueryable"=dword:00000001
41+
"IsCacheable"=dword:00000001
42+
"IsFreeThreaded"=dword:00000000
43+
3744
[$RootKey$\Services\{dba64c84-56df-4e20-8aa6-02332a97f474}]
3845
@="{13c3bbb4-f18f-4111-9f54-a0fb010d9194}"
3946
"Name"="C# TempPE Compiler Service"

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ internal abstract partial class AbstractPackage<TPackage, TLanguageService> : Ab
2828
private VisualStudioSymbolSearchService? _symbolSearchService;
2929
private IVsShell? _shell;
3030

31+
/// <summary>
32+
/// Set to true if we've already preloaded project system components.
33+
/// </summary>
34+
private bool _projectSystemComponentsPreloaded;
35+
3136
protected AbstractPackage()
3237
{
3338
}
@@ -177,4 +182,29 @@ protected virtual void UnregisterObjectBrowserLibraryManager()
177182
// it is virtual rather than abstract to not break other languages which derived from our
178183
// base package implementations
179184
}
185+
186+
protected void PreloadProjectSystemComponents()
187+
{
188+
// No reason to do this more than once, but if we do, there's no harm, since we're just fetching services from MEF.
189+
if (_projectSystemComponentsPreloaded)
190+
return;
191+
192+
_projectSystemComponentsPreloaded = true;
193+
194+
// Preload some components so later uses don't block. This is specifically to help out csproj and msvbprj project systems. They push changes
195+
// to us on the UI thread as fundamental part of their design. This causes blocking on the UI thread as we create MEF components and JIT code
196+
// for the first time, even though those components could have been loaded on the background thread first. This method is called from the two places
197+
// we expose a service for the project systems to create us; this can be called by VS's preloading support on a background thread to ensure this is ran
198+
// on a background thread so the later calls on the UI thread will block less. For CPS projects, we don't need this, as CPS already creates us on
199+
// background threads, so we can just let things load as they're pulled in.
200+
//
201+
// The expectation is no thread switching should happen here; if we're being called on a background thread that means we're being pulled in
202+
// by the preloading logic. If we're being called on the UI thread, that means we might be getting created directly by the project systems
203+
// and the UI thread is already blocked, so a switch to the background thread won't unblock the UI thread and might delay us even further.
204+
//
205+
// As long as there's something that's not cheap to load later, and it'll definitely be used in all csproj/msvbprj scenarios, then it's worth
206+
// putting here to preload.
207+
var workspace = this.ComponentModel.GetService<VisualStudioWorkspaceImpl>();
208+
workspace.PreloadProjectSystemComponents(this.RoslynLanguageName);
209+
}
180210
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,4 +1630,16 @@ private async Task UpdateUIContextAsync(string language, CancellationToken cance
16301630
// value after the main thread switch.
16311631
uiContext.IsActive = this.CurrentSolution.Projects.Any(p => p.Language == language);
16321632
}
1633+
1634+
internal void PreloadProjectSystemComponents(string languageName)
1635+
{
1636+
// Ensure we have any listeners for WellKnownEventListeners.Workspace warmed up, since these are otherwise created
1637+
// the first time we make a change to the CurrentSolution
1638+
base.EnsureEventListeners();
1639+
1640+
// Load up the command line parser and warm it up. This generally ensures we have our language specific binaries loaded
1641+
// and we have the command line parser ready to go, since those tend to be more expensive things to JIT.
1642+
var commandLineParserService = Services.GetRequiredLanguageService<ICommandLineParserService>(languageName);
1643+
commandLineParserService.Parse([], null, isInteractive: false, sdkDirectory: null);
1644+
}
16331645
}

src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic
7474
isMainThreadTask:=False,
7575
task:=Function() As Task
7676
Try
77-
AddService(GetType(IVbCompilerService), Function(_1, cancellationToken, _2) Task.FromResult(_comAggregate), promote:=True)
77+
AddService(GetType(IVbCompilerService), Function(_1, cancellationToken, _2)
78+
PreloadProjectSystemComponents()
79+
Return Task.FromResult(_comAggregate)
80+
End Function, promote:=True)
7881

7982
DirectCast(Me, IServiceContainer).AddService(
8083
GetType(IVbTempPECompilerFactory),

src/Workspaces/Core/Portable/Workspace/Workspace_Events.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ private EventHandlerSet GetEventHandlers(WorkspaceEventType eventType)
189189
return _eventMap.GetEventHandlerSet(eventType);
190190
}
191191

192-
private void EnsureEventListeners()
192+
private protected void EnsureEventListeners()
193193
{
194194
// Cache this service so it doesn't need to be retrieved from MEF during disposal.
195195
_workspaceEventListenerService ??= this.Services.GetService<IWorkspaceEventListenerService>();

0 commit comments

Comments
 (0)