Phase 2 ended with LLVM deleted and every number pointing the right way. Phase 3 — the seven pure-logic classes in LinuxTracing — was supposed to be the easy encore: excellent test coverage, no I/O, algorithms with the C++ as their own specification. It is instead the phase where gate 4 finally fired, and this post is the write-up of a stop.
What got ported, and how well
Two classes, chosen from opposite ends of the FFI-shape spectrum on purpose.
PerfEventQueue — the heap-of-queues timestamp merge. PerfEvent is a large C++ variant that must never cross the boundary, so the Rust side owns only (stream, timestamp, handle) keys and the facade keeps events in a stable-reference slab the handles index. The hand-written heap was replicated comparison for comparison — that code is the spec — with one documented strengthening: equal-timestamp unordered events pop FIFO where std::priority_queue leaves the order unspecified.
ThreadStateManager — the per-thread sched state machine. Pure integers in, one small struct out; no slab, no handles, the cheapest FFI shape there is. Every quirk reproduced deliberately: stale initial states replaced by older tracepoints, wakeups of runnable threads disregarded, runnable adjusted to running on switch-out, no callstack status on a switch-out slice. The crate returns warnings as data and the facade owns the ORBIT_ERROR strings, so even the log lines stay byte-identical.
Correctness held everywhere it has all along: all three backend modes pass the unedited test suites, the sabotage checks fail exactly the targets they should, and every benchmark run of both backends produced identical checksums — the implementations pop the same events and emit the same slices, always.
And then the benchmarks
| Class | FFI shape | cpp | rust | |
|---|---|---|---|---|
PerfEventQueue |
slab + handles, 3 calls/event | 63–72 ns/event | 77–79 ns/event | ~15–20% slower |
ThreadStateManager |
integers only, 1 call/event | 11.8–12.8 ns/transition | 30.3–33.3 ns/transition | ~2.4× slower |
Two optimisation rounds went into each before those numbers settled: an FxHash-style hasher replacing SipHash (thread ids do not need DoS resistance), entry-API single-hash pushes, O(1) stream removal, a C++-side size counter making HasEvent free, a cached top handle. They moved the merge queue from 27% slower to 20% and the state machine from 3× to 2.4×. The floor underneath is not optimisable from inside:
The verdict, and why it is the plan working
The plan wrote this gate down before any of Phase 3 existed: "correctness first, performance later is how a port quietly becomes a regression" — and, about this exact phase: "this is the one place where a Rust port could plausibly be slower — an FFI call per event is not free." The original analysis called for a measured experiment before any commitment. This was it.
- No defaults flip. Both classes keep
cppas the default; the Rust stays in tree, validated, behindORBIT_PERF_MERGE_BACKENDandORBIT_THREAD_STATES_BACKEND. - The five remaining classes are not ported. Porting them would produce five more validated-but-never-default dual implementations: maintenance cost with no path to deletion, and — unlike Phase 2 — no dependency win, because Phase 3 deletes nothing external.
- The batching escape hatch is closed by semantics, not laziness: prefetching pops in bulk would remove the calls, but a push may legally deliver an event older than anything prefetched, and popped events cannot be pushed back.
Two things, both named in the verdict file. Cross-language LTO — clang with linker-plugin LTO can inline Rust into C++ callers, which would erase most of the fixed toll; the CI toolchain is gcc today. Or moving the language boundary to where the original analysis wanted it all along: the wire format. A Rust collector speaking the protocol has no per-event FFI, because there is no C++ on the other side of the call. Rust islands inside a C++ pipeline were always the second-best shape; now there is a number on exactly how much second-best costs.
The port, closed out
| Phase | Outcome |
|---|---|
1 — ParseMaps | Ported, default Rust, 1.75× faster, zero dependencies |
2 — ObjectUtils | Fully ported, LLVM deleted, cold build −34%, verified against 16.3M symbols |
3 — LinuxTracing pure logic | Two classes ported and validated; gate 4 measured both slower; defaults stay C++; five classes not ported, by the numbers |
A log that only ever reported green would be advertising. The gates were written down when everything was going well precisely so that this post could be short, specific, and final: the merge queue and the state machine work, they are provably equivalent to the C++, and they are slower, so the C++ keeps the job. Six posts in, that is the first sentence of its kind — and the instruments that produced it are the same ones that justified deleting LLVM.