Skip to content

Commit 2e9a0e7

Browse files
committed
Cache the MEF composition in the Roslyn LSP.
1 parent 361a0a8 commit 2e9a0e7

File tree

3 files changed

+94
-14
lines changed

3 files changed

+94
-14
lines changed

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ public static Task<ExportProvider> CreateExportProviderAsync(
3939
ExtensionLogDirectory: string.Empty);
4040
var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory);
4141
assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory);
42-
return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, loggerFactory);
42+
return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory: null, loggerFactory);
4343
}
4444
}

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Immutable;
6+
using System.Security.Cryptography;
7+
using System.Text;
68
using Microsoft.CodeAnalysis.LanguageServer.Logging;
79
using Microsoft.CodeAnalysis.LanguageServer.Services;
810
using Microsoft.CodeAnalysis.Shared.Collections;
@@ -18,6 +20,7 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
1820
ExtensionAssemblyManager extensionManager,
1921
IAssemblyLoader assemblyLoader,
2022
string? devKitDependencyPath,
23+
string? cacheDirectory,
2124
ILoggerFactory loggerFactory)
2225
{
2326
var logger = loggerFactory.CreateLogger<ExportProviderBuilder>();
@@ -38,17 +41,60 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
3841
// Add the extension assemblies to the MEF catalog.
3942
assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths);
4043

41-
logger.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}.");
44+
// Get the cached MEF composition or create a new one.
45+
var exportProviderFactory = await GetCompositionConfigurationAsync(assemblyPaths.ToImmutableArray(), assemblyLoader, cacheDirectory, logger);
46+
47+
// Create an export provider, which represents a unique container of values.
48+
// You can create as many of these as you want, but typically an app needs just one.
49+
var exportProvider = exportProviderFactory.CreateExportProvider();
50+
51+
// Immediately set the logger factory, so that way it'll be available for the rest of the composition
52+
exportProvider.GetExportedValue<ServerLoggerFactory>().SetFactory(loggerFactory);
53+
54+
// Also add the ExtensionAssemblyManager so it will be available for the rest of the composition.
55+
exportProvider.GetExportedValue<ExtensionAssemblyManagerMefProvider>().SetMefExtensionAssemblyManager(extensionManager);
56+
57+
return exportProvider;
58+
}
4259

60+
private static async Task<IExportProviderFactory> GetCompositionConfigurationAsync(
61+
ImmutableArray<string> assemblyPaths,
62+
IAssemblyLoader assemblyLoader,
63+
string? cacheDirectory,
64+
ILogger logger)
65+
{
4366
// Create a MEF resolver that can resolve assemblies in the extension contexts.
4467
var resolver = new Resolver(assemblyLoader);
4568

69+
string? compositionCacheFile = cacheDirectory is not null
70+
? GetCompositionCacheFilePath(cacheDirectory, assemblyPaths)
71+
: null;
72+
73+
// Try to load a cached composition.
74+
try
75+
{
76+
if (compositionCacheFile is not null && File.Exists(compositionCacheFile))
77+
{
78+
logger.LogTrace($"Loading cached MEF catalog: {compositionCacheFile}");
79+
80+
CachedComposition cachedComposition = new();
81+
using FileStream cacheStream = new(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
82+
return await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, resolver);
83+
}
84+
}
85+
catch (Exception ex)
86+
{
87+
// Log the error, and move on to recover by recreating the MEF composition.
88+
logger.LogError($"Loading cached MEF composition failed: {ex}");
89+
}
90+
91+
logger.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}.");
92+
4693
var discovery = PartDiscovery.Combine(
4794
resolver,
4895
new AttributedPartDiscovery(resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition)
4996
new AttributedPartDiscoveryV1(resolver));
5097

51-
// TODO - we should likely cache the catalog so we don't have to rebuild it every time.
5298
var catalog = ComposableCatalog.Create(resolver)
5399
.AddParts(await discovery.CreatePartsAsync(assemblyPaths))
54100
.WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import
@@ -59,20 +105,52 @@ public static async Task<ExportProvider> CreateExportProviderAsync(
59105
// Verify we only have expected errors.
60106
ThrowOnUnexpectedErrors(config, catalog, logger);
61107

62-
// Prepare an ExportProvider factory based on this graph.
63-
var exportProviderFactory = config.CreateExportProviderFactory();
108+
// Try to cache the composition.
109+
if (compositionCacheFile is not null)
110+
{
111+
if (Path.GetDirectoryName(compositionCacheFile) is string directory)
112+
{
113+
Directory.CreateDirectory(directory);
114+
}
64115

65-
// Create an export provider, which represents a unique container of values.
66-
// You can create as many of these as you want, but typically an app needs just one.
67-
var exportProvider = exportProviderFactory.CreateExportProvider();
116+
CachedComposition cachedComposition = new();
117+
var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
118+
using (FileStream cacheStream = new(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true))
119+
{
120+
await cachedComposition.SaveAsync(config, cacheStream);
121+
}
68122

69-
// Immediately set the logger factory, so that way it'll be available for the rest of the composition
70-
exportProvider.GetExportedValue<ServerLoggerFactory>().SetFactory(loggerFactory);
123+
File.Move(tempFilePath, compositionCacheFile, overwrite: true);
124+
}
71125

72-
// Also add the ExtensionAssemblyManager so it will be available for the rest of the composition.
73-
exportProvider.GetExportedValue<ExtensionAssemblyManagerMefProvider>().SetMefExtensionAssemblyManager(extensionManager);
126+
// Prepare an ExportProvider factory based on this graph.
127+
return config.CreateExportProviderFactory();
128+
}
74129

75-
return exportProvider;
130+
private static string GetCompositionCacheFilePath(string cacheDirectory, ImmutableArray<string> assemblyPaths)
131+
{
132+
// Include the .NET runtime version in the cache path so that running on a newer
133+
// runtime causes the cache to be rebuilt.
134+
var cacheSubdirectory = $".NET {Environment.Version.Major}";
135+
return Path.Combine(cacheDirectory, cacheSubdirectory, $"c#-languageserver.{ComputeAssemblyHash(assemblyPaths)}.mef-composition");
136+
137+
static string ComputeAssemblyHash(ImmutableArray<string> assemblyPaths)
138+
{
139+
var assemblies = new StringBuilder();
140+
foreach (var assemblyPath in assemblyPaths)
141+
{
142+
// Include assembly path in the hash so that changes to the set of included
143+
// assemblies cause the composition to be rebuilt.
144+
assemblies.Append(assemblyPath);
145+
// Include the last write time in the hash so that newer assemblies written
146+
// to the same location cause the composition to be rebuilt.
147+
assemblies.Append(File.GetLastWriteTimeUtc(assemblyPath).ToString("F"));
148+
}
149+
150+
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(assemblies.ToString()));
151+
// Convert to filename safe base64 string.
152+
return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').TrimEnd('=');
153+
}
76154
}
77155

78156
private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog, ILogger logger)

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation
8686
var assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory);
8787
var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, loggerFactory);
8888

89-
using var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, loggerFactory);
89+
var cacheDirectory = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location)!, "cache");
90+
91+
using var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, cacheDirectory, loggerFactory);
9092

9193
// LSP server doesn't have the pieces yet to support 'balanced' mode for source-generators. Hardcode us to
9294
// 'automatic' for now.

0 commit comments

Comments
 (0)