Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Licence & updates
Licensing
Updates
Elyra
Collection

Collection

Which kernel interface each figure comes from, and what each one gets wrong if read carelessly. Every call here is unprivileged.

Processes

sysctl(CTL_KERN, KERN_PROC, KERN_PROC_ALL) enumerates. The obvious alternative, proc_listallpids, silently omits the processes that deny access rather than listing them — on this machine it returned 315 of 1,245. The whole point is to list the denied ones, so sysctl it is.

Per-process counters then come from two calls that can be refused independently:

Call Gives
proc_pidinfo(PROC_PIDTASKINFO) CPU time (Mach ticks), resident size, thread count
proc_pid_rusage(RUSAGE_INFO_V4) physical footprint, disk bytes, interrupt wake-ups

proc_pidpath gives the full executable path, from which the display name is taken — p_comm stops at 16 characters, and for a bundled app the bundle name is what a person recognises.

kinfo_proc is not in libc

So sys/ffi.rs addresses the five fields it needs by offset. Those offsets came from compiling offsetof against the installed SDK headers. verify_layout re-checks them at every enumeration by finding Starf's own record and comparing PID, parent, owner and start time against values already known. If a future macOS moves them, the process list reports that it cannot read the table rather than printing whatever the bytes say.

Mach ticks, and Rosetta

proc_taskinfo reports CPU time in Mach absolute ticks, converted with the machine's timebase. Under Rosetta the caller's mach_timebase_info is 1:1 while the counters remain host ticks, so an x86_64 build reads hw.tbfrequency instead. proc_threadinfo, by contrast, reports nanoseconds directly — applying the timebase to those again would inflate them by roughly forty.

Host

Figure Source
CPU ticks host_statistics(HOST_CPU_LOAD_INFO); nice time counts as user time
Memory pages host_statistics64(HOST_VM_INFO64)
Physical memory, swap, pressure sysctlbyname
Interface traffic sysctl(NET_RT_IFLIST2), non-loopback only

NET_RT_IFLIST2 carries 64-bit counters. getifaddrs would wrap at 32 bits.

Memory used is active + wired + compressed. Inactive and speculative pages are shown separately as cached, because they are reclaimable.

Logical processors

host_processor_info(PROCESSOR_CPU_LOAD_INFO) gives per-processor user, nice, system and idle ticks; PROCESSOR_BASIC_INFO gives slot numbers. A reading is only compared with one taken under the same identification — without slot numbers two readings may describe different processors, and the difference between them would mean nothing.

Performance and efficiency processors are read, not guessed

sysctl hw.perflevel* gives the counts. The mapping from a slot to a kind comes from explicit logical-cpu-id and cluster-type properties on the IODeviceTree CPU nodes:

cpu0–cpu3    cluster-type "E"    Efficiency
cpu4–cpu15   cluster-type "P"    Performance

which agrees with hw.perflevel0 (12 performance) and hw.perflevel1 (4 efficiency). Array order is never used as a source. A slot claimed by two nodes is dropped rather than guessed — an ambiguous answer is worse than none, and those processors keep the neutral label "Logical processor". Intel SMT threads appear as logical processors; no physical-core affinity is implied.

Threads

PROC_PIDLISTTHREADIDS (flavour 28, from XNU's private proc_info_private.h) gives unique kernel thread IDs. Where a kernel does not know it, PROC_PIDLISTTHREADS gives cthread handles the kernel may reuse, and the interface says so — a thread that exits can then be replaced by an unrelated one under the same label. Per-thread counters come from PROC_PIDTHREADID64INFO or PROC_PIDTHREADINFO to match.

Collection is opt-in per process, runs at most every two seconds on the existing sampling turn, and creates no timer of its own.

GPU

IOServiceMatching("IOAccelerator") enumerates devices. Each entry's PerformanceStatistics dictionary carries the counters:

Key Becomes
Device Utilization %, or GPU Activity(%) device utilisation
Renderer Utilization % renderer
Tiler Utilization % tiler
In use system memory, Alloc system memory driver memory

Per-process GPU time comes from the accelerator's client entries: IOUserClientCreator reads "pid 399, WindowServer", and AppUsage[].accumulatedGPUTime counts nanoseconds.

Metal is not linked. The device name comes from the driver's own model property — "Apple M4 Max" rather than "AGXAcceleratorG16X" — falling back up the registry and then to the class name. Metal would add little beyond a second source for the same string.

Registry properties are driver-defined, not a documented cross-vendor contract, so every GPU counter is optional. A driver that publishes none still lists its device, with everywhere.

Observed, not lifetime

The counters are lifetime totals, but Starf reports only time observed during this session. A client seen for the first time, one whose counters reset or changed shape, and one belonging to a reused PID each establish a new baseline instead of contributing their history as a spike. Work by clients that both appear and disappear between two samples cannot be recovered. A partially readable counter array is discarded whole rather than summed into a number that would understate the process.

Power

AppleSmartBattery in the IORegistry: charge, cycle count, charge against design capacity, voltage, current, temperature, and the adapter's AdapterDetails.

Two traps:

  • Current is signed. Read unsigned, a discharge of 2,227 mA arrives as 18446744073709543389 and the wattage comes out positive. The watt figure is negative while discharging.
  • 65535 means "no estimate". It is the controller's sentinel, not a value. Read raw, "time remaining" would show forty-five days.

The watt figure is the battery's own charge or discharge power. It is not the machine's total draw: on mains the battery current falls to zero while the machine keeps drawing from the adapter, and measuring that needs SMC keys or powermetrics. The adapter figure is its rating, not its measured output.

Applications

AppKit would answer "does this have a Dock presence?" with NSRunningApplication.activationPolicy, but that means a GUI framework in the sampling path. The same fact is in the bundle's own Info.plist: LSUIElement or LSBackgroundOnly marks a bundle with no Dock presence. Results are cached per bundle, since bundles do not change while running.

Nested helper bundles declare it and classify correctly without special-casing. The limit is real: a bundle that sets its policy at runtime cannot be seen this way. On this machine AppSSOAgent, UserNotificationCenter and privatecloudcomputed declare neither key and so classify as applications. Excluding them by path would be a guess.

Per-process diagnostics

libc carries none of the structures these flavours return, so sys/diagnostics.rs addresses their fields by offset, produced the same way as the kinfo_proc offsets and documented beside each one.

Page Source
Overview PROC_PIDTASKINFO, PROC_PIDVNODEPATHINFO, proc_pidpath, proc_pid_rusage
Open files PROC_PIDLISTFDS, then PROC_PIDFD*INFO per descriptor
Memory map PROC_PIDREGIONPATHINFO, walked one mapping at a time
Mach ports task_name_for_pid then mach_port_names
Fileports PROC_PIDLISTFILEPORTS
Reports sysctl(KERN_PROCARGS2) for arguments; sample, lsof, vmmap and codesign run directly, without a shell

The region walk ends when the kernel reports nothing at or after the next address, which is EINVAL — not a failure once anything has been read. A mapping that does not advance, or whose end would wrap, stops the walk rather than turning it into a loop.

Mach-port enumeration needs a task-name port, which macOS refuses for most processes you do not own. That refusal is reported as a refusal.

proc_pidinfo reports "none" as a return of zero with errno untouched, so errno is cleared before the descriptor and fileport calls. Without that, a stale value left by an earlier syscall makes an empty list look like a denial.

Per-process network

macOS exposes no unprivileged per-process network accounting through a library call. nettop is Apple's own bundled client for it, so Starf runs it — with DNS disabled so it never blocks on a lookup, a three-second deadline, and one run at a time. It refreshes every five seconds on a background thread, because a subprocess must never delay the one-second sampler, and these counters change far more slowly than that anyway.

Two things in the output are easy to get wrong:

  • The columns do not arrive in the order they were requested. Asking for bytes_in,bytes_out,packets_in,packets_out returns a header reading ,packets_in,bytes_in,packets_out,bytes_out,. Reading by position would report packet counts as byte counts. They are matched by the header's own names.
  • The identity field is name.pid, and names contain dotscom.apple.WebKit.GPU.4949. The PID is whatever follows the last one.

The figures are cumulative byte totals for the process, not rates, and they follow the connections nettop can see, so they need not add up to the interface totals. A process it reported nothing for shows .