Measurement
What a number in Starf means, and when there is deliberately no number.
Unavailable is not zero
macOS shares a process's detailed counters only when it chooses to. On an ordinary desktop about a quarter of processes deny them — on the machine this was built on, 341 of 1,245. Two calls can be refused independently, so a process carries two flags:
| Flag | Set when | Guards |
|---|---|---|
accessible |
proc_pidinfo(PROC_PIDTASKINFO) answered |
CPU, CPU time, memory, threads |
ioAccessible |
proc_pid_rusage(RUSAGE_INFO_V4) answered |
bytes read and written, wake-ups, physical footprint |
A counter behind a flag that is false was never measured. It renders as —, exports as
an empty CSV cell and a JSON null, and sorts below every real value in both directions —
because an unavailable counter is not a small one.
A measured zero is a measurement. It renders as 0, exports as 0, and sorts with the
other numbers.
Refused is not gone
Two different things stop Starf reading a process, and the kernel distinguishes them:
| Kernel answer | Meaning | Reported as |
|---|---|---|
EPERM |
the process exists; macOS will not describe it | "macOS does not share this process's details." |
ESRCH |
no such process | "This process has exited." |
Conflating them would be the easiest mistake in the codebase to make, and it was made
once: the first per-thread probe against WindowServer reported it as exited when it was
running perfectly well. sys::process::identity now returns Same, Gone or Denied,
and every message downstream says which.
Identity
A PID on its own is not an identity — macOS reuses them. Everything that has to survive across samples is keyed by PID and start time together:
- rate baselines, so a new process never inherits the previous holder's counters
- GPU observed time
- tree expansion state, so a recycled PID does not arrive collapsed
- per-thread baselines
Before Starf sends a signal to a process, or reads a diagnostics page, it re-checks that the PID still names the same process, with the same start time and owner. Diagnostics checks again after the read, so a process that exits mid-walk cannot have its successor's files attributed to it.
Rates
Every rate is a difference between two readings divided by the time actually measured between them, never by the interval that was asked for.
Three rules apply everywhere:
- A first reading has no rate. It establishes a baseline and reports nothing. A
process, processor, thread or GPU client seen for the first time shows
—, not0. - A counter that went backwards is a reset. It reports nothing rather than a negative rate or a wrapped-around spike.
- A gap too long to describe one interval is not differenced. Per-thread collection refuses a gap over thirty seconds, which is what a paused monitor or a sleeping machine produces.
Host CPU tick counters are 32 bits and wrap. The difference is taken with wrapping subtraction, which is the real elapsed count as long as fewer than 2³² ticks passed.
Scales
Two CPU scales exist, and mixing them up would make every number wrong by the processor count:
| Where | Scale | On a 16-processor machine |
|---|---|---|
| Process list, per-thread charts | 100% is one logical processor | a process on four processors reads 400% |
| CPU card, breakdown, per-processor tiles | 100% is the whole machine | user + system + idle is always 100 |
A per-processor tile is 0–100% of that one processor, whatever the machine's count.
GPU percentages are driver-reported GPU time over elapsed time. Overlapping work can exceed 100%.
Totals that do not add up, and why that is correct
Host CPU against the process column. The host counters include kernel work and the processes that deny their own counters. The visible process column does not sum to the host total, and forcing it to would mean inventing the difference.
Subtree totals against each other. A parent's Σ includes its children's, so adding the visible rows double-counts. The overview charts are what measure the machine.
Memory against the machine. Process memory counters are summed as reported. Shared mappings overlap between processes, so a subtree's memory is not unique physical RAM and not what quitting it would free.
Resident against virtual. Resident pages are part of a virtual mapping. The two are selectable measures, never stacked.
Bounds and coverage
A subtree total tracks how many of its members reported the counter:
| Shown | Meaning |
|---|---|
| a number | every member reported; this is the total |
≥ and a number |
some member did not report, or the sum reached an arithmetic limit; this is a lower bound |
— |
no member reported |
Above 2⁵³ a f64 no longer represents every integer, so a sum past that point is flagged
as a bound rather than passed off as exact. No real counter comes close; the guard exists
so a corrupt one cannot slip through.
Retention
Histories keep fifteen minutes. Per-processor and per-thread series keep at most 901 points each under a shared budget of 131,072 points, so a machine with very many processors keeps a shorter history rather than an unbounded one. An unavailable reading is stored as a gap, and the chart breaks its line there instead of drawing through time it did not measure.