Files
llmdbg/FIELD_NOTES.md
llmdbg-dev f7a2cd0600 docs: field notes from smarm context-switch session (v0.8)
Friction-and-wishlist from one real profiling/verification session, not a
defect report. Highlights: demangling and trace({over,capture}) worked well;
pc-range checks are treacherous when helpers sit adjacent to the function
under test (symbol extent, not address, is the missing primitive); no
float/vector register access forced the XMM verification out to objdump.

Wishlist, JS-expressible first: symStart/symSize (or inFn), countTo, hexdump
— candidates for a v0.9 prelude with no Rust changes. xmm reads and
prologue-aware breakpoints need native support.
2026-05-28 15:19:43 +00:00

3.9 KiB

Field notes — smarm context-switch session (v0.8)

Notes from one real session using llmdbg v0.8 to profile and verify smarm's naked-asm context switch (the TLS-accessor perf question and the XMM-not-saved correctness question). Recorded as friction-and-wishlist, not a defect report — nothing here blocked the work; every rough edge had a workaround.

What worked

  • Demangling. addr("switch_to_scheduler") resolving directly meant zero nm | grep for mangled names whose hash changes every rebuild. Biggest single quality-of-life win.
  • trace({over, range, capture}). The capture closure returning a structured record per step ({pc, insn, len, is_call}) and letting the caller format it is the right shape — produced a clean 16-line shim trace in one pass.
  • next() vs step(). The distinction the docs emphasise is exactly the axis this task needed: stepping into the accessors (164 insns of debug-build LocalKey ceremony) vs over them is the difference between readable and unreadable, and step() through the stack-swapping ret is what let me walk the switch itself.
  • The worked example (examples/smarm_ctx_switch_next.js) was close enough to the real task to adapt directly. Good investment.

What bit me

  • Range checks on state.pc are treacherous when helpers sit adjacent to the function under test. set_actor_sp lived at sched+0x50, inside my +0x60 window, so a "stop when pc returns to the shim" loop terminated the instant it entered the accessor. Cost me two attempts before I abandoned range-based exit detection and dumped a flat step() stream to count by eye. The missing primitive is symbol extent (start ≤ pc < start+size), not symbol address.
  • The push rbx consumed-by-breakpoint quirk is correctly documented, but I had to re-apply "the first observed instruction is the second one" mentally at every session. A documented footgun is still a footgun.
  • No float/vector register access. The XMM-not-saved verification had to leave llmdbg entirely and fall back to objdump to confirm the compiler spills live XMM around the switch call. For a tool whose reason for existing is x86-64 context-switch correctness, watching xmm0..15 across a switch feels squarely in scope — it would have turned a static inference into a dynamic proof (XMM dead at switch entry, live again after reload).

Wishlist (rough priority order)

  1. Symbol-extent queriessymStart(pc) / symSize(pc), or an inFn(pc, "name") membership test. Fixes the range-check footgun directly. This is the highest-value add for asm work, where "am I still inside this function" is a constant question. Pure-JS-expressible if the native sym layer exposes extents.
  2. countTo(stop, {over}) — step until an address/predicate and return just the instruction count, instead of hand-rolling let n=0; while(...){ step(); n++ } and getting the boundary wrong. Counting per-call instruction cost was the core measurement of this task and had no first-class support. Pure-JS prelude helper.
  3. XMM/SSE register readsstate.xmm[i] or reg("xmm0"). Needs Rust-side support (ptrace GETFPREGS/xsave). In scope for this tool's purpose.
  4. Prologue-aware breakpoints — a mode (or bp.setAfterPrologue()) that removes the "first instruction eaten" caveat rather than documenting around it.
  5. hexdump(addr, n) — peek at raw memory, e.g. a freshly-built initial actor stack from init_actor_stack. Pure-JS over a readMem primitive. Nice-to-have; didn't strictly need it this round.

Suggested next step

Items 1, 2, and 5 are pure-JS and fit the SPEC ethos ("everything expressible in JS lives in JS") — they belong in src/prelude.js and would be a clean v0.9 with no Rust changes (assuming sym extents and a readMem primitive are reachable from JS). Items 3 and 4 need native support and are larger.