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

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

By Arin Prajapati 12 min read
White headline on black reading What a menu bar app costs while idle, with the kicker Performance and the NotchStar logo

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

QuestionToolWhy this one
How much CPU did it use?ps -o time= read twiceCumulative 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 PIDMemory 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 PIDOne line per thread, plus a header
How often does it wake the CPU?top -l N -stats idlewCumulative idle wake ups, so the difference is the rate
Is it doing work while idle?all of the above, over minutesA 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

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.

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.

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.

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.

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

MeasureValue
Window20:58:28 to 20:59:31, 63 seconds
CPU time used0.03 s (0.22 to 0.25)
As a share of one coreabout 0.05 percent
Footprint16 MB
Threads4
Idle wake ups13 to 29, 16 in 60 seconds
Resident size78,848 KB, then 77,552 KB

Run 2: 300 seconds, sampled every 10 seconds

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

SecondsCPU timeResident KBThreads
00:00.1472,8964
500:00.1769,8405
1600:00.2469,8885
1700:00.5076,3527
2500:00.5769,3125
2600:00.8074,8806
3000:00.8866,2404

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.

SecondsCPU timeThreadsBattery
00:00.90664%
900:00.99564%
1000:01.00765%
2100:01.07466%
2600:01.13566%
3300:01.17567%
3600:01.19567%

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.

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
CommandAnswers
ps -o time=,rss= -p PIDCumulative CPU time and resident size
ps -M -p PIDThreads, one per line
footprint PIDMemory the process owns, by category
top -l N -s S -pid PID -stats pid,cpu,mem,threads,idlewCPU, memory, threads, idle wake ups over time
uptimeLoad average
pmset -g battPower source and battery level
sysctl -n machdep.cpu.brand_string hw.ncpu hw.memsizeThe 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, with a 7 day free trial.

NotchStar is a native Mac notch app, $4 once, with a 7 day free trial. Try it, or read the ten tools.