Skip to content

Commit 74b5a93

Browse files
committed
Cache MEF composition in OOP and VS Code
1 parent ce330d0 commit 74b5a93

File tree

13 files changed

+300
-16
lines changed

13 files changed

+300
-16
lines changed

eng/targets/Services.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SemanticTokens" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSemanticTokensService+Factory" />
2020
<ServiceHubService Include="Microsoft.VisualStudio.Razor.HtmlDocument" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteHtmlDocumentService+Factory" />
2121
<ServiceHubService Include="Microsoft.VisualStudio.Razor.TagHelperProvider" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteTagHelperProviderService+Factory"/>
22+
<ServiceHubService Include="Microsoft.VisualStudio.Razor.MEFInitialization" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteMEFInitializationService+Factory" />
2223
<ServiceHubService Include="Microsoft.VisualStudio.Razor.ClientInitialization" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteClientInitializationService+Factory" />
2324
<ServiceHubService Include="Microsoft.VisualStudio.Razor.UriPresentation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteUriPresentationService+Factory" />
2425
<ServiceHubService Include="Microsoft.VisualStudio.Razor.FoldingRange" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFoldingRangeService+Factory" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.CodeAnalysis.Razor.Remote;
8+
9+
internal interface IRemoteMEFInitializationService : IRemoteJsonService
10+
{
11+
ValueTask InitializeAsync(string cacheDirectory, CancellationToken cancellationToken);
12+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ internal static class RazorServices
4343
(typeof(IRemoteCompletionService), null),
4444
(typeof(IRemoteCodeActionsService), null),
4545
(typeof(IRemoteFindAllReferencesService), null),
46+
(typeof(IRemoteMEFInitializationService), null),
4647
];
4748

4849
private const string ComponentName = "Razor";
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
4+
using System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Razor;
10+
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Api;
11+
using Microsoft.CodeAnalysis.Razor.Remote;
12+
using Microsoft.ServiceHub.Framework;
13+
using static Microsoft.CodeAnalysis.Remote.Razor.RazorBrokeredServiceBase;
14+
15+
namespace Microsoft.CodeAnalysis.Remote.Razor;
16+
17+
/// <summary>
18+
/// A special service that is used to initialize the MEF composition for Razor in the remote host.
19+
/// </summary>
20+
/// <remarks>
21+
/// It's special because it doesn't use MEF. Nor can it use anything else really.
22+
/// </remarks>
23+
internal sealed class RemoteMEFInitializationService : IRemoteMEFInitializationService
24+
{
25+
internal sealed class Factory : FactoryBase<IRemoteMEFInitializationService>
26+
{
27+
protected override Task<object> CreateInternalAsync(Stream? stream, IServiceProvider hostProvidedServices, IServiceBroker? serviceBroker)
28+
{
29+
var traceSource = (TraceSource?)hostProvidedServices.GetService(typeof(TraceSource));
30+
31+
var service = new RemoteMEFInitializationService();
32+
if (stream is not null)
33+
{
34+
var serverConnection = CreateServerConnection(stream, traceSource);
35+
ConnectService(serverConnection, service);
36+
}
37+
38+
return Task.FromResult<object>(service);
39+
}
40+
41+
protected override IRemoteMEFInitializationService CreateService(in ServiceArgs args)
42+
=> Assumed.Unreachable<IRemoteMEFInitializationService>("This service overrides CreateInternalAsync to avoid MEF instatiation, so the CreateService method should never be called.");
43+
}
44+
45+
public ValueTask InitializeAsync(string cacheDirectory, CancellationToken cancellationToken)
46+
{
47+
return RazorBrokeredServiceImplementation.RunServiceAsync(_ =>
48+
{
49+
RemoteMefComposition.CacheDirectory = cacheDirectory;
50+
return new();
51+
}, cancellationToken);
52+
}
53+
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorBrokeredServiceBase.FactoryBase`1.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ private Task<object> CreateAsync(Stream stream, IServiceProvider hostProvidedSer
6060
#endif
6161
}
6262

63-
protected async Task<object> CreateInternalAsync(
63+
protected virtual async Task<object> CreateInternalAsync(
6464
Stream? stream,
6565
IServiceProvider hostProvidedServices,
6666
IServiceBroker? serviceBroker)
@@ -103,21 +103,31 @@ protected async Task<object> CreateInternalAsync(
103103
return CreateService(in inProcArgs);
104104
}
105105

106+
var serverConnection = CreateServerConnection(stream, traceSource);
107+
108+
var args = new ServiceArgs(serviceBroker.AssumeNotNull(), exportProvider, targetLoggerFactory, workspaceProvider, serverConnection, brokeredServiceData?.Interceptor);
109+
var service = CreateService(in args);
110+
111+
ConnectService(serverConnection, service);
112+
113+
return service;
114+
}
115+
116+
protected static ServiceRpcDescriptor.RpcConnection CreateServerConnection(Stream stream, TraceSource? traceSource)
117+
{
106118
var pipe = stream.UsePipe();
107119

108120
var descriptor = typeof(IRemoteJsonService).IsAssignableFrom(typeof(TService))
109121
? RazorServices.JsonDescriptors.GetDescriptorForServiceFactory(typeof(TService))
110122
: RazorServices.Descriptors.GetDescriptorForServiceFactory(typeof(TService));
111123
var serverConnection = descriptor.WithTraceSource(traceSource).ConstructRpcConnection(pipe);
124+
return serverConnection;
125+
}
112126

113-
var args = new ServiceArgs(serviceBroker.AssumeNotNull(), exportProvider, targetLoggerFactory, workspaceProvider, serverConnection, brokeredServiceData?.Interceptor);
114-
115-
var service = CreateService(in args);
116-
127+
protected static void ConnectService(ServiceRpcDescriptor.RpcConnection serverConnection, TService service)
128+
{
117129
serverConnection.AddLocalRpcTarget(service);
118130
serverConnection.StartListening();
119-
120-
return service;
121131
}
122132
}
123133
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteMefComposition.cs

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Immutable;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.IO;
58
using System.Reflection;
69
using System.Threading;
710
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Razor;
12+
using Microsoft.AspNetCore.Razor.Utilities;
813
using Microsoft.VisualStudio.Composition;
914
using Microsoft.VisualStudio.Threading;
1015

@@ -21,9 +26,13 @@ internal sealed class RemoteMefComposition
2126
joinableTaskFactory: null);
2227

2328
private static readonly AsyncLazy<ExportProvider> s_lazyExportProvider = new(
24-
static () => CreateExportProviderAsync(CancellationToken.None),
29+
static () => CreateExportProviderAsync(CacheDirectory, CancellationToken.None),
2530
joinableTaskFactory: null);
2631

32+
private static Task? s_saveCacheFileTask;
33+
34+
public static string? CacheDirectory { get; set; }
35+
2736
/// <summary>
2837
/// Gets a <see cref="CompositionConfiguration"/> built from <see cref="Assemblies"/>. Note that the
2938
/// same <see cref="CompositionConfiguration"/> instance is returned for subsequent calls to this method.
@@ -52,17 +61,99 @@ private static async Task<CompositionConfiguration> CreateConfigurationAsync(Can
5261
/// Creates a new MEF composition and returns an <see cref="ExportProvider"/>. The catalog and configuration
5362
/// are reused for subsequent calls to this method.
5463
/// </summary>
55-
public static async Task<ExportProvider> CreateExportProviderAsync(CancellationToken cancellationToken)
64+
public static async Task<ExportProvider> CreateExportProviderAsync(string? cacheDirectory, CancellationToken cancellationToken)
5665
{
66+
var cache = new CachedComposition();
67+
var compositionCacheFile = GetCompositionCacheFile(cacheDirectory);
68+
if (await TryLoadCachedExportProviderAsync(cache, compositionCacheFile, cancellationToken).ConfigureAwait(false) is { } cachedProvider)
69+
{
70+
return cachedProvider;
71+
}
72+
5773
var configuration = await s_lazyConfiguration.GetValueAsync(cancellationToken).ConfigureAwait(false);
5874
cancellationToken.ThrowIfCancellationRequested();
5975

6076
var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
6177
var exportProviderFactory = runtimeComposition.CreateExportProviderFactory();
6278

79+
// We don't need to block on saving the cache, because if it fails or is corrupt, we'll just try again next time, but
80+
// we capture the task just so that tests can verify things.
81+
s_saveCacheFileTask = TrySaveCachedExportProviderAsync(cache, compositionCacheFile, runtimeComposition, cancellationToken);
82+
6383
return exportProviderFactory.CreateExportProvider();
6484
}
6585

86+
private static async Task<ExportProvider?> TryLoadCachedExportProviderAsync(CachedComposition cache, string? compositionCacheFile, CancellationToken cancellationToken)
87+
{
88+
if (compositionCacheFile is null)
89+
{
90+
return null;
91+
}
92+
93+
try
94+
{
95+
if (File.Exists(compositionCacheFile))
96+
{
97+
var resolver = new Resolver(SimpleAssemblyLoader.Instance);
98+
using var cacheStream = new FileStream(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
99+
var cachedFactory = await cache.LoadExportProviderFactoryAsync(cacheStream, resolver, cancellationToken).ConfigureAwait(false);
100+
return cachedFactory.CreateExportProvider();
101+
}
102+
}
103+
catch (Exception)
104+
{
105+
// We ignore all errors when loading the cache, because if the cache is corrupt we will just create a new export provider.
106+
}
107+
108+
return null;
109+
}
110+
111+
private static async Task TrySaveCachedExportProviderAsync(CachedComposition cache, string? compositionCacheFile, RuntimeComposition runtimeComposition, CancellationToken cancellationToken)
112+
{
113+
if (compositionCacheFile is null)
114+
{
115+
return;
116+
}
117+
118+
try
119+
{
120+
var cacheDirectory = Path.GetDirectoryName(compositionCacheFile).AssumeNotNull();
121+
var directoryInfo = Directory.CreateDirectory(cacheDirectory);
122+
123+
using var cacheStream = new FileStream(compositionCacheFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true);
124+
await cache.SaveAsync(runtimeComposition, cacheStream, cancellationToken).ConfigureAwait(false);
125+
}
126+
catch (Exception)
127+
{
128+
// We ignore all errors when saving the cache, because if something goes wrong, the next run will just create a new export provider.
129+
}
130+
}
131+
132+
[return: NotNullIfNotNull(nameof(cacheDirectory))]
133+
private static string? GetCompositionCacheFile(string? cacheDirectory)
134+
{
135+
if (cacheDirectory is null)
136+
{
137+
return null;
138+
}
139+
140+
var checksum = new Checksum.Builder();
141+
foreach (var assembly in Assemblies)
142+
{
143+
var assemblyPath = assembly.Location.AssumeNotNull();
144+
checksum.AppendData(Path.GetFileName(assemblyPath));
145+
checksum.AppendData(File.GetLastWriteTimeUtc(assemblyPath).ToString("F"));
146+
}
147+
148+
// Create base64 string of the hash.
149+
var hashAsBase64String = checksum.FreeAndGetChecksum().ToBase64String();
150+
151+
// Convert to filename safe base64 string.
152+
hashAsBase64String = hashAsBase64String.Replace('+', '-').Replace('/', '_').TrimEnd('=');
153+
154+
return Path.Combine(cacheDirectory, $"razor.mef.{hashAsBase64String}.cache");
155+
}
156+
66157
private sealed class SimpleAssemblyLoader : IAssemblyLoader
67158
{
68159
public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader();
@@ -83,4 +174,19 @@ public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath)
83174
return LoadAssembly(assemblyName);
84175
}
85176
}
177+
178+
public static class TestAccessor
179+
{
180+
public static Task? SaveCacheFileTask => s_saveCacheFileTask;
181+
182+
public static void ClearSaveCacheFileTask()
183+
{
184+
s_saveCacheFileTask = null;
185+
}
186+
187+
public static string GetCacheCompositionFile(string cacheDirectory)
188+
{
189+
return GetCompositionCacheFile(cacheDirectory);
190+
}
191+
}
86192
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Remote/RemoteServiceInvoker.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
1717
using Microsoft.CodeAnalysis.Razor.Telemetry;
1818
using Microsoft.CodeAnalysis.Razor.Workspaces;
19+
using Microsoft.VisualStudio.Settings;
20+
using Microsoft.VisualStudio.Shell;
21+
using Microsoft.VisualStudio.Shell.Settings;
1922

2023
namespace Microsoft.VisualStudio.Razor.Remote;
2124

@@ -26,12 +29,14 @@ internal sealed class RemoteServiceInvoker(
2629
LanguageServerFeatureOptions languageServerFeatureOptions,
2730
IClientCapabilitiesService clientCapabilitiesService,
2831
ISemanticTokensLegendService semanticTokensLegendService,
32+
SVsServiceProvider serviceProvider,
2933
ITelemetryReporter telemetryReporter,
3034
ILoggerFactory loggerFactory) : IRemoteServiceInvoker, IDisposable
3135
{
3236
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
3337
private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService;
3438
private readonly ISemanticTokensLegendService _semanticTokensLegendService = semanticTokensLegendService;
39+
private readonly IServiceProvider _serviceProvider = serviceProvider;
3540
private readonly ITelemetryReporter _telemetryReporter = telemetryReporter;
3641
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RemoteServiceInvoker>();
3742

@@ -163,8 +168,15 @@ async Task InitializeCoreAsync(bool oopInitialized, bool lspInitialized)
163168
await _initializeLspTask.ConfigureAwait(false);
164169
}
165170

166-
Task InitializeOOPAsync(RazorRemoteHostClient remoteClient)
171+
async Task InitializeOOPAsync(RazorRemoteHostClient remoteClient)
167172
{
173+
// The first call to OOP must be to initialize the MEF services, because everything after that relies MEF.
174+
var localSettingsDirectory = new ShellSettingsManager(_serviceProvider).GetApplicationDataFolder(ApplicationDataFolder.LocalSettings);
175+
var cacheDirectory = Path.Combine(localSettingsDirectory, "Razor", "RemoteMEFCache");
176+
await remoteClient.TryInvokeAsync<IRemoteMEFInitializationService>(
177+
(s, ct) => s.InitializeAsync(cacheDirectory, ct),
178+
_disposeTokenSource.Token).ConfigureAwait(false);
179+
168180
var initParams = new RemoteClientInitializationOptions
169181
{
170182
UseRazorCohostServer = _languageServerFeatureOptions.UseRazorCohostServer,
@@ -179,11 +191,10 @@ Task InitializeOOPAsync(RazorRemoteHostClient remoteClient)
179191

180192
_logger.LogDebug($"First OOP call, so initializing OOP service.");
181193

182-
return remoteClient
194+
await remoteClient
183195
.TryInvokeAsync<IRemoteClientInitializationService>(
184196
(s, ct) => s.InitializeAsync(initParams, ct),
185-
_disposeTokenSource.Token)
186-
.AsTask();
197+
_disposeTokenSource.Token).ConfigureAwait(false);
187198
}
188199

189200
Task InitializeLspAsync(RazorRemoteHostClient remoteClient)

src/Razor/src/Microsoft.VisualStudioCode.RazorExtension/Services/VSCodeRemoteServicesInitializer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Composition;
5+
using System.IO;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
89
using Microsoft.CodeAnalysis.Razor.Logging;
910
using Microsoft.CodeAnalysis.Razor.Remote;
1011
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
1112
using Microsoft.CodeAnalysis.Razor.Workspaces;
13+
using Microsoft.CodeAnalysis.Remote.Razor;
1214
using Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
1315

1416
namespace Microsoft.VisualStudioCode.RazorExtension.Services;
@@ -41,6 +43,9 @@ public async Task StartupAsync(VSInternalClientCapabilities clientCapabilities,
4143
// we know this is VS Code specific, its all just smoke and mirrors anyway. We can avoid the smoke :)
4244
var serviceInterceptor = new VSCodeBrokeredServiceInterceptor();
4345

46+
// First things first, set the cache directory for the MEF composition.
47+
RemoteMefComposition.CacheDirectory = Path.Combine(Path.GetDirectoryName(this.GetType().Assembly.Location)!, "cache");
48+
4449
var logger = _loggerFactory.GetOrCreateLogger<VSCodeRemoteServicesInitializer>();
4550
logger.LogDebug("Initializing remote services.");
4651
var service = await InProcServiceFactory.CreateServiceAsync<IRemoteClientInitializationService>(serviceInterceptor, _workspaceProvider, _loggerFactory).ConfigureAwait(false);

src/Razor/test/Microsoft.CodeAnalysis.Remote.Razor.Test/RazorServicesTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public void JsonServicesHaveTheRightParameters(Type serviceType, Type? _)
5454
{
5555
Assert.Fail($"Method {method.Name} in a Json service has a pinned solution info wrapper parameter that isn't Json serializable");
5656
}
57-
5857
}
5958
}
6059
}

0 commit comments

Comments
 (0)