[Lldb-commits] [lldb] 77aa4b9 - [lldb][debugserver] Expedite the memory ranges lldb read most recently (#223417)
via lldb-commits
lldb-commits at lists.llvm.org
Wed Sep 16 05:40:44 PDT 2026
Author: Yao Qi
Date: 2026-09-16T13:40:38+01:00
New Revision: 77aa4b9ac117acfcd7384166cccd4a333829bf81
URL: https://github.com/llvm/llvm-project/commit/77aa4b9ac117acfcd7384166cccd4a333829bf81
DIFF: https://github.com/llvm/llvm-project/commit/77aa4b9ac117acfcd7384166cccd4a333829bf81.diff
LOG: [lldb][debugserver] Expedite the memory ranges lldb read most recently (#223417)
The rationale of this change is that if user/ide/lldb reads memory on
some address, it more likely to read them again in next stop. If
debugserver expedites the memory of recent reads in public stop, lldb
won't send memory read packet to read them again. Those addresses are
already known when the previous stop ends.
Expedite up to 16 memory ranges (at most 512 bytes each) read by `m`,
`x`, and `MultiMemRead` in `jThreadsInfo` for the stopped thread, and
advertise `ExpediteRecentReads+` via `qSupported`. Re-reading a range
moves it to the newest slot, and the ring is cleared on exec.
The gain is large in three use cases:
- TLS variables. `DynamicLoaderDarwin::GetThreadLocalData` reads the TLS
thunk and then calls its `get_addr` in the target, and each such call
surfaces as a public stop, in `ProcessEventData::DoOnRemoval`. lldb
clears its memory cache at every stop, so the thunk and its data block
were read again at each one. Three `frame variable tls_var tls_double`
commands on a two-variable test program send 205 memory-read packets
before this change and 9 after; the reads at that one call site go
from 22 to 2.
- conditional breakpoints: one every stop, lldb reads the same variables
evaluate the condition, and resumes the execution if condition is
false. After this change, these variables are expedited in stop, and
lldb doesn't need to send memory read packets, `x` packet is reduced
from 473 to 432.
- pointer-chase reads. Printing an Objective-C object reads the isa
pointer, then walks the class and its method lists, in
`ClassDescriptorV2::method_list_t::Read` and `method_t::ReadNames`.
None of that is on the stack that the frame 0 expedite covers. A `po`
of one object sends 387 memory-read packets before and 368 after.
Assisted-by: claude
Added:
lldb/test/API/macosx/expedited-recent-reads/Makefile
lldb/test/API/macosx/expedited-recent-reads/TestExpeditedRecentReads.py
lldb/test/API/macosx/expedited-recent-reads/main.c
Modified:
lldb/packages/Python/lldbsuite/test/tools/lldb-server/gdbremote_testcase.py
lldb/tools/debugserver/source/RNBRemote.cpp
lldb/tools/debugserver/source/RNBRemote.h
Removed:
################################################################################
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-server/gdbremote_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-server/gdbremote_testcase.py
index b88fced342218..20d152a86356a 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-server/gdbremote_testcase.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-server/gdbremote_testcase.py
@@ -955,6 +955,7 @@ def add_qSupported_packets(self, client_features=[]):
"jMultiBreakpoint",
"accelerator-plugins",
"ExpediteStack",
+ "ExpediteRecentReads",
]
def parse_qSupported_response(self, context):
diff --git a/lldb/test/API/macosx/expedited-recent-reads/Makefile b/lldb/test/API/macosx/expedited-recent-reads/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/macosx/expedited-recent-reads/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/macosx/expedited-recent-reads/TestExpeditedRecentReads.py b/lldb/test/API/macosx/expedited-recent-reads/TestExpeditedRecentReads.py
new file mode 100644
index 0000000000000..b0e64e6eb4fcb
--- /dev/null
+++ b/lldb/test/API/macosx/expedited-recent-reads/TestExpeditedRecentReads.py
@@ -0,0 +1,205 @@
+"""
+Tests that debugserver expedites the memory ranges lldb read most recently, so a
+value examined at one stop is not read from the stub again at the next stop.
+
+The value under test lives on the heap, so a read served without a packet at the
+second stop can only come from the recent-reads expedite. Disabling lldb's
+memory cache brings the read back, which shows the saving comes from the seeded
+cache rather than from lldb not asking.
+"""
+
+import os
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+from lldbsuite.test.gdbclientutils import (
+ PacketDirection,
+ parse_memory_read_ranges,
+ parse_packet_log,
+)
+
+
+class TestExpeditedRecentReads(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ @requireDarwin
+ def test_heap_read_expedited_at_the_next_stop(self):
+ """The second stop serves the heap buffer from the cache, no packet."""
+ self.check_heap_reads(disable_memory_cache=False)
+
+ @requireDarwin
+ def test_heap_read_not_expedited_without_the_cache(self):
+ """With the memory cache off the second stop reads the heap buffer from
+ the stub, which confirms the test exercises the cache."""
+ self.check_heap_reads(disable_memory_cache=True)
+
+ @requireDarwin
+ def test_stack_read_expedited_at_the_next_stop(self):
+ """A stack array below the window frame 0 expedites is served from the
+ cache at the next stop, which only the recent reads can supply."""
+ self.build()
+ logfile = self.packet_log("stack-reads-packets")
+
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec("main.c")
+ )
+ self.runCmd("settings set target.process.disable-memory-cache false")
+
+ stack_addr, stack_size = self.stack_range(thread)
+ self.assertNotEqual(
+ self.examine_stack(logfile, thread, "FirstStackStop"),
+ [],
+ "the first stop should read the array from the stub",
+ )
+
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+ thread = process.GetSelectedThread()
+
+ second = self.examine_stack(logfile, thread, "SecondStackStop")
+ lldbutil.require_qsupported_capability(self, "ExpediteRecentReads+")
+ self.assertEqual(
+ self.covering(second, stack_addr, stack_size),
+ [],
+ "the second stop should read no memory covering the array's first "
+ "elements [0x%x,0x%x); reads: %s"
+ % (stack_addr, stack_addr + stack_size, self.describe(second)),
+ )
+
+ def packet_log(self, name):
+ """Enable packet logging to a scratch file, removed on teardown."""
+ logfile = os.path.join(
+ self.getBuildDir(), "%s-%s.txt" % (name, self.getArchitecture())
+ )
+ self.runCmd("log enable -f %s gdb-remote packets" % logfile)
+
+ def cleanup():
+ self.runCmd("log disable gdb-remote packets")
+ if os.path.exists(logfile):
+ os.unlink(logfile)
+
+ self.addTearDownHook(cleanup)
+ return logfile
+
+ @staticmethod
+ def stack_range(thread):
+ """The (address, size) of the leading elements of the 'distant' array.
+
+ Only the elements the test reads are covered, so an unrelated read
+ elsewhere in the array does not count."""
+ local = thread.GetFrameAtIndex(0).FindVariable("distant")
+ element_size = local.GetType().GetArrayElementType().GetByteSize()
+ return local.GetLoadAddress(), 4 * element_size
+
+ def examine_stack(self, logfile, thread, marker):
+ """Read the array's leading elements, and return the ranges the
+ memory-read packets asked the stub for."""
+ self.runCmd("process plugin packet send Start%s" % marker, check=False)
+
+ local = thread.GetFrameAtIndex(0).FindVariable("distant")
+ self.assertTrue(local.IsValid(), "could not find 'distant'")
+ for i in range(4):
+ local.GetChildAtIndex(i).GetValueAsSigned()
+
+ self.runCmd("process plugin packet send End%s" % marker, check=False)
+ return self.ranges_between_markers(logfile, marker)
+
+ def check_heap_reads(self, disable_memory_cache):
+ self.build()
+ # Every method in this class shares one build directory.
+ suffix = "nocache" if disable_memory_cache else "cached"
+ logfile = self.packet_log("heap-reads-%s-packets" % suffix)
+
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec("main.c")
+ )
+ self.runCmd(
+ "settings set target.process.disable-memory-cache %s"
+ % ("true" if disable_memory_cache else "false")
+ )
+
+ heap_addr, heap_size = self.heap_range(thread)
+ first = self.examine_heap(logfile, thread, "FirstHeapStop" + suffix)
+ self.assertNotEqual(
+ self.covering(first, heap_addr, heap_size),
+ [],
+ "the first stop should read the heap buffer from the stub; reads: %s"
+ % self.describe(first),
+ )
+
+ process.Continue()
+ self.assertState(process.GetState(), lldb.eStateStopped)
+ thread = process.GetSelectedThread()
+ self.assertEqual(
+ self.heap_range(thread),
+ (heap_addr, heap_size),
+ "the heap buffer moved between stops",
+ )
+
+ second = self.examine_heap(logfile, thread, "SecondHeapStop" + suffix)
+ covering = self.covering(second, heap_addr, heap_size)
+ if disable_memory_cache:
+ self.assertNotEqual(
+ covering,
+ [],
+ "with the memory cache disabled the second stop should read the "
+ "heap buffer from the stub; reads: %s" % self.describe(second),
+ )
+ else:
+ lldbutil.require_qsupported_capability(self, "ExpediteRecentReads+")
+ self.assertEqual(
+ covering,
+ [],
+ "the second stop should read no memory covering the heap buffer "
+ "[0x%x,0x%x) (the range is expedited in jThreadsInfo); reads: %s"
+ % (heap_addr, heap_addr + heap_size, self.describe(second)),
+ )
+
+ @staticmethod
+ def heap_range(thread):
+ """The (address, size) of the buffer the 'heap' local points at."""
+ heap = thread.GetFrameAtIndex(0).FindVariable("heap")
+ return heap.GetValueAsUnsigned(0), heap.GetType().GetPointeeType().GetByteSize()
+
+ def examine_heap(self, logfile, thread, marker):
+ """Read every element of the heap buffer the way a variables view would,
+ and return the ranges the memory-read packets asked the stub for.
+
+ The reads are bracketed by two unsupported packets, whose names appear in
+ the log around them."""
+ self.runCmd("process plugin packet send Start%s" % marker, check=False)
+
+ pointee = thread.GetFrameAtIndex(0).FindVariable("heap").Dereference()
+ self.assertTrue(pointee.IsValid(), "could not dereference 'heap'")
+ values = pointee.GetChildMemberWithName("values")
+ for i in range(values.GetNumChildren()):
+ values.GetChildAtIndex(i).GetValueAsSigned()
+ pointee.GetChildMemberWithName("name").GetSummary()
+
+ self.runCmd("process plugin packet send End%s" % marker, check=False)
+ return self.ranges_between_markers(logfile, marker)
+
+ def ranges_between_markers(self, logfile, marker):
+ """The memory ranges requested between the two marker packets."""
+ self.assertTrue(os.path.exists(logfile), "packet log was created")
+ log_text = open(logfile).read()
+ self.assertIn("Start%s" % marker, log_text, "start marker not logged")
+ self.assertIn("End%s" % marker, log_text, "end marker not logged")
+ window = log_text.split("Start%s" % marker, 1)[-1].split("End%s" % marker, 1)[0]
+
+ ranges = []
+ for direction, body in parse_packet_log(window.splitlines()):
+ if direction == PacketDirection.SEND:
+ ranges += parse_memory_read_ranges(body)
+ return ranges
+
+ @staticmethod
+ def covering(ranges, addr, size):
+ """The reads that intersect [addr, addr + size)."""
+ return [r for r in ranges if r[0] < addr + size and addr < r[0] + r[1]]
+
+ @staticmethod
+ def describe(ranges):
+ return ", ".join("[0x%x,0x%x)" % (a, a + n) for a, n in ranges) or "none"
diff --git a/lldb/test/API/macosx/expedited-recent-reads/main.c b/lldb/test/API/macosx/expedited-recent-reads/main.c
new file mode 100644
index 0000000000000..11ddce3d0f35a
--- /dev/null
+++ b/lldb/test/API/macosx/expedited-recent-reads/main.c
@@ -0,0 +1,25 @@
+#include <stdlib.h>
+
+struct Payload {
+ long values[8];
+ char name[16];
+};
+
+#define DISTANT_COUNT 512
+
+int main(void) {
+ struct Payload *heap = (struct Payload *)calloc(1, sizeof(struct Payload));
+ for (int i = 0; i < 8; ++i)
+ heap->values[i] = i * 100;
+
+ long distant[DISTANT_COUNT];
+ for (int i = 0; i < DISTANT_COUNT; ++i)
+ distant[i] = i * 7 + 1;
+
+ long total = 0;
+ for (int i = 0; i < 4; ++i) {
+ total += heap->values[i] + distant[i]; // break here
+ }
+ free(heap);
+ return (int)(total & 0x7f);
+}
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index 6ac1ca12f1b54..4318c16b5fda1 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -256,7 +256,7 @@ int csr_check(csr_config_t mask);
RNBRemote::RNBRemote()
: m_ctx(), m_comm(), m_arch(), m_continue_thread(-1), m_thread(-1),
m_mutex(), m_dispatch_queue_offsets(),
- m_dispatch_queue_offsets_addr(INVALID_NUB_ADDRESS),
+ m_dispatch_queue_offsets_addr(INVALID_NUB_ADDRESS), m_recent_reads(),
m_qSymbol_index(UINT32_MAX), m_packets_recvd(0), m_packets(),
m_rx_packets(), m_rx_partial_data(), m_rx_pthread(0),
m_max_payload_size(DEFAULT_GDB_REMOTE_PROTOCOL_BUFSIZE - 4),
@@ -2779,6 +2779,9 @@ static const nub_size_t k_expedite_stack_arg_size = 160;
static_assert(k_expedite_stack_arg_size <= k_expedite_stack_window,
"above-fp arg window must fit within the total stack budget");
+static const size_t k_recent_reads_count = 16;
+static const nub_size_t k_recent_read_max_size = 512;
+
// Heuristic to decide whether frame 0's $fp looks like a valid frame pointer.
static bool FrameZeroFPLooksValid(nub_process_t pid, nub_thread_t tid,
uint64_t sp, uint64_t fp,
@@ -2837,6 +2840,20 @@ static std::vector<ExpeditedMemory> ReadFrameZeroStackMemory(nub_process_t pid,
return chunks;
}
+void RNBRemote::RecordRecentRead(nub_addr_t addr, nub_size_t size) {
+ if (size == 0 || size > k_recent_read_max_size)
+ return;
+
+ auto entry = std::make_pair(addr, size);
+ auto pos = std::find(m_recent_reads.begin(), m_recent_reads.end(), entry);
+ if (pos != m_recent_reads.end())
+ m_recent_reads.erase(pos);
+ m_recent_reads.push_back(entry);
+
+ while (m_recent_reads.size() > k_recent_reads_count)
+ m_recent_reads.pop_front();
+}
+
rnb_err_t RNBRemote::SendStopReplyPacketForThread(nub_thread_t tid) {
const nub_process_t pid = m_ctx.ProcessID();
if (pid == INVALID_NUB_PROCESS)
@@ -2855,6 +2872,7 @@ rnb_err_t RNBRemote::SendStopReplyPacketForThread(nub_thread_t tid) {
// Reset any symbols that need resetting when we exec
m_dispatch_queue_offsets_addr = INVALID_NUB_ADDRESS;
m_dispatch_queue_offsets.Clear();
+ m_recent_reads.clear();
}
std::ostringstream ostrm;
@@ -3319,6 +3337,7 @@ rnb_err_t RNBRemote::HandlePacket_m(const char *p) {
// "The reply may contain fewer bytes than requested if the server was able
// to read only part of the region of memory."
length = bytes_read;
+ RecordRecentRead(addr, length);
std::ostringstream ostrm;
for (unsigned long i = 0; i < length; i++)
@@ -3390,6 +3409,7 @@ rnb_err_t RNBRemote::HandlePacket_MultiMemRead(const char *p) {
nub_size_t bytes_read = DNBProcessMemoryRead(m_ctx.ProcessID(), base_addr,
length, buffers.back().data());
buffers.back().resize(bytes_read);
+ RecordRecentRead(base_addr, bytes_read);
}
std::ostringstream reply_stream;
@@ -3530,6 +3550,7 @@ rnb_err_t RNBRemote::HandlePacket_x(const char *p) {
}
buf.resize(bytes_read);
+ RecordRecentRead(addr, buf.size());
std::ostringstream ostrm;
binary_encode_data_vector(ostrm, buf);
@@ -3830,6 +3851,7 @@ rnb_err_t RNBRemote::HandlePacket_qSupported(const char *p) {
reply << "jMultiBreakpoint+;";
// The stopped thread's frame 0 stack memory is expedited in jThreadsInfo.
reply << "ExpediteStack+;";
+ reply << "ExpediteRecentReads+;";
return SendPacket(reply.str().c_str());
}
@@ -5973,6 +5995,11 @@ RNBRemote::GetJSONThreadsInfo(bool threads_with_valid_stop_info_only) {
std::vector<ExpeditedMemory> frame_zero_chunks =
ReadFrameZeroStackMemory(pid, tid);
AppendExpeditedMemoryToJSON(frame_zero_chunks, memory_array_sp);
+
+ std::vector<ExpeditedMemory> recent_chunks;
+ for (const auto &[addr, size] : m_recent_reads)
+ AppendExpeditedMemory(pid, addr, size, recent_chunks);
+ AppendExpeditedMemoryToJSON(recent_chunks, memory_array_sp);
}
if (!memory_array_sp->empty())
diff --git a/lldb/tools/debugserver/source/RNBRemote.h b/lldb/tools/debugserver/source/RNBRemote.h
index 2c47ee6bb390e..e8af580960541 100644
--- a/lldb/tools/debugserver/source/RNBRemote.h
+++ b/lldb/tools/debugserver/source/RNBRemote.h
@@ -402,6 +402,8 @@ class RNBRemote {
JSONGenerator::ObjectSP
GetJSONThreadsInfo(bool threads_with_valid_stop_info_only);
+ void RecordRecentRead(nub_addr_t addr, nub_size_t size);
+
RNBContext m_ctx; // process context
RNBSocket m_comm; // communication port
std::string m_arch;
@@ -410,6 +412,7 @@ class RNBRemote {
std::mutex m_mutex; // Mutex that protects
DispatchQueueOffsets m_dispatch_queue_offsets;
nub_addr_t m_dispatch_queue_offsets_addr;
+ std::deque<std::pair<nub_addr_t, nub_size_t>> m_recent_reads;
uint32_t m_qSymbol_index;
uint32_t m_packets_recvd;
Packet::collection m_packets;
More information about the lldb-commits
mailing list