Skip to content

Commit

Permalink
Cache the MEF composition in the Roslyn LSP. (#76276)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
JoeRobich authored Dec 10, 2024
2 parents a13d7dc + b768288 commit 7997469
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Xunit.Abstractions;

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests
{
public sealed class ExportProviderBuilderTests(ITestOutputHelper testOutputHelper)
: AbstractLanguageServerHostTests(testOutputHelper)
{
[Fact]
public async Task MefCompositionIsCached()
{
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);

await AssertCacheWriteWasAttemptedAsync();

AssertCachedCompositionCountEquals(expectedCount: 1);
}

[Fact]
public async Task MefCompositionIsReused()
{
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);

await AssertCacheWriteWasAttemptedAsync();

// Second test server with the same set of assemblies.
await using var testServer2 = await CreateLanguageServerAsync(includeDevKitComponents: false);

AssertNoCacheWriteWasAttempted();

AssertCachedCompositionCountEquals(expectedCount: 1);
}

[Fact]
public async Task MultipleMefCompositionsAreCached()
{
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);

await AssertCacheWriteWasAttemptedAsync();

// Second test server with a different set of assemblies.
await using var testServer2 = await CreateLanguageServerAsync(includeDevKitComponents: true);

await AssertCacheWriteWasAttemptedAsync();

AssertCachedCompositionCountEquals(expectedCount: 2);
}

private async Task AssertCacheWriteWasAttemptedAsync()
{
var cacheWriteTask = ExportProviderBuilder.TestAccessor.GetCacheWriteTask();
Assert.NotNull(cacheWriteTask);

await cacheWriteTask;
}

private void AssertNoCacheWriteWasAttempted()
{
var cacheWriteTask2 = ExportProviderBuilder.TestAccessor.GetCacheWriteTask();
Assert.Null(cacheWriteTask2);
}

private void AssertCachedCompositionCountEquals(int expectedCount)
{
var mefCompositions = Directory.EnumerateFiles(MefCacheDirectory.Path, "*.mef-composition", SearchOption.AllDirectories);

Assert.Equal(expectedCount, mefCompositions.Count());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;

public class LspFileChangeWatcherTests : AbstractLanguageServerHostTests
public class LspFileChangeWatcherTests(ITestOutputHelper testOutputHelper)
: AbstractLanguageServerHostTests(testOutputHelper)
{
private readonly ClientCapabilities _clientCapabilitiesWithFileWatcherSupport = new ClientCapabilities
{
Expand All @@ -24,22 +25,18 @@ public class LspFileChangeWatcherTests : AbstractLanguageServerHostTests
}
};

public LspFileChangeWatcherTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
{
}

[Fact]
public async Task LspFileWatcherNotSupportedWithoutClientSupport()
{
await using var testLspServer = await TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger);
await using var testLspServer = await TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, MefCacheDirectory.Path);

Assert.False(LspFileChangeWatcher.SupportsLanguageServerHost(testLspServer.LanguageServerHost));
}

[Fact]
public async Task LspFileWatcherSupportedWithClientSupport()
{
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger);
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger, MefCacheDirectory.Path);

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

await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger);
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger, MefCacheDirectory.Path);
var lspFileChangeWatcher = new LspFileChangeWatcher(
testLspServer.LanguageServerHost,
testLspServer.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>());

var dynamicCapabilitiesRpcTarget = new DynamicCapabilitiesRpcTarget();
testLspServer.AddClientLocalRpcTarget(dynamicCapabilitiesRpcTarget);

using var tempRoot = new TempRoot();
var tempDirectory = tempRoot.CreateDirectory();
var tempDirectory = TempRoot.CreateDirectory();

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

await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger);
await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger, MefCacheDirectory.Path);
var lspFileChangeWatcher = new LspFileChangeWatcher(
testLspServer.LanguageServerHost,
testLspServer.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>());

var dynamicCapabilitiesRpcTarget = new DynamicCapabilitiesRpcTarget();
testLspServer.AddClientLocalRpcTarget(dynamicCapabilitiesRpcTarget);

using var tempRoot = new TempRoot();
var tempDirectory = tempRoot.CreateDirectory();
var tempDirectory = TempRoot.CreateDirectory();

// Try creating a single file watch and ensure we created the registration
var context = lspFileChangeWatcher.CreateContext([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;

public sealed class TelemetryReporterTests : AbstractLanguageServerHostTests
public sealed class TelemetryReporterTests(ITestOutputHelper testOutputHelper)
: AbstractLanguageServerHostTests(testOutputHelper)
{
public TelemetryReporterTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}

private async Task<ITelemetryReporter> CreateReporterAsync()
{
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(TestOutputLogger.Factory, includeDevKitComponents: true, out var _, out var _);
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
TestOutputLogger.Factory,
includeDevKitComponents: true,
MefCacheDirectory.Path,
out var _,
out var _);

// VS Telemetry requires this environment variable to be set.
Environment.SetEnvironmentVariable("CommonPropertyBagPath", Path.GetTempFileName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.VisualStudio.Composition;
using Nerdbank.Streams;
using Roslyn.LanguageServer.Protocol;
Expand All @@ -11,18 +12,27 @@

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;

public abstract class AbstractLanguageServerHostTests
public abstract class AbstractLanguageServerHostTests : IDisposable
{
protected TestOutputLogger TestOutputLogger { get; }
protected TempRoot TempRoot { get; }
protected TempDirectory MefCacheDirectory { get; }

protected AbstractLanguageServerHostTests(ITestOutputHelper testOutputHelper)
{
TestOutputLogger = new TestOutputLogger(testOutputHelper);
TempRoot = new();
MefCacheDirectory = TempRoot.CreateDirectory();
}

protected Task<TestLspServer> CreateLanguageServerAsync(bool includeDevKitComponents = true)
{
return TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, includeDevKitComponents);
return TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, MefCacheDirectory.Path, includeDevKitComponents);
}

public void Dispose()
{
TempRoot.Dispose();
}

protected sealed class TestLspServer : IAsyncDisposable
Expand All @@ -32,10 +42,10 @@ protected sealed class TestLspServer : IAsyncDisposable

private ServerCapabilities? _serverCapabilities;

internal static async Task<TestLspServer> CreateAsync(ClientCapabilities clientCapabilities, TestOutputLogger logger, bool includeDevKitComponents = true)
internal static async Task<TestLspServer> CreateAsync(ClientCapabilities clientCapabilities, TestOutputLogger logger, string cacheDirectory, bool includeDevKitComponents = true)
{
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
logger.Factory, includeDevKitComponents, out var _, out var assemblyLoader);
logger.Factory, includeDevKitComponents, cacheDirectory, out var _, out var assemblyLoader);
var testLspServer = new TestLspServer(exportProvider, logger, assemblyLoader);
var initializeResponse = await testLspServer.ExecuteRequestAsync<InitializeParams, InitializeResult>(Methods.InitializeName, new InitializeParams { Capabilities = clientCapabilities }, CancellationToken.None);
Assert.NotNull(initializeResponse?.Capabilities);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private static string GetDevKitExtensionPath()
public static Task<ExportProvider> CreateExportProviderAsync(
ILoggerFactory loggerFactory,
bool includeDevKitComponents,
string cacheDirectory,
out ServerConfiguration serverConfiguration,
out IAssemblyLoader assemblyLoader)
{
Expand All @@ -39,6 +40,6 @@ public static Task<ExportProvider> CreateExportProviderAsync(
ExtensionLogDirectory: string.Empty);
var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory);
assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory);
return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, loggerFactory);
return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
using Microsoft.CodeAnalysis.Remote.ProjectSystem;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Shell.ServiceBroker;
using Xunit.Abstractions;

namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;

public class WorkspaceProjectFactoryServiceTests
public class WorkspaceProjectFactoryServiceTests(ITestOutputHelper testOutputHelper)
: AbstractLanguageServerHostTests(testOutputHelper)
{
[Fact]
public async Task CreateProjectAndBatch()
{
var loggerFactory = new LoggerFactory();
using var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(
loggerFactory, includeDevKitComponents: false, out var serverConfiguration, out var _);
loggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, out var serverConfiguration, out var _);

exportProvider.GetExportedValue<ServerConfigurationFactory>()
.InitializeConfiguration(serverConfiguration);
Expand Down
Loading

0 comments on commit 7997469

Please sign in to comment.