From 89bf5c6e291614ede2f184464919f9b1af5be950 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Tue, 25 Feb 2025 15:54:01 -0800 Subject: [PATCH] Delay server start if nothing opened to avoid blocking devkit --- src/activateRoslyn.ts | 114 ++++++++++++++++++++++++++++++++---------- src/main.ts | 2 + 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/activateRoslyn.ts b/src/activateRoslyn.ts index 7742dd93e..b08b3a8ae 100644 --- a/src/activateRoslyn.ts +++ b/src/activateRoslyn.ts @@ -39,24 +39,6 @@ export function activateRoslyn( const roslynLanguageServerEvents = new RoslynLanguageServerEvents(); context.subscriptions.push(roslynLanguageServerEvents); - // Activate Razor. Needs to be activated before Roslyn so commands are registered in the correct order. - // Otherwise, if Roslyn starts up first, they could execute commands that don't yet exist on Razor's end. - // - // Flow: - // Razor starts up and registers dynamic file info commands -> - // Roslyn starts up and registers Razor-specific didOpen/didClose/didChange commands and sends request to Razor - // for dynamic file info once project system is ready -> - // Razor sends didOpen commands to Roslyn for generated docs and responds to request with dynamic file info - const razorLanguageServerStartedPromise = activateRazorExtension( - context, - context.extension.extensionPath, - eventStream, - reporter, - csharpDevkitExtension, - platformInfo, - /* useOmnisharpServer */ false - ); - // Setup a listener for project initialization complete before we start the server. const projectInitializationCompletePromise = new Promise((resolve, _) => { roslynLanguageServerEvents.onServerStateChange(async (e) => { @@ -66,25 +48,22 @@ export function activateRoslyn( }); }); - // Start the server, but do not await the completion to avoid blocking activation. - const roslynLanguageServerStartedPromise = activateRoslynLanguageServer( + // Start the language server but do not wait for it to avoid blocking the extension activation. + const roslynLanguageServerStartedPromise = getLanguageServerPromise( context, platformInfo, optionStream, + eventStream, csharpChannel, reporter, - roslynLanguageServerEvents + csharpDevkitExtension, + roslynLanguageServerEvents, + getCoreClrDebugPromise ); - debugSessionTracker.initializeDebugSessionHandlers(context); - tryGetCSharpDevKitExtensionExports(csharpDevkitExtension, csharpChannel); - const coreClrDebugPromise = getCoreClrDebugPromise(roslynLanguageServerStartedPromise); - const languageServerExport = new RoslynLanguageServerExport(roslynLanguageServerStartedPromise); const exports: CSharpExtensionExports = { initializationFinished: async () => { - await coreClrDebugPromise; - await razorLanguageServerStartedPromise; await roslynLanguageServerStartedPromise; await projectInitializationCompletePromise; }, @@ -104,6 +83,87 @@ export function activateRoslyn( return exports; } +async function getLanguageServerPromise( + context: vscode.ExtensionContext, + platformInfo: PlatformInformation, + optionStream: Observable, + eventStream: EventStream, + csharpChannel: vscode.LogOutputChannel, + reporter: TelemetryReporter, + csharpDevkitExtension: vscode.Extension | undefined, + roslynLanguageServerEvents: RoslynLanguageServerEvents, + getCoreClrDebugPromise: (languageServerStarted: Promise) => Promise +): Promise { + // It is possible we're getting asked to activate due to dev kit activating to create a new project. + // We do not want to slow down devkit activation by taking up time on the extension host main thread. + // So we can avoid starting the server until there is a workspace or file we can work with. + const waitForWorkspaceOrFilePromise = new Promise((resolve, _) => { + // check if there is a workspace opened or a csharp file opened and resolve the promise. + // if neither are true, subscribe to vscode events to resolve the promise when a workspace or csharp file is opened. + if ( + vscode.workspace.workspaceFolders || + vscode.workspace.textDocuments.some((doc) => doc.languageId === 'csharp') + ) { + resolve(); + } else { + csharpChannel.info('Waiting for a workspace or C# file to be opened to activate the server.'); + // Subscribe to VS Code events to resolve the promise when a workspace or C# file is opened. + const onDidOpenTextDocument = vscode.workspace.onDidOpenTextDocument((doc) => { + if (doc.languageId === 'csharp') { + resolve(); + onDidOpenTextDocument.dispose(); + } + }); + + const onDidChangeWorkspaceFolders = vscode.workspace.onDidChangeWorkspaceFolders((event) => { + if (event.added.length > 0) { + resolve(); + onDidChangeWorkspaceFolders.dispose(); + } + }); + + context.subscriptions.push(onDidOpenTextDocument, onDidChangeWorkspaceFolders); + } + }); + + // Delay startup of language servers until there is something we can actually work with. + await waitForWorkspaceOrFilePromise; + + // Activate Razor. Needs to be activated before Roslyn so commands are registered in the correct order. + // Otherwise, if Roslyn starts up first, they could execute commands that don't yet exist on Razor's end. + // + // Flow: + // Razor starts up and registers dynamic file info commands -> + // Roslyn starts up and registers Razor-specific didOpen/didClose/didChange commands and sends request to Razor + // for dynamic file info once project system is ready -> + // Razor sends didOpen commands to Roslyn for generated docs and responds to request with dynamic file info + const razorLanguageServerStartedPromise = activateRazorExtension( + context, + context.extension.extensionPath, + eventStream, + reporter, + csharpDevkitExtension, + platformInfo, + /* useOmnisharpServer */ false + ); + + const roslynLanguageServerStartedPromise = activateRoslynLanguageServer( + context, + platformInfo, + optionStream, + csharpChannel, + reporter, + roslynLanguageServerEvents + ); + + const coreClrDebugPromise = getCoreClrDebugPromise(roslynLanguageServerStartedPromise); + debugSessionTracker.initializeDebugSessionHandlers(context); + tryGetCSharpDevKitExtensionExports(csharpDevkitExtension, csharpChannel); + + await Promise.all([razorLanguageServerStartedPromise, roslynLanguageServerStartedPromise, coreClrDebugPromise]); + return roslynLanguageServerStartedPromise; +} + /** * This method will try to get the CSharpDevKitExports through a thenable promise, * awaiting `activate` will cause this extension's activation to hang. diff --git a/src/main.ts b/src/main.ts index 71a40187f..6b8eafada 100644 --- a/src/main.ts +++ b/src/main.ts @@ -83,6 +83,8 @@ export async function activate( const installDependencies: IInstallDependencies = async (dependencies: AbsolutePathPackage[]) => downloadAndInstallPackages(dependencies, networkSettingsProvider, eventStream, isValidDownload); + // Fine to do this as part of activate as it is always a no-op in Roslyn mode (the debugger and Razor are shipped in-box) + // Only actually downloads files in O# mode or local development. const runtimeDependenciesExist = await installRuntimeDependencies( context.extension.packageJSON, context.extension.extensionPath,