Blog

The cheapest sample is the one you never take

I spent a week making the collectors faster. Deleting the work instead is what got idle CPU to 0.046%.

11 July 20265 min read

I spent about a week trying to make AirStats' collectors faster. I shaved allocations, I cached sysctl lookups, and I stared at Instruments traces until the flame graph stopped meaning anything. The numbers came down a bit.

Then I asked a different question, and most of that week turned out to be beside the point. Not how do I make this sample cheaper, but why am I taking this sample at all.

The app already knows if you are looking

A menu bar monitor has a good signal for what it needs, and it is sitting right there: what is on screen.

If the panel is closed, nothing on the machine is displaying a list of top processes, so there is no reason to build one. If the display is asleep, nothing is displaying anything at all.

Obvious in hindsight. Most of the distance between AirStats' 0.046% idle CPU and everything else I measured comes from that, not from the clever work I did the week before.

Five levels

So the sampling engine takes an activity level, and every collector's cadence is derived from it. Five of them:

  • Suspended. Display asleep, screen locked, or the system going to sleep. The timer is fully suspended, not slowed down. Nothing runs.
  • Occluded. The status item is not visible to you, usually because some app is full screen over it. Menu bar sources only, at a quarter of the normal cadence.
  • Menu bar. The normal state. Status item visible, panel closed. Only the collectors feeding the metrics you actually put in your menu bar get sampled.
  • Desktop widget. The desktop widget is up. Same cadence, wider set of sources.
  • Panel. You opened the panel. Full cadence, every enabled source, processes included.

The gap that pays for everything is between menu bar and panel. The process collector is by far the most expensive thing AirStats does, and it runs only while the panel is actually open, which for most people is a few seconds a day.

Enabled is not the same as displayed

This is the part I think most apps get wrong, and it took me longest to accept.

Say you have never once put GPU in your menu bar and the panel is closed. The GPU collector is not sampled at a slower rate. It is not sampled at all, and it lets go of everything it was holding on the way out: IOAccelerator service handles, SMC keys, buffers. When it comes back later it re-acquires them and primes itself before producing a single number.

That priming step sounds like a detail. It is not. Several of these counters report a value over the interval since their last read, so a collector waking up after an hour of retirement would happily report an hour-long average as if it were the last second.

I know that because it shipped, and it told me my idle GPU was at 98%.

Nothing allocates on the tick

Underneath all this the tick itself does no housekeeping, so that week was not a total loss. Sample buffers are swapped rather than reassigned. sysctl MIBs resolve once at collector setup instead of walking a name resolver on every sample. SMC request and response buffers live as long as the collector that owns them, instead of being built and thrown away on every read.

But none of that would have rescued a design that sampled everything all the time, which is the whole reason I am writing it down.

Why this goes beyond menu bar apps

Deciding not to run is worth more than making the run fast, and it is available to almost any program that can tell whether anyone is looking.

A dashboard on a tab nobody has in front of them. A poller behind a collapsed section. A background refresh for a screen that got dismissed four minutes ago. Every one of those is the same shape: work scheduled by a timer when it should be scheduled by attention.

Optimise the sample if you like, but ask why you are taking it first.

AirStats

Sixteen metrics in your menu bar for 0.046% CPU. Free and open source.

Download for Mac

How much CPU should a menu bar app use?