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.
72 lines
3.9 KiB
Markdown
72 lines
3.9 KiB
Markdown
# 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.
|