Service latency and per-update collection costs
Waybar requests one JSON status update on a fixed schedule. rstat's original Waybar module answered each request by launching a shell script, which called command-line tools including awk, bc and powerprofilesctl.
- Service round trip ~800 ms with the D-Bus request in the update path
powerprofilesctlcrossed process and D-Bus boundaries to retrieve the active power profile, even though Linux exposed the same value through a sysfs file.- Process creation Shell + at least 3 child processes / update
- Each update started external utilities, incurring process creation, executable loading and teardown costs.
- Text interfaces Per-process file reads and parsing
- Linux's process filesystem, procfs, converted kernel counters into text. The command-line utilities then parsed that text and passed more text through the shell.
- State lifetime One shell invocation per update
- The script exited after each update, releasing its file descriptors and buffers. Persistent sampling state required a collector that remained running between updates.
Persistent collection and direct sysfs access
The first revision replaced the one-shot shell module with a long-running Rust collector that retained counters, buffers and previous samples between Waybar updates. The Rust collector removed nearly every external utility, but powerprofilesctl still contacted the system power-profile daemon over D-Bus inside every update.
Reading the active power profile directly from sysfs removed that round trip. The procfs changes then went after the number of files opened per update. The collector had walked the process directories three times, opening files for every process on each walk, so collapsing the walks into one removed most of that work.
Each refresh still enumerated /proc and opened per-process files, and the kernel formatted their counters as text for userspace to parse. Collection cost therefore still grew with the number of live processes.
Scheduler tracepoints and task lifetime tracking
To stop the refresh cost growing with the process count, the next revision moved recurring task accounting into the kernel, as an extended Berkeley Packet Filter (eBPF) program. Each scheduler event tells the program which task it concerns, so the program accumulates that task’s CPU time and snapshots its resident memory (RSS) and I/O counters as the machine runs. A refresh then reads those counters instead of rebuilding them from files.
Four scheduler tracepoints maintain task state as processes fork, run, exit and are freed. Every record is keyed by its process identifier (PID) and kernel start timestamp, so a reused PID can't join two unrelated process lifetimes. A retired record stays in the statistics map until the Rust daemon reads its final counters, so short-lived work still shows up. One procfs scan at startup seeds the tasks that existed before the program attached.
sched_process_fork
sched_switch
sched_process_exit
sched_process_free
Four tracepoints create, update and retire task state.
CPU ← every switch
RSS / I/O ← gated
At most once per task every 10 ms, and exit forces a final snapshot.
stats[PID, start time]
Final counters survive task exit until userspace consumes them.
batch map lookup
reused buffers → Waybar JSON
Batch reads, retained storage and bounded output
Each userspace refresh collects the task records, reads global metrics, aggregates processes and writes the Waybar response. Batch reads cut the system calls, while storage reserved once and small bounded result sets keep allocation and sorting off the refresh path.
- Batch map collection
- Linux's
BPF_MAP_LOOKUP_BATCHcopies many task records into preallocated arrays in one system call. Without it, each record takes its own key-advance and lookup calls. - Retained storage
- The kernel maps have fixed capacities, and userspace reserves its batch arrays and aggregation tables once, then clears them between refreshes. The current and previous snapshots swap roles, sorted by PID and start time so each record finds its predecessor by binary search.
- System files
- The daemon opens its sysfs temperature, frequency, graphics and throttling files once and rereads them with
pread()into fixed buffers, parsing numbers straight from the bytes. - Bounded output
- CPU, memory and I/O leaders go into fixed five-entry arrays, and only those are sorted. The JSON is appended to reused strings, with task labels escaped in place.