Architecture
Shape
Rust reads the kernel and computes every rate and total. Svelte draws, filters, sorts and arranges. The boundary is a serialised snapshot, pushed once per interval.
src-tauri/src/
sys/ one module per kernel interface
process.rs sysctl(KERN_PROC_ALL), per-process counters
ffi.rs kinfo_proc field offsets, with a runtime guard
host.rs host CPU ticks, VM statistics, interface traffic
cpu.rs per-logical-processor ticks, P/E identification
threads.rs per-thread counters for one process
gpu.rs IOAccelerator devices and clients
power.rs AppleSmartBattery and the adapter
network.rs per-process traffic, from nettop on its own thread
reports.rs KERN_PROCARGS2, and Apple's tools run with a deadline
applications.rs Dock presence from Info.plist, cached
diagnostics.rs open files, memory regions, Mach ports, resource counters
iokit.rs the IOKit calls the GPU and CPU collectors share
corefoundation.rs the CF readers those need, each type-checked
machine.rs static facts, read once
timebase.rs Mach ticks to nanoseconds, with the Rosetta correction
sample.rs successive readings turned into rates and histories
tree.rs process ancestry and subtree totals
model.rs the shapes the interface receives
diagnostics.rs the per-process pages, collected on request
menubar.rs the status item, its pins, and the popover window
license.rs offline key verification and the reminder's timing
update.rs the one network request, and version comparison
lib.rs commands, and the collector thread
src/
lib/
state.svelte.ts the single source the interface reads from
query.ts filtering, sorting, column layout per metric
tree.ts building and collapsing the display forest
export.ts CSV and JSON
format.ts every formatter; missing values render as an em dash
*.svelte the views
Data flow
A collector thread samples on its own cadence, sequentially: a slow sample delays the next one rather than overlapping with it, so every rate describes measured elapsed time.
collector thread ──► Sampler::sample() ──► Snapshot ──► emit("snapshot") ──► state.svelte.ts
│ │
├── sys::* readers ├── query.ts filter, sort
├── rate computation ├── tree.ts forest, collapse
├── tree::annotate ancestry and Σ totals └── views
└── history, 15 minutes
Histories live in the backend as well as the frontend. The frontend fetches the full
series once at startup through the history command and appends from each snapshot, so a
reload does not start from an empty chart.
Two things do not ride the snapshot stream:
- Per-thread collection is opt-in, runs at most every two seconds, and is switched on
and off with
watch_threads. A snapshot carries afreshflag so the intervening turns do not append the same reading twice. - Diagnostics are request-and-response through the
diagnosticscommand. Only the page being looked at is collected, and each carries its own capture time so switching pages never presents an older snapshot as a new one.
Commands
| Command | Does |
|---|---|
machine |
static facts: chip, processors, memory, performance levels |
history |
the retained series, once at startup |
set_interval, set_paused, refresh_now |
sampling cadence |
watch_threads |
starts or stops per-thread collection for one process |
diagnostics |
collects one diagnostics page |
terminate |
sends SIGTERM or SIGKILL after re-checking identity |
save_text |
opens the save panel and writes what the interface built |
set_menu_bar, set_menu_bar_metric |
the status item |
pin_process, unpin_process, pinned, hide_popover |
menu bar pins and their popover |
license_status, activate_license, deactivate_license, license_reminded |
the licence, checked on this machine |
update_status, set_update_checks, skip_version, check_updates |
the update check, off until asked for |
report, cancel_report |
one diagnostics report, and stopping it |
The menu bar popover is the same bundle in a second window, mounted on #popover. It
subscribes to the same snapshot stream rather than sampling anything of its own.
The webview has no filesystem access of its own. Export hands over text and a suggested name; the only path ever written is the one the save panel returned.
Reading structures libc does not carry
Three kernel structures Starf needs are absent from the libc crate: kinfo_proc, the
per-descriptor proc_pidfdinfo structures, and proc_regionwithpathinfo. Each is read by
byte offset, and the offsets were produced by compiling offsetof against the installed
SDK headers rather than copied from memory or a blog post. They are named constants with
the structure and field in the name, so a future check has something to verify against.
kinfo_proc additionally carries a runtime guard: verify_layout locates Starf's own
record at every enumeration and compares PID, parent, owner and start time against values
already known. A failure disables the process list with an explanation instead of printing
whatever the bytes contain.
Performance
Measured on an M4 Max with about 1,250 processes:
| Collector | Cost per sample |
|---|---|
| Per-processor ticks | 0.03 ms |
| Open files, one process | 0.3 ms |
| Battery and adapter | 0.44 ms |
| GPU devices and 46 clients | 3.0 ms |
| Memory map, one process (1,300 mappings) | 10.6 ms |
The interface keeps two large payloads in $state.raw rather than deep reactive state —
the snapshot and the histories are replaced wholesale each second, and proxying a thousand
process objects would cost more than the sampling does. The process table renders only the
rows on screen.
Subtree totals are sent only for the processes that have descendants — 68 of 1,249 on this machine. A leaf's subtree is itself, which the interface synthesises.
Why this stack
GPUI was considered first and rejected for a concrete reason: it compiles its shaders with
xcrun metal at build time, and this machine has only the Command Line Tools, so nothing
would build until a full Xcode was installed. On top of that, GPUI has no table or chart
primitives, and this is a table-and-chart application.
Tauri 2 with Svelte 5 builds with what is already here, and puts the interesting work — the syscalls, the rate arithmetic, the aggregation — in Rust where it is testable, while the dense tables and charts are drawn by the technology best at them.