Since the original analysis, UprobesReturnAddressManager and LeafFunctionCallManager have been the two classes every phase walked around: welded to libunwindstack — MapInfo, Maps, the unwinder itself — and a dependency boundary is not something a performance decision can overrule. The only way through was to replace the unwinder. So Phase 5 replaces the unwinder.
5a — two DWARF engines, one set of samples
orbit-unwind is framehop — the unwinder built for samply, a shipping all-Rust profiler — plus the object crate for the CFI sections, loading modules from a /proc/<pid>/maps snapshot. The differential is the strictest yet: a child runs a 28-deep non-inlined recursion whose leaves put real time into libc (memset, qsort through a callback, snprintf); live samples carry full registers and 64,000-byte stack copies; and each sample goes, with the same registers, stack bytes, and maps, to libunwindstack and to framehop. Frames compared position by position.
The first run diverged on every single sample — by exactly one, on every frame past the first. libunwindstack reports return addresses minus one: the address inside the call instruction, which is what symbolization wants at function boundaries. framehop reports raw return addresses and keeps the adjusted form behind address_for_lookup(). The crate adopts the adjusted convention, because that is what Orbit's consumers are built on. This is precisely the class of off-by-one that ships silently and mis-attributes call sites — and precisely what a 12,000-sample differential exists to catch.
After the fix: 11,890 of 12,000 samples frame-for-frame identical, zero divergences — and the 110 one-sided failures all lean the same way: libunwindstack giving up ("Invalid Elf at 0") on samples framehop unwinds fine. The Rust unwinder never failed where the C++ succeeded. Sabotage: shifting every module's load bias by 0x1000 diverged 1,433 of 1,500 samples.
One hurdle for the log: the crate's first self-test used 8 KB stack dumps and 80% of unwinds "failed" — a full walk to the thread root needs the whole used stack in the copy. Orbit's 65,000-byte default is not generosity; it is a requirement.
5b — a weld that was one predicate
UprobesReturnAddressManager — the bookkeeping that lets unwinding continue past uretprobe-hijacked return addresses — turned out to touch libunwindstack in exactly one place: "is this instruction pointer in the [uprobes] map?" The Rust twin takes that as a closure; the maps lookup stays where the maps object lives. Everything else is pure logic, ported quirk for quirk: the reverse-order stack patching that resolves tail-call duplicates to the original return address, and the three discard conditions for callchains the bookkeeping cannot explain.
The unedited 19-test C++ suite now runs three ways on the shared ORBIT_TRACING_STATE_BACKEND switch, default rust, with both-mode comparing patched outputs byte for byte. The FFI contributed the hurdle this time: empty test stacks arrive as (NULL, 0) — an empty std::vector's data() is null — which Rust's slice::from_raw_parts_mut rejects even at length zero. The C++ never noticed, because its early-return happens before the memcpy.
5c — a weld that was three questions
LeafFunctionCallManager fixes frame-pointer callchains whose leaf function keeps no frame pointer. Its weld is richer: a CFI query (is the CFA rule $rbp + 16 at this pc?), one unwinding step, an executable-map check. But the decision tree around those three questions — the frame-pointer analysis, the too-small-versus-error distinction, the caller patch-in — is pure logic, and that is what moved to Rust. The engine stays behind three callbacks: today the facade wires them to libunwindstack, mocks included; in the Rust collector they wire to orbit-unwind, whose answers 5a already proved.
The both-mode machinery earned a refinement here. The tests mock the unwinder with strict one-call-per-query expectations, and running two backends means asking the engine everything twice. The facade's both-mode now runs both trees against small memoizing proxies, so each unique query reaches the collaborator exactly once and both backends decide from identical answers — which is not a concession to the mocks, but what makes both-mode a controlled experiment in the first place.
What this phase deliberately did not do
libunwindstack is still linked. The welded classes' logic defaults to Rust and their engine calls route through the facade; the engine swap happens where it belongs — in the Rust collector, where orbit-unwind is the only unwinder — and the deletion of libunwindstack is Phase 8 work, after the whole-capture differential (4d, a sudo run) has judged the assembled pipeline. The boundary the original analysis drew is now crossed everywhere it can be crossed without root; what remains is assembly, not porting.