Orbit is a native application profiler: sampling, dynamic instrumentation, scheduling and GPU tracing, with a Qt desktop UI and, more recently, a Rust/WebAssembly live viewer. The viewer moved to Rust first. These posts are about whether — and how — the capture backend should follow, written as it happens rather than afterwards.
The rules this log holds itself to:
- No claim without a committed number. Every figure comes from a script in
scripts/whose output is checked in, so any of it can be re-run and disputed. - The C++ test files are never edited. They are the specification. A port that changes its own exam has not passed anything.
- Numbers that get worse get reported. A strangler port makes the tree bigger before it makes it smaller, and pretending otherwise would make the good numbers worth less.
- Every hurdle gets written down — including the ones that were my own mistakes, which so far is most of them.
Posts
One Timeline, Many Machines
Post 22, built: a remote joins a hub over one WebSocket, the hub packs the machine into the top byte of every pid, remaps the strings and fits offset and rate through the fastest round trips so two machines' events share an axis; Record on the hub records everywhere. Diagrams of the link, the id, the clock fit and the ingest path; what was measured and what is deferred.
24 · cross-platformThe Windows Backend
Orbit returns to Windows in Rust: one ETW kernel session for the scheduler, thread states and 1 kHz sampling, decoded from raw MOF bytes rather than a schema parser; manual scopes in a file-backed ring; Frida hooks over a named pipe; symbols from PDBs and export tables. The C++ tracer's ContextSwitchManager tests are the oracle. Built and run on the OS, with the gaps stated.
23 · toolingA Profiler an Agent Can Drive
orbit-service mcp lets an agent list processes, attach, arm Frida hooks, capture, and read hotspots, call stacks, and per-function timings. Twelve tools, stdio or loopback HTTP. The Python optimize-and-patch loop was not merged: it edits source, and this server does not.
Every Machine in One Timeline
A proposal for one viewer watching any number of machines at once — reached over the network, over SSH with nothing pre-installed, or by the remote dialing in. Most of it is plumbing; the real problem is putting two hosts' clocks on one axis, and the plan says what precision it will and will not promise. A design for review; nothing here is implemented yet.
21 · instrumentationThe First Few Bytes
One loop produces three scopes for one call; another accepted hook crashes. Follow the entry patch, relocated instructions and return detour through Orbit's native Frida integration, with a source-level comparison to hijk and the limits across Linux, macOS and Windows.
20 · instrumentationOne Lost Return, Ten Thousand Ghosts
A hooked function kept appearing nested inside itself, tens of thousands of times. Not a kernel that duplicates hits on migration, as the lore says, nor one that loses them in bulk, as a first bad reading of the data claimed: one return lost per thread at the instant of arming, turned into a landslide by pairing entries to returns by count. With two real captures embedded.
19 · testingCall for Call
A stress test for dynamic instrumentation: three hooked functions in a known tree, called a known number of times on a known number of threads, and a checker that holds the capture to it — no extra scopes, no outer under anything, and every missing scope matched by a healing count on the status line. Written after the ghost scopes turned out to be lost hits, not duplicates.
18 · formatsBytes on the Wire, Bytes on Disk
Why the live stream packs a 32-byte record into 11 with a delta and three varints, why deflate on top is a toggle and not the default, why ssh -C gets close in bytes and loses by an order of magnitude in CPU, and why the saved capture is Parquet with no codec at all — every choice with the number that made it.
Where the Frame Went
Measure first, fix what the measurement names, measure again: a 19× faster report tree, 49× faster frame symbolization, a 26× faster track layout, a 36× faster WebSocket drain — and a bug older than the port that had been rebuilding the timeline on every still frame.
16 · renderer internalsHow Orbit Draws a Million Scopes
A line-by-line reading of the three WGSL shaders behind the live timeline, for someone who has never opened a shader: normalised device coordinates, 48 bytes per rectangle, a quad deliberately bigger than the thing it draws, and how you round a corner with no rounded rectangle.
15 · manual instrumentationA Ring Per Core
A ten-nanosecond write path for manual instrumentation: why per-core single-producer rings are unsound in userspace, what rseq is and why it was not used, and the ordering bug that only appeared when someone asked whether a migrating thread still merges correctly.
14 · dynamic instrumentationThe Hook That Would Not Arm
Hooking functions from the web UI: the file-offset arithmetic a uprobe needs, why probes are opened per task instead of per CPU, and the capability check that stopped the last step. Reported with the gap left in.
13 · ease of buildFourteen Times Faster
The softest of the four original goals, measured: the C++ service builds cold in 148.5 s, the Rust one in 10.7 s. Why the gap is that wide, what the number does not say, and why eleven seconds is a different activity from two and a half minutes.
12 · the viewerThirty-Two Workers
The WASM viewer spawned one web worker per hardware thread because that is what navigator.hardwareConcurrency returned, not because anyone chose it. The case for turning it down, the case against, a new default of eight, and a ?threads=N override so the right number can be measured instead of argued.
11 · phase 7, milestones a–bDead Simple
The pod wire format the project asked for at the start: a tag byte and fixed fields, zero dependencies, 4.79x faster to parse than protobuf at 1.69x the bytes — an honest CPU-for-bandwidth trade. Then the first all-Rust vertical slice, sched records to pod SchedulingSlice events with no FFI on the path.
10 · phase 6, milestones a–dInto the Tracee
User-space instrumentation: the ptrace substrate (verified by a tracee confirming its own overwritten byte), syscall injection via the two-byte 0f 05, trampoline placement within 32-bit reach, and the hardest single file — instruction relocation, byte-for-byte identical with the C++ across 888,154 real instructions on iced-x86.
09 · phase 5, milestones a–cThe Weld Comes Apart
framehop replaces libunwindstack: 12,000 live samples unwound by both engines, zero frame divergences after the differential caught a shipping-grade off-by-one on its first run. Then the two welded classes fall — one weld was a single predicate, the other three questions — and their logic runs on Rust by default.
08 · phase 4, milestones a–cMoving the Boundary
Instead of batching FFI calls, the boundary moves to the wire: Rust now owns the record layouts (19 structs, every field offset compared), the record parsing (7,230 live kernel records, zero mismatches), perf_event_open and the ring buffer (600/600 against a C++ watcher), and the event loop — the first FFI-free path from kernel bytes to ordered records.
07 · phase 3, reopenedOverruled
The project owner overruled the performance gate: port everything portable, accept the measured toll, fix it later. Five classes now run on Rust by default, the cost is priced at ~2% of a core worst case, and the two exits are named.
06 · phase 3The Gate Says No
Two LinuxTracing classes ported, provably equivalent — and measurably slower, because a per-event FFI call cannot inline. Phase 3 stops on its own gate: defaults stay C++, five classes stay unported, and the verdict names what could reopen it.
05 · stage 2hLLVM Is Gone
Post 04 called the PDB reader blocked. It wasn't — the CodeView renderer is two hundred lines, and once it existed line info fell too. Cold build −34%, ObjectUtils −2,191 lines, zero LLVM dependents, and an accounting of what deleting the comparison cost.
Two Predicates, One Header
CoffFile fully ported, and a one-symbol mismatch that traced to picking the wrong "is this a function" predicate out of the same LLVM header. Then the PDB reader turned out to be blocked on LLVM's CodeView type-name renderer — so LLVM stays, and now we know exactly why.
03 · unwind ranges, line infoThe Stage That Didn't Ship
16.3 million symbols compared with zero structural mismatches. Then line info passed every test in the repository and failed against real binaries four times — and the fourth one is why it still delegates to C++.
02 · ObjectUtils944,291 Symbols
ELF parsing to the object crate: 20 tests green on the first run and up to 50× faster. Then the demangler failed twice, the mainstream Rust crate turned out to have a correctness bug, and the winner was not written in Rust.
The First 73 Lines
Porting the smallest real function in the tree to find out whether the machinery works. 1.75× faster, zero dependencies, and two bugs found — one of them in my own test. Eight hurdles, including why /proc paths cannot be &str.
Background reading
Should OrbitService Be Rust?
The case both ways, module by module, including the parts that should stay C++ permanently and the reason the standard memory-safety pitch is weaker here than advertised.
PlanTwo Backends, One Test Suite
The staged plan: how one factory runs three backends, the four verification gates, and why deleting LLVM is a bounded outcome rather than an aspiration.
Prior workOrbit on Bazel
The build-system port that made all of this possible, including the SSL problem that took three attempts.
The evidence trail
Two scripts produce everything the posts cite, and their output is committed so a reader can diff phase to phase rather than take a summary on trust.
| Command | Produces | Records |
|---|---|---|
| scripts/port_metrics.sh --bazel | docs/blog/metrics/*.json | Lines of code per module, Bazel dependency and external-repository counts, test-case counts, uses of hand-rolled std equivalents, LLVM's dependent count |
| scripts/bench_parse_maps.sh | docs/blog/metrics/*-bench-*.txt | A/B timings: the same binary run under each backend, alternating rounds, median reported |
The snapshots so far: phase-0-baseline.json taken before any Rust was written, and phase-1-parse-maps.json after the first port.