# Measuring what a menu bar app costs while idle: a method, with numbers

> How to measure a Mac menu bar app's idle CPU, memory and wake ups with ps, top and footprint, with real numbers from three runs and why one window misleads.

Canonical: https://www.notchstar.com/blog/measure-menu-bar-app-idle-cpu-memory-mac/
Last updated: 2026-11-04

*CPU time, memory footprint, threads and wake ups for a real Mac app, three runs, and the readings that nearly gave the wrong answer*

Published 2026-11-04.

## TL;DR

- Idle cost is a handful of numbers read with tools that ship with macOS: cumulative CPU time from `ps`, memory from `footprint`, thread count and idle wake ups from `top`. No root is needed, and `powermetrics` is not used.
- Measured on 4 November 2026 on an M4 Mac, NotchStar 1.0.5 (Release build) with its notch closed used about 0.05 to 0.09 percent of one core in quiet stretches, with a memory footprint of 16 MB at 50 seconds into one run. Occasional bursts of about a quarter of a second of CPU, and a footprint of 23 to 27 MB in later readings, were also recorded. The cause of the bursts was not found.
- A single 60 second window gave a reassuring number that the next window contradicted. The method below uses several windows, and reports the spread.

## The problem: an adjective is not a measurement

A menu bar app runs all day. Its cost is the sum of everything it does while the user is not looking: timers that fire, observers that wake, memory it keeps. The usual claims are adjectives. A number needs a method that someone else can repeat, and an honest list of what the method leaves out.

Earlier NotchStar documents state design targets for idle cost and no results. `blog/FACTS.md` says plainly that idle CPU, memory, wake ups and launch time were not measured, and that the early budgets were targets. This post is the first measurement, and it is limited. It covers a closed notch on one Mac, run in a test mode. Each limit is listed near the end.

## Approach: pick tools that answer one question each

| Question | Tool | Why this one |
|---|---|---|
| How much CPU did it use? | `ps -o time=` read twice | Cumulative CPU seconds, so the difference is exact for any window. A `%CPU` snapshot is an average over the process's life or a short interval |
| How much memory does it hold? | `footprint PID` | Memory attributed to the process, by category: dirty, clean and reclaimable pages. It does not count shared system libraries the way resident size does |
| How many threads? | `ps -M -p PID` | One line per thread, plus a header |
| How often does it wake the CPU? | `top -l N -stats idlew` | Cumulative idle wake ups, so the difference is the rate |
| Is it doing work while idle? | all of the above, over minutes | A single reading proves little |

Everything here runs as an ordinary user. `powermetrics` can report energy per process, and it needs root, so it is skipped.

### The thing under test

The subject is a real build, not a toy: the Release configuration of NotchStar 1.0.5, universal, already present in the repository's `build/` folder from 11 October 2026 (`CFBundleShortVersionString` 1.0.5). It is launched with `--ui-testing`, the flag the UI tests use. That flag is the reason it is safe to run here. In that mode the license is held in memory, the screenshot tray and watcher are not started, and the app reads and writes its own pasteboard and defaults domain, so the user's trial, clipboard, note and screenshots are never touched. `NotchStarApp.swift` documents this in a comment above the `UITesting` enum.

The cost is a gap. The screenshot watcher, which a real user has running, is not in these numbers. Everything else in the idle app runs: the notch panel, the clipboard poll on a half second timer, the menu bar item, the license revalidation check and the opt in analytics check, which does nothing unless the user turned it on.

### The machine

```sh
sysctl -n machdep.cpu.brand_string hw.ncpu hw.memsize
sw_vers -productVersion
uptime
pmset -g batt | head -2
```

Results on 4 November 2026: Apple M4, 10 cores, 25,769,803,776 bytes of memory (24 GiB), macOS 27.0.1, on AC power with the battery at 58 percent and charging. The load average at the first run was 3.64, and 6.04 to 7.32 at later readings, because other work was running on the Mac. This is not a quiet machine, and the numbers are tagged accordingly.

### The procedure

The script launches the app, waits 20 seconds so launch work has finished, then records one line every 10 seconds. The wait is not a guarantee: run 3 below had already used 0.90 CPU seconds when its first line was written.

```sh
open -n -a build/Build/Products/Release/NotchStar.app --args --ui-testing
sleep 20
P=$(pgrep -x NotchStar)
for i in {0..30}; do
  echo "$((i*10)) $(ps -o time=,rss= -p $P | tr -s ' ') $(ps -M -p $P | tail -n +2 | wc -l)"
  sleep 10
done
footprint $P | sed -n 2p
kill -TERM $P
```

The columns are seconds since the first sample, cumulative CPU time in minutes and seconds, resident size in kilobytes, and the thread count. At the end the app is sent `SIGTERM`. The app handles it by stopping its helper and exiting, and `pgrep` finds no process afterwards.

## The wrong turn: one window looked clean

The first measurement was a single window of 63 seconds, read with `ps` before and after, and `top` once every 3 seconds in between.

```text
20:58:28  TIME 0:00.22   RSS 78,848 KB
20:59:31  TIME 0:00.25   RSS 77,552 KB
```

That is 0.03 CPU seconds in 63 seconds, or about 0.05 percent of one core. `ps` reports time in hundredths of a second, so the real figure is within about 0.01 of that. The footprint at the start was 16 MB, `top` showed 4 threads and `0.0` in the CPU column for 20 of 21 samples (one read `0.1`), and the idle wake up counter rose from 13 to 29, which is 16 in 60 seconds or roughly 0.27 a second. It is a nice result and it is what a post would want to print.

It was not the whole picture. A second window began at `20:59:44` and lasted 120 seconds.

```text
20:59:44  TIME 0:00.26   RSS 72,624 KB
21:01:44  TIME 0:00.78   RSS 74,496 KB
          footprint 27 MB, 52 threads in the next top sample
```

That window used 0.52 CPU seconds, about 0.43 percent of one core, which is eight times the rate of the first. The footprint had gone from 16 MB to 27 MB and the process had 52 threads. Something happened in the app. No input was scripted. The cause is not known, and the shape of the lesson does not depend on it: a clean first window said nothing about the next one.

The change after that was to log continuously, for 300 seconds, with a sample every 10 seconds, and to run it more than once.

### The memory number that was five times too big

`ps` reports resident size, and it was 78,848 KB at the first reading. `footprint` said 16 MB for the same process at the same time. They measure different things. Resident size counts every page mapped into the process that is in physical memory, including the shared system libraries that every Mac app uses. The footprint counts the memory that belongs to the process.

```text
NotchStar [39828]: 64-bit    Footprint: 16 MB (16384 bytes per page)
  Dirty
  8704 KB   Malloc Small
  2277 KB   __DATA_DIRTY
  1500 KB   __DATA
  1104 KB   Malloc Metadata
```

Quoting the resident figure alone would have made the app look about five times larger than its footprint. Quoting only the footprint would hide that the process maps those shared pages too. Both are reported here, with their names.

For scale, the same `footprint` command on a bare `sleep 600` process on the same Mac reported 1,056 KB (4 November 2026, `21:12`). A process that does nothing at all still costs about a megabyte.

## Numbers, with the method

Three runs on 4 November 2026. The first was an exploratory one, the other two were scripted. In all of them the notch was closed and the pointer was not moved to it on purpose.

### Run 1: one clean window

| Measure | Value |
|---|---|
| Window | `20:58:28` to `20:59:31`, 63 seconds |
| CPU time used | 0.03 s (0.22 to 0.25) |
| As a share of one core | about 0.05 percent |
| Footprint | 16 MB |
| Threads | 4 |
| Idle wake ups | 13 to 29, 16 in 60 seconds |
| Resident size | 78,848 KB, then 77,552 KB |

### Run 2: 300 seconds, sampled every 10 seconds

Started `21:02:22`, 20 seconds after launch.

| Seconds | CPU time | Resident KB | Threads |
|---|---|---|---|
| 0 | `0:00.14` | 72,896 | 4 |
| 50 | `0:00.17` | 69,840 | 5 |
| 160 | `0:00.24` | 69,888 | 5 |
| 170 | `0:00.50` | 76,352 | 7 |
| 250 | `0:00.57` | 69,312 | 5 |
| 260 | `0:00.80` | 74,880 | 6 |
| 300 | `0:00.88` | 66,240 | 4 |

Between 0 and 160 seconds the app used 0.10 CPU seconds, which is 0.06 percent of one core. At 170 seconds it used 0.26 seconds in a ten second interval, with the thread count rising to 7. At 260 seconds it did the same with 0.23 seconds. Over the whole 300 seconds it used 0.74 CPU seconds, about 0.25 percent of one core. The footprint was 16 MB at 50 seconds and 23 MB at 300 seconds. After the last sample, the final `top` line showed 0.0 percent CPU, 23 MB and 5 threads.

### Run 3: 360 seconds, with the battery level logged

Started `21:08:24`, with a sample every 10 seconds. The reason for this run was a hypothesis: the app listens for power changes through IOKit, and a battery percentage change on a charging Mac might cause the bursts. The battery was logged next to each sample.

| Seconds | CPU time | Threads | Battery |
|---|---|---|---|
| 0 | `0:00.90` | 6 | 64% |
| 90 | `0:00.99` | 5 | 64% |
| 100 | `0:01.00` | 7 | 65% |
| 210 | `0:01.07` | 4 | 66% |
| 260 | `0:01.13` | 5 | 66% |
| 330 | `0:01.17` | 5 | 67% |
| 360 | `0:01.19` | 5 | 67% |

The battery went from 64 to 65 percent around 100 seconds, from 65 to 66 percent around 210 seconds and from 66 to 67 percent around 330 seconds. None of the three changes was followed by a burst: the CPU time rose by 0.01 seconds in the interval of each change. The thread count rose to 7 at the first one. Across the 360 seconds the app used 0.29 CPU seconds, about 0.08 percent of one core. So the hypothesis is not supported by this run. It also shows how much the start can differ: this run began at 0.90 CPU seconds, where run 2 began at 0.14, although both were read 20 seconds after launch.

### What the numbers say, and do not

- In quiet stretches the closed notch cost 0.05 to 0.09 percent of one core on this Mac (0.05, 0.06 and 0.08 or 0.09 in the three runs, depending on the window). That is a rate over minutes, from cumulative CPU time.
- Bursts of about a quarter of a CPU second happened twice in run 2 and not at all in run 3. Their cause was not identified.
- The footprint was 16 MB in the first readings and 23 to 27 MB in later ones. It is not a constant.
- Resident size was between 30 MB and 79 MB across the runs and fell sharply in run 3, from 65 MB to 46 MB at 260 seconds and to 30 MB by 360 seconds, without any action. That is the system compressing or reclaiming pages, not the app freeing them.
- Thread counts were 4 to 7 in the quiet runs and 52 once.

## What is still bad

- The runs used `--ui-testing`, so the screenshot watcher is missing, and a real user's idle app does extra work. A measurement of the app as a customer runs it has not been done.
- Only the closed notch was measured. Open notch, each tool, Now Playing with its helper process, the Mirror camera and the System tab's one second sampling were not.
- Two bursts and a 52 thread reading are unexplained. A trace with Instruments would say what ran. It was not used.
- The Mac was busy, with a load average of 3.6 to 7.3, so the absolute numbers may differ on a quiet machine, in either direction.
- Idle wake ups come from `top`'s counter and were read in one run. The app has a clipboard poll on a half second timer, which would be about two wake ups a second if each tick woke the CPU, and the measured rate was 0.27 a second. Timers can be grouped by the system, and that was not investigated.
- The `POWER` column of `top` read 0.0 in nearly every sample (one 0.1). It is a coarse estimate, and energy was not measured with `powermetrics`.
- Launch time was not measured, and nothing was compared to another app. A claim that this app is lighter than any other would need the same method run on the other app.
- One machine, one macOS version, one build, three runs.

## Try it yourself

None of this needs NotchStar. Pick any menu bar app that is running. The core of it is three lines, and the steps add care around them.

```sh
P=$(pgrep -x AppName)
ps -o time=,rss= -p $P; sleep 60; ps -o time=,rss= -p $P
footprint $P | sed -n 2p
```

1. **Find its process.** Run `pgrep -x AppName`, or `ps -A | grep AppName`.
2. **Write down the machine.** Run `uptime` and `pmset -g batt`. A load average above 1 on a quiet Mac means something else is running.
3. **Read CPU time twice.** Run `ps -o time=,rss= -p PID`, wait 60 seconds, run it again. Subtract the times.
4. **Turn the difference into a rate.** Divide CPU seconds by the window in seconds, and multiply by 100 for a percentage of one core.
5. **Read the footprint.** Run `footprint PID | sed -n 2p`. Note it next to the resident size from step 3.
6. **Count threads and wake ups.** Run `ps -M -p PID | wc -l` and subtract 1, and `top -l 21 -s 3 -pid PID -stats pid,command,cpu,mem,threads,idlew`.
7. **Log for five minutes.** Put steps 3 and 5 in a loop with `sleep 10`.
8. **Repeat the run later.** Compare two or three runs before you write down a number.
9. **Write what you left out.** Which states you did not measure, which tools you skipped.

## Cheat sheet

- [ ] Same user, no root, tools that ship with macOS
- [ ] Wait 20 seconds or more after launch before the first sample
- [ ] Use cumulative CPU time, not a single `%CPU` reading
- [ ] Use `footprint` for memory and name the resident size separately
- [ ] Log for minutes, not seconds
- [ ] Run it more than once and report the spread
- [ ] Record the machine, the macOS version, the load average and the power state
- [ ] Record the build, and any flag that changes what the app does
- [ ] Say what was not measured
- [ ] Do not compare with another app unless the method was identical

| Command | Answers |
|---|---|
| `ps -o time=,rss= -p PID` | Cumulative CPU time and resident size |
| `ps -M -p PID` | Threads, one per line |
| `footprint PID` | Memory the process owns, by category |
| `top -l N -s S -pid PID -stats pid,cpu,mem,threads,idlew` | CPU, memory, threads, idle wake ups over time |
| `uptime` | Load average |
| `pmset -g batt` | Power source and battery level |
| `sysctl -n machdep.cpu.brand_string hw.ncpu hw.memsize` | The machine |

## Closing

The app measured here is NotchStar, a Mac app that puts ten tools in the notch. If you want to try it, it is at [notchstar.com](https://www.notchstar.com/), with a 7 day free trial.

## More posts

- [A universal DMG in a shell script: lipo, hdiutil, notarytool and stapling](https://www.notchstar.com/blog/universal-dmg-shell-script-lipo-hdiutil-notarytool/)
- [How a one person Mac app handles email](https://www.notchstar.com/blog/how-a-one-person-mac-app-handles-email/)
