[Lldb-commits] [lldb] [lldb] Add a packet-test-delay setting for testing slow connections (PR #195440)

Raphael Isemann via lldb-commits lldb-commits at lists.llvm.org
Sat May 2 04:11:06 PDT 2026


https://github.com/Teemperor created https://github.com/llvm/llvm-project/pull/195440

Sending/receiving packages to/from a non-host devices adds latency to the gdb-remote communication that induces substantial slowdown into the debugging experience.

This patch adds an artificial delay before sending a package to simulate this latency without requiring an emulated/physical device.

The test checks only checks that there is a delay when this setting is activated, but not that there is no delay when the setting is not activated. The reason for this is that this negative test would either require a large delay (which further slows down the test suite) or be prone to accidential failures on overloaded test bots.

There is also the question whether we should have dynamic delays that depend on the number of transferred bytes.
>From talking to Felipe it seems the real bottleneck is really just the latency and not the transfer speed. From my own testing, it seems that a simple fixed array mostly simulated the remote debugging performance, and the delays I can see on my own machine seem to mostly resemble the delays we can see when debugging on a remote device.

This patch also required wrapping `SendPacketAndWaitForResponse` so we can sync this setting on a per-package basis for the GDBRemoteCommunication class. Without this, we could only set the delay before connecting and then never change afterwards which would make this feature less useful and writing a simple test impossible.

>From 40a3a8297850609df3fa1bdaf30a8abdefd939eb Mon Sep 17 00:00:00 2001
From: Raphael Isemann <rise at apple.com>
Date: Sat, 2 May 2026 11:44:26 +0100
Subject: [PATCH] [lldb] Add a packet-test-delay setting for testing slow
 connections

Sending/receiving packages to/from a non-host devices adds latency to
the gdb-remote communication that induces substantial slowdown into the
debugging experience.

This patch adds an artificial delay before sending a package to
simulate this latency without requiring an emulated/physical device.

The test checks only checks that there is a delay when this setting
is activated, but not that there is no delay when the setting is
not activated. The reason for this is that this negative test would
either require a large delay (which further slows down the test suite)
or be prone to accidential failures on overloaded test bots.

There is also the question whether we should have dynamic delays
that depend on the number of transferred bytes.
>From talking to Felipe it seems the real bottleneck is really just
the latency and not the transfer speed. From my own testing,
it seems that a simple fixed array mostly simulated the remote
debugging performance, and the delays I can see on my own machine
seem to mostly resemble the delays we can see when debugging on a
remote device.

This patch also required wrapping `SendPacketAndWaitForResponse` so we
can sync this setting on a per-package basis for the
GDBRemoteCommunication class. Without this, we could only set the
delay before connecting and then never change afterwards which would
make this feature less useful and writing a simple test impossible.
---
 .../gdb-remote/GDBRemoteCommunication.cpp     |  4 ++
 .../gdb-remote/GDBRemoteCommunication.h       |  5 ++
 .../Process/gdb-remote/ProcessGDBRemote.cpp   | 54 ++++++++++++-------
 .../Process/gdb-remote/ProcessGDBRemote.h     |  5 ++
 .../gdb-remote/ProcessGDBRemoteProperties.td  |  6 +++
 .../gdb_remote_client/TestPacketTestDelay.py  | 39 ++++++++++++++
 6 files changed, 93 insertions(+), 20 deletions(-)
 create mode 100644 lldb/test/API/functionalities/gdb_remote_client/TestPacketTestDelay.py

diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.cpp
index 80a9954ea9e3b..90c641d7ecd19 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.cpp
@@ -30,6 +30,7 @@
 #include <climits>
 #include <cstring>
 #include <sys/stat.h>
+#include <thread>
 #include <variant>
 
 #if HAVE_LIBCOMPRESSION
@@ -136,6 +137,9 @@ GDBRemoteCommunication::SendNotificationPacketNoLock(
 GDBRemoteCommunication::PacketResult
 GDBRemoteCommunication::SendRawPacketNoLock(llvm::StringRef packet,
                                             bool skip_ack) {
+  if (m_packet_test_delay.count() > 0)
+    std::this_thread::sleep_for(m_packet_test_delay);
+
   if (IsConnected()) {
     Log *log = GetLog(GDBRLog::Packets);
     ConnectionStatus status = eConnectionStatusSuccess;
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.h
index 35bf5eb2e3f0d..83f27aeddd897 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.h
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunication.h
@@ -134,6 +134,10 @@ class GDBRemoteCommunication : public Communication {
 
   std::chrono::seconds GetPacketTimeout() const { return m_packet_timeout; }
 
+  void SetPacketTestDelay(std::chrono::milliseconds delay) {
+    m_packet_test_delay = delay;
+  }
+
   // Start a debugserver instance on the current host using the
   // supplied connection URL.
   static Status
@@ -148,6 +152,7 @@ class GDBRemoteCommunication : public Communication {
 
 protected:
   std::chrono::seconds m_packet_timeout;
+  std::chrono::milliseconds m_packet_test_delay{0};
   uint32_t m_echo_number;
   LazyBool m_supports_qEcho;
   GDBRemoteCommunicationHistory m_history;
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index adf108919b36e..48a5e516c544a 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -177,6 +177,12 @@ class PluginProperties : public Properties {
     const uint32_t idx = ePropertyUseGPacketForReading;
     return GetPropertyAtIndexAs<bool>(idx, true);
   }
+
+  uint64_t GetPacketTestDelay() const {
+    const uint32_t idx = ePropertyPacketTestDelay;
+    return GetPropertyAtIndexAs<uint64_t>(
+        idx, g_processgdbremote_properties[idx].default_uint_value);
+  }
 };
 
 std::chrono::seconds ResumeTimeout() { return std::chrono::seconds(5); }
@@ -463,7 +469,7 @@ void ProcessGDBRemote::BuildDynamicRegisterInfo(bool force) {
     assert(packet_len < (int)sizeof(packet));
     UNUSED_IF_ASSERT_DISABLED(packet_len);
     StringExtractorGDBRemote response;
-    if (m_gdb_comm.SendPacketAndWaitForResponse(packet, response) ==
+    if (SendPacketAndWaitForResponse(packet, response) ==
         GDBRemoteCommunication::PacketResult::Success) {
       response_type = response.GetResponseType();
       if (response_type == StringExtractorGDBRemote::eResponse) {
@@ -930,8 +936,7 @@ Status ProcessGDBRemote::ConnectToDebugserver(llvm::StringRef connect_url) {
   auto handle_cmds = [&] (const Args &args) ->  void {
     for (const Args::ArgEntry &entry : args) {
       StringExtractorGDBRemote response;
-      m_gdb_comm.SendPacketAndWaitForResponse(
-          entry.c_str(), response);
+      SendPacketAndWaitForResponse(entry.c_str(), response);
     }
   };
 
@@ -2804,8 +2809,7 @@ size_t ProcessGDBRemote::DoReadMemory(addr_t addr, void *buf, size_t size,
   assert(packet_len + 1 < (int)sizeof(packet));
   UNUSED_IF_ASSERT_DISABLED(packet_len);
   StringExtractorGDBRemote response;
-  if (m_gdb_comm.SendPacketAndWaitForResponse(packet, response,
-                                              GetInterruptTimeout()) ==
+  if (SendPacketAndWaitForResponse(packet, response, GetInterruptTimeout()) ==
       GDBRemoteCommunication::PacketResult::Success) {
     if (response.IsNormalResponse()) {
       error.Clear();
@@ -2927,8 +2931,8 @@ ProcessGDBRemote::SendMultiMemReadPacket(
 
   StringExtractorGDBRemote response;
   GDBRemoteCommunication::PacketResult packet_result =
-      m_gdb_comm.SendPacketAndWaitForResponse(packet_str.data(), response,
-                                              GetInterruptTimeout());
+      SendPacketAndWaitForResponse(packet_str.data(), response,
+                                   GetInterruptTimeout());
   if (packet_result != GDBRemoteCommunication::PacketResult::Success)
     return llvm::createStringErrorV("MultiMemRead failed to send packet: '{0}'",
                                     packet_str);
@@ -2982,6 +2986,17 @@ llvm::Error ProcessGDBRemote::ParseMultiMemReadPacket(
   return llvm::Error::success();
 }
 
+GDBRemoteCommunication::PacketResult
+ProcessGDBRemote::SendPacketAndWaitForResponse(
+    llvm::StringRef payload, StringExtractorGDBRemote &response,
+    std::chrono::seconds interrupt_timeout, bool sync_on_timeout) {
+
+  m_gdb_comm.SetPacketTestDelay(std::chrono::milliseconds(
+      GetGlobalPluginProperties().GetPacketTestDelay()));
+  return m_gdb_comm.SendPacketAndWaitForResponse(
+      payload, response, interrupt_timeout, sync_on_timeout);
+}
+
 bool ProcessGDBRemote::SupportsMemoryTagging() {
   return m_gdb_comm.GetMemoryTaggingSupported();
 }
@@ -3103,8 +3118,8 @@ Status ProcessGDBRemote::FlashErase(lldb::addr_t addr, size_t size) {
                 (uint64_t)range.GetByteSize());
 
   StringExtractorGDBRemote response;
-  if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response,
-                                              GetInterruptTimeout()) ==
+  if (SendPacketAndWaitForResponse(packet.GetString(), response,
+                                   GetInterruptTimeout()) ==
       GDBRemoteCommunication::PacketResult::Success) {
     if (response.IsOKResponse()) {
       m_erased_flash_ranges.Insert(range, true);
@@ -3134,8 +3149,8 @@ Status ProcessGDBRemote::FlashDone() {
   if (m_erased_flash_ranges.IsEmpty())
     return status;
   StringExtractorGDBRemote response;
-  if (m_gdb_comm.SendPacketAndWaitForResponse("vFlashDone", response,
-                                              GetInterruptTimeout()) ==
+  if (SendPacketAndWaitForResponse("vFlashDone", response,
+                                   GetInterruptTimeout()) ==
       GDBRemoteCommunication::PacketResult::Success) {
     if (response.IsOKResponse()) {
       m_erased_flash_ranges.Clear();
@@ -3196,8 +3211,8 @@ size_t ProcessGDBRemote::DoWriteMemory(addr_t addr, const void *buf,
                              endian::InlHostByteOrder());
   }
   StringExtractorGDBRemote response;
-  if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response,
-                                              GetInterruptTimeout()) ==
+  if (SendPacketAndWaitForResponse(packet.GetString(), response,
+                                   GetInterruptTimeout()) ==
       GDBRemoteCommunication::PacketResult::Success) {
     if (response.IsOKResponse()) {
       error.Clear();
@@ -4240,7 +4255,7 @@ ProcessGDBRemote::GetExtendedInfoForThread(lldb::tid_t tid) {
 
     StringExtractorGDBRemote response;
     response.SetResponseValidatorToJSON();
-    if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) ==
+    if (SendPacketAndWaitForResponse(packet.GetString(), response) ==
         GDBRemoteCommunication::PacketResult::Success) {
       StringExtractorGDBRemote::ResponseType response_type =
           response.GetResponseType();
@@ -4337,7 +4352,7 @@ ProcessGDBRemote::GetLoadedDynamicLibrariesInfos_sender(
 
     StringExtractorGDBRemote response;
     response.SetResponseValidatorToJSON();
-    if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) ==
+    if (SendPacketAndWaitForResponse(packet.GetString(), response) ==
         GDBRemoteCommunication::PacketResult::Success) {
       StringExtractorGDBRemote::ResponseType response_type =
           response.GetResponseType();
@@ -4358,8 +4373,7 @@ StructuredData::ObjectSP ProcessGDBRemote::GetDynamicLoaderProcessState() {
   if (m_gdb_comm.GetDynamicLoaderProcessStateSupported()) {
     StringExtractorGDBRemote response;
     response.SetResponseValidatorToJSON();
-    if (m_gdb_comm.SendPacketAndWaitForResponse("jGetDyldProcessState",
-                                                response) ==
+    if (SendPacketAndWaitForResponse("jGetDyldProcessState", response) ==
         GDBRemoteCommunication::PacketResult::Success) {
       StringExtractorGDBRemote::ResponseType response_type =
           response.GetResponseType();
@@ -4386,7 +4400,7 @@ StructuredData::ObjectSP ProcessGDBRemote::GetSharedCacheInfo() {
 
   StringExtractorGDBRemote response;
   response.SetResponseValidatorToJSON();
-  if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) ==
+  if (SendPacketAndWaitForResponse(packet.GetString(), response) ==
       GDBRemoteCommunication::PacketResult::Success) {
     StringExtractorGDBRemote::ResponseType response_type =
         response.GetResponseType();
@@ -5565,7 +5579,7 @@ Status ProcessGDBRemote::GetFileLoadAddress(const FileSpec &file,
   packet.PutStringAsRawHex8(file_path);
 
   StringExtractorGDBRemote response;
-  if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) !=
+  if (SendPacketAndWaitForResponse(packet.GetString(), response) !=
       GDBRemoteCommunication::PacketResult::Success)
     return Status::FromErrorString("Sending qFileLoadAddress packet failed");
 
@@ -5729,7 +5743,7 @@ llvm::Expected<bool> ProcessGDBRemote::SaveCore(llvm::StringRef outfile) {
   packet.PutStringAsRawHex8(outfile);
 
   StringExtractorGDBRemote response;
-  if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) ==
+  if (SendPacketAndWaitForResponse(packet.GetString(), response) ==
       GDBRemoteCommunication::PacketResult::Success) {
     // TODO: grab error message from the packet?  StringExtractor seems to
     // be missing a method for that
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
index 7c2877fa71d49..67d408ba22ad5 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
@@ -152,6 +152,11 @@ class ProcessGDBRemote : public Process,
       unsigned expected_num_ranges,
       llvm::SmallVectorImpl<llvm::MutableArrayRef<uint8_t>> &memory_regions);
 
+  GDBRemoteClientBase::PacketResult SendPacketAndWaitForResponse(
+      llvm::StringRef payload, StringExtractorGDBRemote &response,
+      std::chrono::seconds interrupt_timeout = std::chrono::seconds(0),
+      bool sync_on_timeout = true);
+
 public:
   Status
   WriteObjectFile(std::vector<ObjectFile::LoadableData> entries) override;
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemoteProperties.td b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemoteProperties.td
index 08dee3089a094..60188d0286678 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemoteProperties.td
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemoteProperties.td
@@ -21,4 +21,10 @@ let Definition = "processgdbremote", Path = "plugin.process.gdb-remote" in {
     Global,
     DefaultFalse,
     Desc<"Specify if the server should use 'g' packets to read registers.">;
+  def PacketTestDelay
+      : Property<"packet-test-delay", "UInt64">,
+        Global,
+        DefaultUnsignedValue<0>,
+        Desc<"Specify an artificial delay in milliseconds inserted before "
+             "sending each packet, for testing purposes.">;
 }
diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestPacketTestDelay.py b/lldb/test/API/functionalities/gdb_remote_client/TestPacketTestDelay.py
new file mode 100644
index 0000000000000..7d498907a05f8
--- /dev/null
+++ b/lldb/test/API/functionalities/gdb_remote_client/TestPacketTestDelay.py
@@ -0,0 +1,39 @@
+import time
+import lldb
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test.decorators import *
+from lldbsuite.test.gdbclientutils import *
+from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase
+
+
+class TestPacketTestDelay(GDBRemoteTestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_packet_test_delay(self):
+        """Verify that packet-test-delay inserts a delay before each sent packet."""
+
+        # 1000ms should be long enough that this test doesn't pass by
+        # accident even on slow machines, but not too long to waste test
+        # suite time.
+        DELAY_MS = 1000
+
+        class MyResponder(MockGDBServerResponder):
+            def x(self, addr, length):
+                return "foobar"
+
+        self.server.responder = MyResponder()
+        target = self.dbg.CreateTargetWithFileAndTargetTriple("", "x86_64-pc-linux")
+        process = self.connect(target)
+
+        error = lldb.SBError()
+        start = time.time()
+        self.runCmd(
+            "settings set plugin.process.gdb-remote.packet-test-delay %d" % DELAY_MS
+        )
+        # Send a single dummy package so we can observe the delay.
+        process.ReadMemory(0x1000, 10, error)
+        elapsed_ms = (time.time() - start) * 1000
+
+        self.assertGreaterEqual(
+            elapsed_ms, DELAY_MS, "Package was sent faster than the set test delay?"
+        )



More information about the lldb-commits mailing list