[llvm-branch-commits] [lldb] dd61593 - [lldb][debugserver] Expedite the stopped frame's stack memory in jThreadsInfo (#212706)
Douglas Yung via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Thu Aug 20 13:47:27 PDT 2026
Author: Yao Qi
Date: 2026-08-20T20:47:13Z
New Revision: dd615938518e037d8e052cddb4f2b5e410275e2b
URL: https://github.com/llvm/llvm-project/commit/dd615938518e037d8e052cddb4f2b5e410275e2b
DIFF: https://github.com/llvm/llvm-project/commit/dd615938518e037d8e052cddb4f2b5e410275e2b.diff
LOG: [lldb][debugserver] Expedite the stopped frame's stack memory in jThreadsInfo (#212706)
Add `ReadFrameZeroStackMemory`, which expedites the innermost frame's stack
memory so a variables view on a stop is served from lldb's memory cache. When
frame 0's `$fp` looks usable, two windows are expedited:
* `[$fp + 2*ptr_size, $fp + 2*ptr_size + k_expedite_stack_arg_size)` for stack-passed
parameters, starting above the saved `{fp, lr}` pair the backchain already covers.
* `[$fp - below, $fp)`, `below = min($fp - $sp, k_expedite_stack_window - k_expedite_stack_arg_size)`,
for locals and spilled register arguments. A small frame gets all of `[$sp, $fp)`; a large one keeps the part nearest `$fp`, so the cost stays bounded.
If `$fp` fails validation (frameless leaf, or `$fp` used as a scratch GPR),
a single `[$sp, $sp + k_expedite_stack_window)` window is expedited instead.
Each window is a separate chunk, because lldb's L1 cache only serves reads
fully contained in one expedited chunk. Only the thread that stopped gets
these windows, so the stop reply does not grow with thread count.
`GetJSONThreadsInfo` now builds the `"memory"` array from both sources and
emits it whenever either produced an entry. Add `JSONGenerator::Array::empty`
for that check.
(cherry picked from commit b631e0cbd1c9e33f29a199b3028026ded89167bd)
Added:
Modified:
lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
lldb/test/API/macosx/expedited-stack-memory/main.c
lldb/tools/debugserver/source/JSONGenerator.h
lldb/tools/debugserver/source/RNBRemote.cpp
Removed:
################################################################################
diff --git a/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py b/lldb/test/API/macosx/expedited-stack-memory/TestExpeditedStackMemory.py
index e8c8076699af7..7e8d25e742f79 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).
"""
@@ -49,25 +51,32 @@ def test_memory_reads_during_backtrace_without_cache(self):
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)
+ Frame 0 (func_e in main.c) carries scalar, aggregate,
+ pointer-to-heap, and stack-passed-parameter 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/test/API/macosx/expedited-stack-memory/main.c b/lldb/test/API/macosx/expedited-stack-memory/main.c
index 57d219d944995..8dca4d8f16ab7 100644
--- a/lldb/test/API/macosx/expedited-stack-memory/main.c
+++ b/lldb/test/API/macosx/expedited-stack-memory/main.c
@@ -10,6 +10,7 @@
// - aggregate locals (a struct and a fixed stack array)
// - a variable-length array (dynamically sized stack storage, like alloca)
// - pointer locals, including a pointer to heap memory
+// - stack-passed parameters
//
// The outer frames (func_d / func_c) also carry locals of these kinds, so that
// walking the whole stack and examining every frame reads the same variety of
@@ -31,11 +32,18 @@ struct Stats {
double mean;
};
+// A large by-value struct. Passed as an argument it does not fit in registers,
+// so it is passed on the stack, above the callee's frame in the caller.
+struct Big {
+ long v[8];
+};
+
// The innermost frame, where we stop. It carries several kind of local: a
// scalar, aggregates (struct + array), pointers (including one into heap
// memory) and a variable-length array. Examining this single frame on a stop
// reads both stack and heap memory.
-static int func_e(int depth) {
+static int func_e(int depth, int a1, int a2, int a3, int a4, int a5, int a6,
+ int a7, int a8, int a9, struct Big big) {
int i = depth + 1;
long l = (long)depth * 1000;
double d = depth + 0.5;
@@ -51,9 +59,10 @@ static int func_e(int depth) {
const char *str = "hello from func_e";
int *self = &i;
g_sink = i + l + (long)d + stats.sum + arr[3] + vla[n - 1] +
- heap[HEAP_COUNT - 1] + str[0] + *self; // break here
+ heap[HEAP_COUNT - 1] + str[0] + *self + a8 + a9 +
+ big.v[7]; // break here
int r = i + (int)l + (int)d + (int)stats.sum + (int)arr[3] + (int)vla[n - 1] +
- (int)heap[HEAP_COUNT - 1] + str[0] + *self;
+ (int)heap[HEAP_COUNT - 1] + str[0] + *self + a8 + a9 + (int)big.v[7];
free(heap);
return r;
}
@@ -62,7 +71,11 @@ static int func_e(int depth) {
static int func_d(int x) {
struct Stats stats = {.sum = x, .min = x - 1, .max = x + 1, .mean = x + 0.5};
long arr[4] = {x, x + 1, x + 2, x + 3};
- int r = func_e(x);
+ struct Big big;
+ for (int k = 0; k < 8; ++k)
+ big.v[k] = 100 + k;
+ int r = func_e(x, x + 1, x + 2, x + 3, x + 4, x + 5, x + 6, x + 7, x + 8,
+ x + 9, big);
return r + (int)stats.sum + (int)arr[3];
}
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..bc4ba6dc10f37 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -2733,6 +2733,90 @@ static void ReadStackMemory(nub_process_t pid, nub_thread_t tid,
}
}
+// The total stack-memory budget we expedite for frame 0, in bytes. Sized to
+// cover the common case (locals, spilled register arguments, and stack-passed
+// parameters) while bounding the per-frame cost.
+static const nub_size_t k_expedite_stack_window = 1024;
+
+// Bytes reserved for the above-fp "stack-passed parameters" window,
+// [fp + 2*ptr_size, fp + 2*ptr_size + k_expedite_stack_arg_size).
+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");
+
+// A single contiguous chunk of expedited memory.
+struct ExpeditedMemory {
+ nub_addr_t addr;
+ std::vector<uint8_t> bytes;
+};
+
+// 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,
+ nub_size_t ptr_size) {
+ static const uint64_t k_expedite_max_frame_size = 8 * 1024 * 1024; // 8 MB
+
+ if (sp == 0 || fp == 0 || fp <= sp)
+ return false;
+ if (fp - sp > k_expedite_max_frame_size)
+ return false;
+
+ const nub_size_t rec = 2 * ptr_size;
+ uint8_t bytes[2 * sizeof(uint64_t)];
+ if (DNBProcessMemoryRead(pid, fp, rec, bytes) != rec)
+ return false;
+
+ uint64_t prev_fp =
+ (ptr_size == 4) ? ((uint32_t *)bytes)[0] : ((uint64_t *)bytes)[0];
+ // The saved previous fp must chain upward (stack grows down).
+ return prev_fp > fp;
+}
+
+// Read the innermost frame's stack memory.
+static std::vector<ExpeditedMemory> ReadFrameZeroStackMemory(nub_process_t pid,
+ nub_thread_t tid) {
+ std::vector<ExpeditedMemory> chunks;
+ DNBRegisterValue sp_value;
+ DNBRegisterValue fp_value;
+ if (!DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
+ GENERIC_REGNUM_SP, &sp_value) ||
+ !DNBThreadGetRegisterValueByID(pid, tid, REGISTER_SET_GENERIC,
+ GENERIC_REGNUM_FP, &fp_value))
+ return chunks;
+
+ 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;
+
+ auto read_range = [&](uint64_t start, uint64_t length) {
+ if (length == 0)
+ return;
+ std::vector<uint8_t> buf(length);
+ if (DNBProcessMemoryRead(pid, start, length, buf.data()) != length)
+ return;
+ chunks.push_back({start, std::move(buf)});
+ };
+
+ if (FrameZeroFPLooksValid(pid, tid, sp, fp, ptr_size)) {
+ // above-fp: stack-passed params, skipping the already-expedited frame
+ // record.
+ read_range(fp + 2 * ptr_size, k_expedite_stack_arg_size);
+
+ // below-fp: locals + spilled register args, clamped at $sp so a small frame
+ // reads only [sp, fp).
+ uint64_t below = std::min<uint64_t>(fp - sp, k_expedite_stack_window -
+ k_expedite_stack_arg_size);
+ read_range(fp - below, below);
+ return chunks;
+ }
+
+ // Frameless / cannot validate $fp: expedite a single window anchored at $sp.
+ if (sp != 0)
+ read_range(sp, k_expedite_stack_window);
+ return chunks;
+}
+
rnb_err_t RNBRemote::SendStopReplyPacketForThread(nub_thread_t tid) {
const nub_process_t pid = m_ctx.ProcessID();
if (pid == INVALID_NUB_PROCESS)
@@ -5866,9 +5950,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 +5962,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);
+
+ 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;
More information about the llvm-branch-commits
mailing list