[Lldb-commits] [lldb] [lldb][debugserver] Expedite the stopped frame's stack memory in jThreadsInfo (PR #212706)
via lldb-commits
lldb-commits at lists.llvm.org
Wed Jul 29 01:20:34 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Yao Qi (qiyao)
<details>
<summary>Changes</summary>
`ReadStackMemory` only expedites the frame-pointer backchain, which is
enough to unwind the stack but not to read a frame's locals: a local's
bytes live between `$sp` and `$fp`, addresses that are never sent up. As
a result, examining the stopped frame's locals at a public stop generated
one memory-read packets.
Add `ReadFrameZeroStackMemory`, which expedites the innermost frame's
stack memory. Rather than a single `[$sp, $fp)` block (which would have
to give up entirely on large frames), it covers where variables actually
live using two anchored windows: `[$fp - k_expedite_stack_window, $fp)`
for locals/spills and `[$sp, $sp + k_expedite_stack_window)` for params.
k_expedite_stack_window = 512 is sized to cover the common case
(variables sit within a few hundred bytes of their anchor) while bounding
the per-frame cost to 2*k_expedite_stack_window bytes regardless of frame
size.
For a small frame (`$fp - $sp <= 2*k_expedite_stack_window`) the two
windows would overlap, so a single contiguous `[$sp, $fp)` chunk is emitted
instead, giving full coverage with no gap. The `$fp` side always stops at
`$fp` exclusive so it does not duplicate the saved `{fp, lr}` pair already
covered by the backchain expedite. Each window is emitted as its own
chunk because lldb's L1 cache only serves a read fully contained in one
expedited chunk.
A debugger reads a frame's locals only for the selected frame
of the thread that stopped, so this frame-0 window is emitted for the
current thread only rather than every thread, avoiding stop-reply bloat
proportional to thread count.
`GetJSONThreadsInfo` now builds the `"memory"` array from both sources
and emits it whenever either produced an entry. Add
`JSONGenerator::Array::empty` to support that check.
---
Full diff: https://github.com/llvm/llvm-project/pull/212706.diff
3 Files Affected:
- (modified) lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py (+44-28)
- (modified) lldb/tools/debugserver/source/JSONGenerator.h (+2)
- (modified) lldb/tools/debugserver/source/RNBRemote.cpp (+92-3)
``````````diff
diff --git a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
index e8c8076699af7..e76750c13aa25 100644
--- a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
+++ b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
@@ -3,19 +3,21 @@
public stop.
On Darwin, debugserver expedites the frame-pointer backchain (up to 256 frames,
-for every thread) in the jThreadsInfo response at a public stop, and lldb seeds
-those bytes into its memory cache. Consequences exercised here:
+for every thread) and the stopped frame's stack memory in the jThreadsInfo
+response at a public stop, and lldb seeds those bytes into its memory cache.
+Consequences exercised here:
* A backtrace (GetNumFrames() / GetFrameAtIndex() for every frame) is
satisfied entirely from the expedited/cached backchain and sends no packets.
With the cache disabled it must read the backchain frame by frame, which
confirms the test is really exercising the unwinder's memory reads.
- * Examining frame local variables the way an IDE does is NOT covered by the
- expedite: the values live at addresses that were never sent up, so reading
- them produces memory-read packets. This is checked two ways, mirroring
- an IDE: examining only the selected frame's locals (frame 0, what a
- variables view does on a stop) and examining every frame's locals (the
+ * Examining the stopped frame's locals is covered by the stack expedite, so
+ it sends no stack memory-read packets; heap buffers behind pointers are not
+ on the stack and are still read from the stub. Examining deeper frames'
+ locals does produce stack memory-read packets. This is checked two ways,
+ mirroring an IDE: examining only the selected frame's locals (frame 0, what
+ a variables view does on a stop) and examining every frame's locals (the
"view all frames" case).
"""
@@ -50,24 +52,31 @@ def test_memory_reads_when_examining_frame0_locals(self):
"""Model an IDE stop: walk the whole stack (a backtrace / debug
navigator) but examine the locals of only the selected frame 0.
Frame 0 (func_e in main.c) carries scalar, aggregate, and
- pointer-to-heap locals, so examining it alone reads both stack and
- heap memory."""
- self.check_memory_reads_when_examining_locals(examine_all_frames=False)
+ pointer-to-heap locals. Its stack is expedited, so examining it
+ reads heap memory but no stack memory."""
+ self.check_memory_reads_when_examining_locals(
+ examine_all_frames=False, expect_stack_reads=False
+ )
@skipUnlessDarwin
def test_memory_reads_when_examining_all_frames_locals(self):
"""Model "view all frames": walk the whole stack and examine every
- frame's locals. This reads the same variety of memory across several
- frames."""
- self.check_memory_reads_when_examining_locals(examine_all_frames=True)
+ frame's locals. Only the stopped frame's stack is expedited, so the
+ deeper frames' locals still read stack memory."""
+ self.check_memory_reads_when_examining_locals(
+ examine_all_frames=True, expect_stack_reads=True
+ )
- def check_memory_reads_when_examining_locals(self, examine_all_frames):
- """Examining frame locals reads value memory that is not expedited.
- Classify those reads into stack vs heap and check the counts.
+ def check_memory_reads_when_examining_locals(
+ self, examine_all_frames, expect_stack_reads
+ ):
+ """Examining frame locals reads value memory; classify those reads into
+ stack vs heap and check the counts.
- The frame-pointer backchain is expedited, but the locals' *values* are
- not, so both stack-resident locals and heap buffers behind pointers are
- read from the stub today.
+ The frame-pointer backchain and the stopped frame's stack are expedited,
+ so frame 0's stack-resident locals are served from the cache while heap
+ buffers behind pointers (and deeper frames' locals) are read from the
+ stub.
We have two regions and ask the process which one each read falls in:
* the stack region: whichever region the stack pointer points into.
@@ -131,15 +140,22 @@ def per_frame(idx, frame):
)
)
- # Examining locals reads both stack and heap memory.
- self.assertGreater(
- len(stack_reads),
- 0,
- "expected stack memory reads while examining stack-resident "
- "locals.\n" + breakdown,
- )
- # Heap reads come from disclosing the pointer-to-heap local; a stack
- # expedite would NOT remove these.
+ if expect_stack_reads:
+ # Deeper frames' stacks are not expedited.
+ self.assertGreater(
+ len(stack_reads),
+ 0,
+ "expected stack memory reads while examining deeper frames' "
+ "stack-resident locals.\n" + breakdown,
+ )
+ else:
+ # The stopped frame's stack is expedited.
+ self.assertEqual(
+ len(stack_reads),
+ 0,
+ "expected NO stack memory reads for frame 0 (its stack is "
+ "expedited in jThreadsInfo).\n" + breakdown,
+ )
self.assertGreater(
len(heap_reads),
0,
diff --git a/lldb/tools/debugserver/source/JSONGenerator.h b/lldb/tools/debugserver/source/JSONGenerator.h
index b545a6e29a767..114de9dd992af 100644
--- a/lldb/tools/debugserver/source/JSONGenerator.h
+++ b/lldb/tools/debugserver/source/JSONGenerator.h
@@ -127,6 +127,8 @@ class JSONGenerator {
void AddItem(ObjectSP item) { m_items.push_back(item); }
+ bool empty() const { return m_items.empty(); }
+
void AddIntegerItem(uint64_t value) {
AddItem(ObjectSP(new Integer(value)));
}
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index 91d611f9ecbc5..a3ca5a73ddfc2 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -2733,6 +2733,76 @@ static void ReadStackMemory(nub_process_t pid, nub_thread_t tid,
}
}
+// The size of each per-side stack window we expedite. 512 is sized to cover
+// the common case (locals and params sit within a few hundred bytes) while
+// bounding the per-frame cost to 1K bytes regardless of frame size.
+static const nub_size_t k_expedite_stack_window = 512;
+
+// A single contiguous chunk of expedited memory.
+struct ExpeditedMemory {
+ nub_addr_t addr;
+ std::vector<uint8_t> bytes;
+};
+
+// Read the innermost frame's stack memory so that examining its local variables
+// at a public stop is served from the expedited cache instead of generating one
+// memory-read packet.
+//
+// We produce either:
+//
+// - one chunk [$sp, $fp) for a small frame ($fp - $sp <=
+// 2*k_expedite_stack_window), which covers the whole frame with no gap, or
+//
+// - two chunks [$sp, $sp + k_expedite_stack_window) and [$fp -
+// k_expedite_stack_window, $fp) for a large frame, covering the params near
+// $sp and the locals near $fp while leaving the (rarely-interesting) middle
+// spill area out so the cost stays bounded.
+static void ReadFrameZeroStackMemory(nub_process_t pid, nub_thread_t tid,
+ std::vector<ExpeditedMemory> &chunks) {
+ chunks.clear();
+ std::unique_ptr<DNBRegisterValue> sp_value =
+ std::make_unique<DNBRegisterValue>();
+ std::unique_ptr<DNBRegisterValue> fp_value =
+ std::make_unique<DNBRegisterValue>();
+ if (!DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
+ GENERIC_REGNUM_SP, sp_value.get()) ||
+ !DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
+ GENERIC_REGNUM_FP, fp_value.get()))
+ return;
+
+ const nub_size_t ptr_size = sp_value->info.size;
+ uint64_t sp =
+ (ptr_size == 4) ? sp_value->value.uint32 : sp_value->value.uint64;
+ uint64_t fp =
+ (ptr_size == 4) ? fp_value->value.uint32 : fp_value->value.uint64;
+
+ // The stack grows down, so a normal frame has sp < fp. Bail on a leaf/empty
+ // frame (sp == fp) or anything that doesn't look like a frame.
+ if (sp == 0 || fp <= sp)
+ return;
+
+ auto read_range = [&](uint64_t start, uint64_t length) {
+ std::vector<uint8_t> buf(length);
+ if (DNBProcessMemoryRead(pid, start, length, buf.data()) != length)
+ return;
+ chunks.push_back({start, std::move(buf)});
+ };
+
+ const uint64_t frame_size = fp - sp;
+ const nub_size_t window = k_expedite_stack_window;
+
+ if (frame_size <= 2 * window) {
+ // Small frame: cover the whole frame as a single contiguous chunk [sp, fp).
+ read_range(sp, frame_size);
+ return;
+ }
+
+ // Large frame: cover the params near $sp and the locals near $fp with two
+ // bounded windows, leaving the middle spill area out to keep the cost capped.
+ read_range(sp, window); // [sp, sp + WINDOW)
+ read_range(fp - window, window); // [fp - WINDOW, fp)
+}
+
rnb_err_t RNBRemote::SendStopReplyPacketForThread(nub_thread_t tid) {
const nub_process_t pid = m_ctx.ProcessID();
if (pid == INVALID_NUB_PROCESS)
@@ -5866,9 +5936,10 @@ RNBRemote::GetJSONThreadsInfo(bool threads_with_valid_stop_info_only) {
// frame pointer chain.
StackMemoryMap stack_mmap;
ReadStackMemory(pid, tid, stack_mmap);
- if (!stack_mmap.empty()) {
- JSONGenerator::ArraySP memory_array_sp(new JSONGenerator::Array());
+ JSONGenerator::ArraySP memory_array_sp(new JSONGenerator::Array());
+
+ if (!stack_mmap.empty()) {
for (const auto &stack_memory : stack_mmap) {
JSONGenerator::DictionarySP stack_memory_sp(
new JSONGenerator::Dictionary());
@@ -5877,9 +5948,27 @@ RNBRemote::GetJSONThreadsInfo(bool threads_with_valid_stop_info_only) {
"bytes", stack_memory.second.bytes, stack_memory.second.length);
memory_array_sp->AddItem(stack_memory_sp);
}
- thread_dict_sp->AddItem("memory", memory_array_sp);
}
+ // Also expedite the innermost frame's stack memory of the thread that
+ // stopped.
+ if (tid == DNBProcessGetCurrentThread(pid)) {
+ std::vector<ExpeditedMemory> frame_zero_chunks;
+ ReadFrameZeroStackMemory(pid, tid, frame_zero_chunks);
+
+ for (const auto &chunk : frame_zero_chunks) {
+ JSONGenerator::DictionarySP frame_zero_sp(
+ new JSONGenerator::Dictionary());
+ frame_zero_sp->AddIntegerItem("address", chunk.addr);
+ frame_zero_sp->AddBytesAsHexASCIIString("bytes", chunk.bytes.data(),
+ chunk.bytes.size());
+ memory_array_sp->AddItem(frame_zero_sp);
+ }
+ }
+
+ if (!memory_array_sp->empty())
+ thread_dict_sp->AddItem("memory", memory_array_sp);
+
std::vector<uint64_t> added_binaries;
JSONGenerator::ObjectSP detailed_binary_infos;
``````````
</details>
https://github.com/llvm/llvm-project/pull/212706
More information about the lldb-commits
mailing list