Skip to content

Commit 3d59cef

Browse files
Jeffrey Tankusmour
authored andcommitted
Support reuse lldb-dap across VSCode debug sessions
Summary: This diff adds a new mode in lldb-dap to support reusing lldb-dap across debug sessions which can improve incremental debug session launch/attach time. The reuse lldb-dap design is implemented by two parts: * Changes in VSCode fb-lldb extension to decide the reuse policy. Like how long to keep lldb-dap alive, when to reuse, when to spawn a separate instance (like support parallel debug sessions). D59239749 and D58840599 * A new `--keep-alive <timeout_in_ms>` option is added to lldb-dap to keep lldb-dap alive. This diff. To support keep alive, a new `waitDataAvailableOrTimeout()` function is added in I/O code of lldb-dap to wait for next packet or specified timeout. And the DAP loop will wait for next packet with timeout after disconnection instead of exiting. Also, the global DAP object is reset to initial state. Test framework is updated to support keep-alive mode and a new test case is added to demonstrate reuse across debug sessions. Since this feature depends on two parts collaboration, the best way is first landing it as an internal patch for dogfooding (controlled by VSCode fb-lldb options) so that we can surface issues out of it. Once we feel confident about the feature, I will bring it to upstream. Test Plan: Test framework is updated to support keep-alive mode and a new test case is added to demonstrate reuse across debug sessions. Measured performance improvement for incremental debug sessions: ``` adfinder built with ads focused mode: * changed from 35s => 6s //systemhealth/parrot:parrot_debug built with mode/lldb: * 13GB of debug info and 199 total modules (3 modules hydrated with symbol on-demand) * changed from 55s => 5s //fblearner/feature_store/cpp:server built with mode/lldb: * 58GB of debug info and 889 total modules (2 modules hydrated with symbol on-demand) * changed from 110s => 11s ``` Reviewers: freik, gclayton, toyang, wanyi, #lldb_team Reviewed By: freik Subscribers: freik, #lldb_team Differential Revision: https://phabricator.intern.facebook.com/D59401005
1 parent df15e8d commit 3d59cef

File tree

15 files changed

+238
-14
lines changed

15 files changed

+238
-14
lines changed

lldb/include/lldb/API/SBDebugger.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ class LLDB_API SBDebugger {
385385
const char *value,
386386
const char *debugger_instance_name);
387387

388+
static lldb::SBError
389+
ClearInternalVariable(const char *var_name,
390+
const char *debugger_instance_name);
391+
388392
static lldb::SBStringList
389393
GetInternalVariableValue(const char *var_name,
390394
const char *debugger_instance_name);

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,33 +146,43 @@ def __init__(
146146
send: BinaryIO,
147147
init_commands: list[str],
148148
log_file: Optional[TextIO] = None,
149+
keepAlive=False,
149150
):
150151
# For debugging test failures, try setting `trace_file = sys.stderr`.
151152
self.trace_file: Optional[TextIO] = None
152153
self.log_file = log_file
154+
self.trace_file = None
153155
self.send = send
154156
self.recv = recv
155157
self.recv_packets: list[Optional[ProtocolMessage]] = []
156158
self.recv_condition = threading.Condition()
157159
self.recv_thread = threading.Thread(target=self._read_packet_thread)
160+
self.sequence = 1
161+
self.recv_thread.start()
162+
self.output_condition = threading.Condition()
163+
self.reset(init_commands, keepAlive)
164+
165+
# This will be called to re-initialize DebugCommunication object during
166+
# reusing lldb-dap.
167+
def reset(self, init_commands, keepAlive=False):
168+
self.trace_file = None
169+
self.recv_packets = []
158170
self.process_event_body = None
159171
self.exit_status: Optional[int] = None
160172
self.capabilities: dict[str, Any] = {}
161173
self.progress_events: list[Event] = []
162174
self.thread_events_body = []
163175
self.reverse_requests = []
164-
self.sequence = 1
165176
self.threads = None
166177
self.thread_stop_reasons = {}
167-
self.recv_thread.start()
168-
self.output_condition = threading.Condition()
169178
self.output: dict[str, list[str]] = {}
170179
self.configuration_done_sent = False
171180
self.initialized = False
172181
self.frame_scopes = {}
173182
self.init_commands = init_commands
174183
self.resolved_breakpoints = {}
175184
self.initialized_event = None
185+
self.keepAlive = keepAlive
176186

177187
@classmethod
178188
def encode_content(cls, s: str) -> bytes:
@@ -321,7 +331,9 @@ def _handle_recv_packet(self, packet: Optional[ProtocolMessage]) -> bool:
321331

322332
elif packet_type == "response":
323333
if packet["command"] == "disconnect":
324-
keepGoing = False
334+
# Disconnect response should exit the packet read loop unless
335+
# client wants to keep adapter alive for reusing.
336+
keepGoing = self.keepAlive
325337
self._enqueue_recv_packet(packet)
326338
return keepGoing
327339

@@ -1322,12 +1334,17 @@ def __init__(
13221334
init_commands: list[str] = [],
13231335
log_file: Optional[TextIO] = None,
13241336
env: Optional[dict[str, str]] = None,
1337+
keepAliveTimeout: Optional[int] = None,
13251338
):
13261339
self.process = None
13271340
self.connection = None
13281341
if executable is not None:
13291342
process, connection = DebugAdapterServer.launch(
1330-
executable=executable, connection=connection, env=env, log_file=log_file
1343+
executable=executable,
1344+
connection=connection,
1345+
env=env,
1346+
log_file=log_file,
1347+
keepAliveTimeout=keepAliveTimeout,
13311348
)
13321349
self.process = process
13331350
self.connection = connection
@@ -1349,9 +1366,22 @@ def __init__(
13491366
self.connection = connection
13501367
else:
13511368
DebugCommunication.__init__(
1352-
self, self.process.stdout, self.process.stdin, init_commands, log_file
1369+
self,
1370+
self.process.stdout,
1371+
self.process.stdin,
1372+
init_commands,
1373+
log_file,
1374+
keepAlive=(keepAliveTimeout is not None),
13531375
)
13541376

1377+
@staticmethod
1378+
def get_args(executable, keepAliveTimeout=None):
1379+
return (
1380+
[executable]
1381+
if keepAliveTimeout is None
1382+
else [executable, "--keep-alive", str(keepAliveTimeout)]
1383+
)
1384+
13551385
@classmethod
13561386
def launch(
13571387
cls,
@@ -1360,14 +1390,15 @@ def launch(
13601390
env: Optional[dict[str, str]] = None,
13611391
log_file: Optional[TextIO] = None,
13621392
connection: Optional[str] = None,
1393+
keepAliveTimeout: Optional[int] = None,
13631394
) -> tuple[subprocess.Popen, Optional[str]]:
13641395
adapter_env = os.environ.copy()
13651396
if env is not None:
13661397
adapter_env.update(env)
13671398

13681399
if log_file:
13691400
adapter_env["LLDBDAP_LOG"] = log_file
1370-
args = [executable]
1401+
args = cls.get_args(executable, keepAliveTimeout)
13711402

13721403
if connection is not None:
13731404
args.append("--connection")
@@ -1748,6 +1779,14 @@ def main():
17481779
),
17491780
)
17501781

1782+
parser.add_option(
1783+
"--keep-alive",
1784+
type="int",
1785+
dest="keepAliveTimeout",
1786+
help="The number of milliseconds to keep lldb-dap alive after client disconnection for reusing. Zero or negative value will not keep lldb-dap alive.",
1787+
default=None,
1788+
)
1789+
17511790
(options, args) = parser.parse_args(sys.argv[1:])
17521791

17531792
if options.vscode_path is None and options.connection is None:
@@ -1758,7 +1797,9 @@ def main():
17581797
)
17591798
return
17601799
dbg = DebugAdapterServer(
1761-
executable=options.vscode_path, connection=options.connection
1800+
executable=options.vscode_path,
1801+
connection=options.connection,
1802+
keepAliveTimeout=options.keepAliveTimeout,
17621803
)
17631804
if options.debug:
17641805
raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,50 @@ class DAPTestCaseBase(TestBase):
1515
# timeout by a factor of 10 if ASAN is enabled.
1616
DEFAULT_TIMEOUT = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1)
1717
NO_DEBUG_INFO_TESTCASE = True
18+
# Caches last dap server for reusing.
19+
alive_dap_server = None
1820

1921
def create_debug_adapter(
2022
self,
2123
lldbDAPEnv: Optional[dict[str, str]] = None,
24+
keepAliveTimeout: Optional[int] = None,
25+
reuseDapServer: bool = False,
2226
connection: Optional[str] = None,
2327
):
2428
"""Create the Visual Studio Code debug adapter"""
2529
self.assertTrue(
2630
is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable"
2731
)
2832
log_file_path = self.getBuildArtifact("dap.txt")
33+
34+
if reuseDapServer:
35+
self.assertIsNotNone(
36+
DAPTestCaseBase.alive_dap_server, "No alive dap server found."
37+
)
38+
self.dap_server = DAPTestCaseBase.alive_dap_server
39+
self.dap_server.reset(self.setUpCommands(), keepAlive=True)
40+
return
41+
2942
self.dap_server = dap_server.DebugAdapterServer(
3043
executable=self.lldbDAPExec,
3144
connection=connection,
3245
init_commands=self.setUpCommands(),
3346
log_file=log_file_path,
3447
env=lldbDAPEnv,
48+
keepAliveTimeout=keepAliveTimeout,
3549
)
50+
if keepAliveTimeout is not None:
51+
# Keep alive for future reuse
52+
DAPTestCaseBase.alive_dap_server = self.dap_server
3653

3754
def build_and_create_debug_adapter(
3855
self,
3956
lldbDAPEnv: Optional[dict[str, str]] = None,
57+
keepAliveTimeout: Optional[int] = None,
4058
dictionary: Optional[dict] = None,
4159
):
4260
self.build(dictionary=dictionary)
43-
self.create_debug_adapter(lldbDAPEnv)
61+
self.create_debug_adapter(lldbDAPEnv, keepAliveTimeout)
4462

4563
def build_and_create_debug_adapter_for_attach(self):
4664
"""Variant of build_and_create_debug_adapter that builds a uniquely
@@ -483,12 +501,13 @@ def build_and_launch(
483501
program,
484502
*,
485503
lldbDAPEnv: Optional[dict[str, str]] = None,
504+
keepAliveTimeout: Optional[int] = None,
486505
**kwargs,
487506
):
488507
"""Build the default Makefile target, create the DAP debug adapter,
489508
and launch the process.
490509
"""
491-
self.build_and_create_debug_adapter(lldbDAPEnv)
510+
self.build_and_create_debug_adapter(lldbDAPEnv, keepAliveTimeout)
492511
self.assertTrue(os.path.exists(program), "executable must exist")
493512

494513
return self.launch(program, **kwargs)

lldb/source/API/SBDebugger.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,28 @@ const char *SBDebugger::GetInstanceName() {
13071307
return ConstString(m_opaque_sp->GetInstanceName()).AsCString();
13081308
}
13091309

1310+
SBError SBDebugger::ClearInternalVariable(const char *var_name,
1311+
const char *debugger_instance_name) {
1312+
LLDB_INSTRUMENT_VA(var_name, debugger_instance_name);
1313+
1314+
SBError sb_error;
1315+
DebuggerSP debugger_sp(
1316+
Debugger::FindDebuggerWithInstanceName(debugger_instance_name));
1317+
Status error;
1318+
if (debugger_sp) {
1319+
ExecutionContext exe_ctx(
1320+
debugger_sp->GetCommandInterpreter().GetExecutionContext());
1321+
error = debugger_sp->SetPropertyValue(&exe_ctx, eVarSetOperationClear,
1322+
var_name, llvm::StringRef());
1323+
} else {
1324+
error.FromErrorStringWithFormat("invalid debugger instance name '%s'",
1325+
debugger_instance_name);
1326+
}
1327+
if (error.Fail())
1328+
sb_error.SetError(std::move(error));
1329+
return sb_error;
1330+
}
1331+
13101332
SBError SBDebugger::SetInternalVariable(const char *var_name, const char *value,
13111333
const char *debugger_instance_name) {
13121334
LLDB_INSTRUMENT_VA(var_name, value, debugger_instance_name);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CXX_SOURCES := main.cpp
2+
3+
include Makefile.rules
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
Test reuse lldb-dap adapter across debug sessions.
3+
"""
4+
5+
import dap_server
6+
from lldbsuite.test.decorators import *
7+
from lldbsuite.test.lldbtest import *
8+
from lldbsuite.test import lldbutil
9+
import lldbdap_testcase
10+
import time
11+
import os
12+
13+
14+
class TestDAP_reuseAdpater(lldbdap_testcase.DAPTestCaseBase):
15+
@skipIfWindows
16+
def test_basic_reuse(self):
17+
"""
18+
Test reuse lldb-dap works across debug sessions.
19+
"""
20+
program = self.getBuildArtifact("a.out")
21+
22+
# Keep lldb-dap alive for 10 minutes.
23+
dapKeepAliveTimeInMS = 10 * 1000 * 60
24+
self.build_and_launch(program, disconnectAutomatically=False, keepAliveTimeout=dapKeepAliveTimeInMS)
25+
26+
source = "main.cpp"
27+
breakpoint1_line = line_number(source, "// breakpoint 1")
28+
breakpoint_ids = self.set_source_breakpoints(source, [breakpoint1_line])
29+
self.continue_to_breakpoints(breakpoint_ids)
30+
self.dap_server.request_disconnect()
31+
32+
# Second debug session by reusing lldb-dap.
33+
self.create_debug_adapter(reuseDapServer=True)
34+
self.launch(program)
35+
36+
breakpoint2_line = line_number(source, "// breakpoint 2")
37+
breakpoint_ids = self.set_source_breakpoints(source, [breakpoint2_line])
38+
self.continue_to_breakpoints(breakpoint_ids)
39+
self.dap_server.request_disconnect()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <unistd.h>
4+
5+
int main(int argc, char const *argv[], char const *envp[]) {
6+
for (int i = 0; i < argc; ++i)
7+
printf("arg[%i] = \"%s\"\n", i, argv[i]);
8+
for (int i = 0; envp[i]; ++i)
9+
printf("env[%i] = \"%s\"\n", i, envp[i]);
10+
char *cwd = getcwd(NULL, 0);
11+
printf("cwd = \"%s\"\n", cwd); // breakpoint 1
12+
free(cwd);
13+
cwd = NULL;
14+
return 0; // breakpoint 2
15+
}

0 commit comments

Comments
 (0)