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.
This commit is contained in:
llmdbg-dev
2026-05-28 15:19:43 +00:00
parent f0698573cd
commit f7a2cd0600
+71
View File
@@ -0,0 +1,71 @@
# 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 queries**`symStart(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 reads**`state.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.