-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[lldb-dap] Adding server mode support to lldb-dap VSCode extension. #128957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
@llvm/pr-subscribers-lldb Author: John Harrison (ashgti) ChangesThis adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first. Full diff: https://github.com/llvm/llvm-project/pull/128957.diff 3 Files Affected:
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index 31d808eda4c35..51f392e386b22 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -88,6 +88,12 @@
"additionalProperties": {
"type": "string"
}
+ },
+ "lldb-dap.serverMode": {
+ "scope": "resource",
+ "type": "boolean",
+ "description": "Run lldb-dap in server mode. When enabled, lldb-dap will start a background server that will be reused between debug sessions. This can improve launch performance and caching of debug symbols between debug sessions.",
+ "default": false
}
}
},
@@ -543,4 +549,4 @@
}
]
}
-}
+}
\ No newline at end of file
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 36107336ebc4d..89a76d7e6f40c 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -4,6 +4,8 @@ import * as vscode from "vscode";
import * as child_process from "child_process";
import * as fs from "node:fs/promises";
+const exec = util.promisify(child_process.execFile);
+
export async function isExecutable(path: string): Promise<Boolean> {
try {
await fs.access(path, fs.constants.X_OK);
@@ -16,7 +18,6 @@ export async function isExecutable(path: string): Promise<Boolean> {
async function findWithXcrun(executable: string): Promise<string | undefined> {
if (process.platform === "darwin") {
try {
- const exec = util.promisify(child_process.execFile);
let { stdout, stderr } = await exec("/usr/bin/xcrun", [
"-find",
executable,
@@ -24,7 +25,7 @@ async function findWithXcrun(executable: string): Promise<string | undefined> {
if (stdout) {
return stdout.toString().trimEnd();
}
- } catch (error) {}
+ } catch (error) { }
}
return undefined;
}
@@ -97,8 +98,15 @@ async function getDAPExecutable(
* depending on the session configuration.
*/
export class LLDBDapDescriptorFactory
- implements vscode.DebugAdapterDescriptorFactory
-{
+ implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable {
+ private server?: Promise<{ process: child_process.ChildProcess, host: string, port: number }>;
+
+ dispose() {
+ this.server?.then(({ process }) => {
+ process.kill();
+ });
+ }
+
async createDebugAdapterDescriptor(
session: vscode.DebugSession,
executable: vscode.DebugAdapterExecutable | undefined,
@@ -115,7 +123,18 @@ export class LLDBDapDescriptorFactory
}
const configEnvironment =
config.get<{ [key: string]: string }>("environment") || {};
- const dapPath = await getDAPExecutable(session);
+ const dapPath = (await getDAPExecutable(session)) ?? executable?.command;
+
+ if (!dapPath) {
+ LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
+ return undefined;
+ }
+
+ if (!(await isExecutable(dapPath))) {
+ LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
+ return;
+ }
+
const dbgOptions = {
env: {
...executable?.options?.env,
@@ -123,30 +142,45 @@ export class LLDBDapDescriptorFactory
...env,
},
};
- if (dapPath) {
- if (!(await isExecutable(dapPath))) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
- return undefined;
- }
- return new vscode.DebugAdapterExecutable(dapPath, [], dbgOptions);
- } else if (executable) {
- if (!(await isExecutable(executable.command))) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(executable.command);
- return undefined;
- }
- return new vscode.DebugAdapterExecutable(
- executable.command,
- executable.args,
- dbgOptions,
- );
+ const dbgArgs = executable?.args ?? [];
+
+ const serverMode = config.get<boolean>('serverMode', false);
+ if (serverMode) {
+ const { host, port } = await this.startServer(dapPath, dbgArgs, dbgOptions);
+ return new vscode.DebugAdapterServer(port, host);
}
- return undefined;
+
+ return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
+ }
+
+ startServer(dapPath: string, args: string[], options: child_process.CommonSpawnOptions): Promise<{ host: string, port: number }> {
+ if (this.server) return this.server;
+
+ this.server = new Promise(resolve => {
+ args.push(
+ '--connection',
+ 'connect://localhost:0'
+ );
+ const server = child_process.spawn(dapPath, args, options);
+ server.stdout!.setEncoding('utf8').on('data', (data: string) => {
+ const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data);
+ if (connection) {
+ const host = connection[1];
+ const port = Number(connection[2]);
+ resolve({ process: server, host, port });
+ }
+ });
+ server.on('exit', () => {
+ this.server = undefined;
+ })
+ });
+ return this.server;
}
/**
* Shows a message box when the debug adapter's path is not found
*/
- static async showLLDBDapNotFoundMessage(path: string) {
+ static async showLLDBDapNotFoundMessage(path?: string) {
const openSettingsAction = "Open Settings";
const callbackValue = await vscode.window.showErrorMessage(
`Debug adapter path: ${path} is not a valid file`,
diff --git a/lldb/tools/lldb-dap/src-ts/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index 71fd48298f8f5..a07bcdebcb68b 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,5 +1,3 @@
-import * as path from "path";
-import * as util from "util";
import * as vscode from "vscode";
import {
@@ -15,13 +13,14 @@ import { DisposableContext } from "./disposable-context";
export class LLDBDapExtension extends DisposableContext {
constructor() {
super();
+ const factory = new LLDBDapDescriptorFactory();
+ this.pushSubscription(factory);
this.pushSubscription(
vscode.debug.registerDebugAdapterDescriptorFactory(
"lldb-dap",
- new LLDBDapDescriptorFactory(),
- ),
+ factory,
+ )
);
-
this.pushSubscription(
vscode.workspace.onDidChangeConfiguration(async (event) => {
if (event.affectsConfiguration("lldb-dap.executable-path")) {
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Random question: what happens when the server crashes? Do you get a popup in the UI telling you?
lldb/tools/lldb-dap/package.json
Outdated
"lldb-dap.serverMode": { | ||
"scope": "resource", | ||
"type": "boolean", | ||
"description": "Run lldb-dap in server mode. When enabled, lldb-dap will start a background server that will be reused between debug sessions. This can improve launch performance and caching of debug symbols between debug sessions.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can make this a stronger statement. Maybe something like: "This allows caching of things like debug symbols and should improve launch performance".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the wording, let me know what you think.
…he handler to collect the connection parameter to only run once.
If the server is killed while the debug session is running then the debug session stops. This is the same behavior as today when running not in server mode. If the server crashes between debug sessions a new one will be spawned on demand. I don't currently alert the user if it shuts down or is killed and I don't have an idle timeout or anything. The server is running under the extension host process and VS Code uses one extension host process per window. That means the server will be around until the window is closed or the user quits VS Code. As far as server management goes, I don't have any explicit logic for that at the moment, but we could add it in the future. Some thoughts:
The main downside to the background server is the memory usage between debug sessions. For example, having the server attach to an iOS application then site between debug sessions results in a process consuming 1.3GB of memory for my machine. At the moment though, I am keeping this relatively simple. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -15,13 +13,14 @@ import { DisposableContext } from "./disposable-context"; | |||
export class LLDBDapExtension extends DisposableContext { | |||
constructor() { | |||
super(); | |||
const factory = new LLDBDapDescriptorFactory(); | |||
this.pushSubscription(factory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the factory itself implement vscode.Disposable
so if the extension is deactivated it will stop the server, if its running.
There is
This sounds like a nice to have and I think should be fairly straightforward to hook up.
That's what Xcode does with the Thanks for putting this list together.I think all the options you mentioned make sense.
Yup, we see the same thing with the lldb-rpc-server. I wonder how much calling
👍 |
I filed #129283 to track some of the improvements discussed in the comments so they're not lost. |
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your persistence to get this across the finish line! 🎉
A couple of (late) review comments below. Sorry for the late review :/
} | ||
|
||
startServer(dapPath: string, args: string[], options: child_process.CommonSpawnOptions): Promise<{ host: string, port: number }> { | ||
if (this.server) return this.server; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
afaict, this does not work correctly in combination with #126803. We might end up reusing the wrong lldb-dap binary
Not sure what this means for us, though... We also can't just kill the old server when we receive a different lldb-dap path, because we could have two separate debugging sessions running at the same time, each one using a different server binary. I guess the most robust solution would be to use a Map
to associate each lldb-dap path with its own Promise<{ process: child_process.ChildProcess, host: string, port: number }>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a simple solution could be to just ask the user if we can kill the running server or if they want to reuse the existing server. I don't expect that users will be changing lldb-dap paths often, I hope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussion continued in #129262 (comment)
'connect://localhost:0' | ||
); | ||
const server = child_process.spawn(dapPath, args, options); | ||
server.stdout!.setEncoding('utf8').once('data', (data: string) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this actually "race-free" (for lack of a better term)?
Looking at https://nodejs.org/api/stream.html#event-data I am not sure if we have a guarantee that the complete stdout content will be delivered in a single data
event or if the connection://...
string could be split across multiple data
events?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A single 'data' event usually corresponds to a single 'write' event which
llvm-project/lldb/tools/lldb-dap/lldb-dap.cpp
Line 298 in cef6dbb
llvm::outs() << "Listening for: " << address << "\n"; |
"scope": "resource", | ||
"type": "boolean", | ||
"markdownDescription": "Run lldb-dap in server mode.\n\nWhen enabled, lldb-dap will start a background server that will be reused between debug sessions. This allows caching of debug symbols between sessions and improves launch performance.", | ||
"default": false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have plans to enable this by default in the future? I think we should. In my experience, users rarely fiddle with the default settings, and to allow the widest possible user base to benefit from your great work, I think we should aim for making server-mode the default. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have more testing of the setting first. I know the unit tests are failing on Windows right now and I'm not sure why at the moment. Right now this is fairly basic, I have #129283 filed to look into improving server management operations in the extension code.
We may be able to enable it on macOS and linux sooner than Windows, we'd just want to do some more testing before making it the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 sounds good to me
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
…lvm#128957) This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions. For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.
This adds support for launching lldb-dap in server mode. The extension will start lldb-dap in server mode on-demand and retain the server until the VSCode window is closed (when the extension context is disposed). While running in server mode, launch performance for binaries is greatly improved by improving caching between debug sessions.
For example, on my local M1 Max laptop it takes ~5s to attach for the first attach to an iOS Simulator process and ~0.5s to attach each time after the first.