Skip to content

How It Works

fm turns a pile of raw counters into one answer: why is the fan spinning, and what should I close? This page explains the reasoning; the exact numbers live in The Algorithm.

The two regimes

Every hot Mac is in one of two states, and they look identical on the surface (hot, loud, load average through the roof) but are opposite underneath:

Something is genuinely computing. Load average is high and the sum of per-process CPU% is high. The fix: find the CPU hog and stop it (or wait it out).

The Mac has run out of RAM and is paging to disk. Load average is high — load counts tasks blocked on I/O, not just CPU — but the sum of per-process CPU% is low, because processes are stuck waiting on the disk, not running. The fix: free RAM by closing the biggest, longest-lived apps.

??? example "A real one" In one observed episode, load average hit 15 on 10 cores while the app's 15-second CPU-time delta measured only 0.0 CPU-seconds. The machine wasn't computing at all — it was paging. Swap had grown from 14 GB to 24 GB in ~90 minutes. A CPU-only tool would have reported "nothing using the CPU, fan is fine" while the fan screamed.

Why ps %cpu lies

ps's %cpu column is a process's CPU time averaged over its entire lifetime, and can be stale. A process that spiked and then blocked on I/O looks calm. fm instead samples cumulative CPU time twice and computes a delta over the sampling window — the true instantaneous usage. That delta is what makes the CPU-vs-memory distinction reliable.

flowchart TD
    A[raw counters] --> B{memory pressure? swap, compressor, RAM free}
    A --> C{cpu pressure? per-process delta}
    A --> D{load vs cores}
    B & C & D --> E[classify regime]
    E -->|memory| F[weight RAM + age]
    E -->|cpu| G[weight CPU]
    E -->|both| H[mixed weights]
    E -->|neither| I[nominal]
    F & G & H --> J[rank closeable processes]
    I --> K[no action]
    J --> L[close list]

Sampling pipeline

Every refresh, engine.Engine assembles one snapshot by reading:

Source What it gives you
smc (via Stats.app, optional) fan count, RPM, target, range; all temperature sensors
pmset -g therm CPU speed limit — how much macOS is throttling
Mach host_processor_info per-core user / system / idle ticks (ctypes, no sudo)
ps -axww PID, PPID, RSS, age, cumulative CPU time, full command line
vm_stat free / wired / anonymous / file-backed pages, compressor stored vs occupied, page-in/out counters
sysctl vm.swapusage, vm.loadavg, hw.ncpu, hw.memsize, hw.perflevel*.logicalcpu, kern.boottime
memory_pressure system-wide RAM free %
~/watchdogs/… fan-event log + probe status (read-only)

CPU% and page I/O rates are deltas against the previous sample, so the very first frame shows zeros and subsequent frames are real. Sampling runs in a background worker thread, so the UI never blocks on subprocess I/O.

Classification

Each process is tagged into one category, which drives both the recommendation weight and whether it's even killable:

Category Examples Killable? Prior
agent claude, codex, ChatGPT/Codex, omp, devin, cmux, sinter, node_repl yes 1.00
browser Chrome, Edge, Safari, Arc, Firefox, Brave (+ helpers) yes 0.85
chat WhatsApp, Telegram, Discord, Slack, Teams, Zoom yes 0.60
app anything else under /Applications yes 0.50
system WindowServer, launchd, kernel_task, mds/mdworker, suggestd, com.apple.*, /usr/libexec/, … never

System processes are symptoms, not causes

If WindowServer is burning CPU, closing it is not the answer — it's the compositor rendering too many windows/displays. fm surfaces these as advisories ("WindowServer at 78% — reduce windows/displays") instead of offering a kill.

Grouping swarm siblings

Long-running agent setups (multi-agent "swarms") spawn many processes sharing a session id. fm extracts the session-<8hex> token from the command line and groups same-session agents, so a recommendation like claude@session-476111c5 x3 collapses three siblings into one row and one kill action.

From verdict to a ranked list

Once the regime is chosen, every closeable group gets a score and the top six are shown. Weights flip between regimes: memory thrash prioritises RAM and age; CPU load prioritises CPU%. The exact formula, thresholds, and defaults are in The Algorithm.