Skip to content

Commit 7997469

Browse files
authored
Cache the MEF composition in the Roslyn LSP. (#76276)
To improve startup time of the LSP we can cache the MEF composition and load it from file instead of building up a new composition. The cache is invalidated when: 1. The location of the Microsoft.CodeAnalysis.LanguageServer changes (such as installing an updated C# extension or `dotnet.server.path` is updated). 2. Changing major .NET runtime version. 3. The set of assembly paths that make up the composition changes. 4. The last write time of any assembly that is part of the composition changes. Resolves #68568
2 parents a13d7dc + b768288 commit 7997469

File tree

8 files changed

+232
-39
lines changed

8 files changed

+232
-39
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 Xunit.Abstractions;
6+
7+
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests
8+
{
9+
public sealed class ExportProviderBuilderTests(ITestOutputHelper testOutputHelper)
10+
: AbstractLanguageServerHostTests(testOutputHelper)
11+
{
12+
[Fact]
13+
public async Task MefCompositionIsCached()
14+
{
15+
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);
16+
17+
await AssertCacheWriteWasAttemptedAsync();
18+
19+
AssertCachedCompositionCountEquals(expectedCount: 1);
20+
}
21+
22+
[Fact]
23+
public async Task MefCompositionIsReused()
24+
{
25+
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);
26+
27+
await AssertCacheWriteWasAttemptedAsync();
28+
29+
// Second test server with the same set of assemblies.
30+
await using var testServer2 = await CreateLanguageServerAsync(includeDevKitComponents: false);
31+
32+
AssertNoCacheWriteWasAttempted();
33+
34+
AssertCachedCompositionCountEquals(expectedCount: 1);
35+
}
36+
37+
[Fact]
38+
public async Task MultipleMefCompositionsAreCached()
39+
{
40+
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);
41+
42+
await AssertCacheWriteWasAttemptedAsync();
43+
44+
// Second test server with a different set of assemblies.
45+
await using var testServer2 = await CreateLanguageServerAsync(includeDevKitComponents: true);
46+
47+
await AssertCacheWriteWasAttemptedAsync();
48+
49+
AssertCachedCompositionCountEquals(expectedCount: 2);
50+
}
51+
52+
private async Task AssertCacheWriteWasAttemptedAsync()
53+
{
54+
var cacheWriteTask = ExportProviderBuilder.TestAccessor.GetCacheWriteTask();
55+
Assert.NotNull(cacheWriteTask);
56+
57+
await cacheWriteTask;
58+
}
59+
60+
private void AssertNoCacheWriteWasAttempted()
61+
{
62+
var cacheWriteTask2 = ExportProviderBuilder.TestAccessor.GetCacheWriteTask();
63+
Assert.Null(cacheWriteTask2);
64+
}
65+
66+
private void AssertCachedCompositionCountEquals(int expectedCount)
67+
{
68+
var mefCompositions = Directory.EnumerateFiles(MefCacheDirectory.Path, "*.mef-composition", SearchOption.AllDirectories);
69+
70+
Assert.Equal(expectedCount, mefCompositions.Count());
71+
}
72+
}
73+
}

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
1616

17-
public class LspFileChangeWatcherTests : AbstractLanguageServerHostTests
17+
public class LspFileChangeWatcherTests(ITestOutputHelper testOutputHelper)
18+
: AbstractLanguageServerHostTests(testOutputHelper)
1819
{
1920
private readonly ClientCapabilities _clientCapabilitiesWithFileWatcherSupport = new ClientCapabilities
2021
{
@@ -24,22 +25,18 @@ public class LspFileChangeWatcherTests : AbstractLanguageServerHostTests
2425
}
2526
};
2627

27-
public LspFileChangeWatcherTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
28-
{
29-
}
30-
3128
[Fact]
3229
public async Task LspFileWatcherNotSupportedWithoutClientSupport()
3330
{
34-
await using var testLspServer = await TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger);
31+
await using var testLspServer = await TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, MefCacheDirectory.Path);
3532

3633
Assert.False(LspFileChangeWatcher.SupportsLanguageServerHost(testLspServer.LanguageServerHost));
3734
}
3835

3936
[Fact]
4037
public async Task LspFileWatcherSupportedWithClientSupport()
4138
{
42-
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger);
39+
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger, MefCacheDirectory.Path);
4340

4441
Assert.True(LspFileChangeWatcher.SupportsLanguageServerHost(testLspServer.LanguageServerHost));
4542
}
@@ -49,16 +46,15 @@ public async Task CreatingDirectoryWatchRequestsDirectoryWatch()
4946
{
5047
AsynchronousOperationListenerProvider.Enable(enable: true);
5148

52-
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger);
49+
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger, MefCacheDirectory.Path);
5350
var lspFileChangeWatcher = new LspFileChangeWatcher(
5451
testLspServer.LanguageServerHost,
5552
testLspServer.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>());
5653

5754
var dynamicCapabilitiesRpcTarget = new DynamicCapabilitiesRpcTarget();
5855
testLspServer.AddClientLocalRpcTarget(dynamicCapabilitiesRpcTarget);
5956

60-
using var tempRoot = new TempRoot();
61-
var tempDirectory = tempRoot.CreateDirectory();
57+
var tempDirectory = TempRoot.CreateDirectory();
6258

6359
// Try creating a context and ensure we created the registration
6460
var context = lspFileChangeWatcher.CreateContext([new ProjectSystem.WatchedDirectory(tempDirectory.Path, extensionFilters: [])]);
@@ -80,16 +76,15 @@ public async Task CreatingFileWatchRequestsFileWatch()
8076
{
8177
AsynchronousOperationListenerProvider.Enable(enable: true);
8278

83-
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger);
79+
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger, MefCacheDirectory.Path);
8480
var lspFileChangeWatcher = new LspFileChangeWatcher(
8581
testLspServer.LanguageServerHost,
8682
testLspServer.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>());
8783

8884
var dynamicCapabilitiesRpcTarget = new DynamicCapabilitiesRpcTarget();
8985
testLspServer.AddClientLocalRpcTarget(dynamicCapabilitiesRpcTarget);
9086

91-
using var tempRoot = new TempRoot();
92-
var tempDirectory = tempRoot.CreateDirectory();
87+
var tempDirectory = TempRoot.CreateDirectory();
9388

9489
// Try creating a single file watch and ensure we created the registration
9590
var context = lspFileChangeWatcher.CreateContext([]);

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@
99

1010
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
1111

12-
public sealed class TelemetryReporterTests : AbstractLanguageServerHostTests
12+
public sealed class TelemetryReporterTests(ITestOutputHelper testOutputHelper)
13+
: AbstractLanguageServerHostTests(testOutputHelper)
1314
{
14-
public TelemetryReporterTests(ITestOutputHelper testOutputHelper)
15-
: base(testOutputHelper)
16-
{
17-
}
18-
1915
private async Task<ITelemetryReporter> CreateReporterAsync()
2016
{
21-
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(TestOutputLogger.Factory, includeDevKitComponents: true, out var _, out var _);
17+
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
18+
TestOutputLogger.Factory,
19+
includeDevKitComponents: true,
20+
MefCacheDirectory.Path,
21+
out var _,
22+
out var _);
2223

2324
// VS Telemetry requires this environment variable to be set.
2425
Environment.SetEnvironmentVariable("CommonPropertyBagPath", Path.GetTempFileName());

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

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

55
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
6+
using Microsoft.CodeAnalysis.Test.Utilities;
67
using Microsoft.VisualStudio.Composition;
78
using Nerdbank.Streams;
89
using Roslyn.LanguageServer.Protocol;
@@ -11,18 +12,27 @@
1112

1213
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
1314

14-
public abstract class AbstractLanguageServerHostTests
15+
public abstract class AbstractLanguageServerHostTests : IDisposable
1516
{
1617
protected TestOutputLogger TestOutputLogger { get; }
18+
protected TempRoot TempRoot { get; }
19+
protected TempDirectory MefCacheDirectory { get; }
1720

1821
protected AbstractLanguageServerHostTests(ITestOutputHelper testOutputHelper)
1922
{
2023
TestOutputLogger = new TestOutputLogger(testOutputHelper);
24+
TempRoot = new();
25+
MefCacheDirectory = TempRoot.CreateDirectory();
2126
}
2227

2328
protected Task<TestLspServer> CreateLanguageServerAsync(bool includeDevKitComponents = true)
2429
{
25-
return TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, includeDevKitComponents);
30+
return TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, MefCacheDirectory.Path, includeDevKitComponents);
31+
}
32+
33+
public void Dispose()
34+
{
35+
TempRoot.Dispose();
2636
}
2737

2838
protected sealed class TestLspServer : IAsyncDisposable
@@ -32,10 +42,10 @@ protected sealed class TestLspServer : IAsyncDisposable
3242

3343
private ServerCapabilities? _serverCapabilities;
3444

35-
internal static async Task<TestLspServer> CreateAsync(ClientCapabilities clientCapabilities, TestOutputLogger logger, bool includeDevKitComponents = true)
45+
internal static async Task<TestLspServer> CreateAsync(ClientCapabilities clientCapabilities, TestOutputLogger logger, string cacheDirectory, bool includeDevKitComponents = true)
3646
{
3747
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
38-
logger.Factory, includeDevKitComponents, out var _, out var assemblyLoader);
48+
logger.Factory, includeDevKitComponents, cacheDirectory, out var _, out var assemblyLoader);
3949
var testLspServer = new TestLspServer(exportProvider, logger, assemblyLoader);
4050
var initializeResponse = await testLspServer.ExecuteRequestAsync<InitializeParams, InitializeResult>(Methods.InitializeName, new InitializeParams { Capabilities = clientCapabilities }, CancellationToken.None);
4151
Assert.NotNull(initializeResponse?.Capabilities);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ private static string GetDevKitExtensionPath()
2323
public static Task<ExportProvider> CreateExportProviderAsync(
2424
ILoggerFactory loggerFactory,
2525
bool includeDevKitComponents,
26+
string cacheDirectory,
2627
out ServerConfiguration serverConfiguration,
2728
out IAssemblyLoader assemblyLoader)
2829
{
@@ -39,6 +40,6 @@ public static Task<ExportProvider> CreateExportProviderAsync(
3940
ExtensionLogDirectory: string.Empty);
4041
var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory);
4142
assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory);
42-
return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, loggerFactory);
43+
return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory);
4344
}
4445
}

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
using Microsoft.CodeAnalysis.Remote.ProjectSystem;
88
using Microsoft.Extensions.Logging;
99
using Microsoft.VisualStudio.Shell.ServiceBroker;
10+
using Xunit.Abstractions;
1011

1112
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
1213

13-
public class WorkspaceProjectFactoryServiceTests
14+
public class WorkspaceProjectFactoryServiceTests(ITestOutputHelper testOutputHelper)
15+
: AbstractLanguageServerHostTests(testOutputHelper)
1416
{
1517
[Fact]
1618
public async Task CreateProjectAndBatch()
1719
{
1820
var loggerFactory = new LoggerFactory();
1921
using var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
20-
loggerFactory, includeDevKitComponents: false, out var serverConfiguration, out var _);
22+
loggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, out var serverConfiguration, out var _);
2123

2224
exportProvider.GetExportedValue<ServerConfigurationFactory>()
2325
.InitializeConfiguration(serverConfiguration);

0 commit comments

Comments
 (0)