Thermal Pilot is a lightweight menu bar app that shows what's happening inside your Mac in plain English. See CPU load, memory pressure, temperatures, fan activity, and the apps causing problems without opening Activity Monitor or digging through Terminal.
Built for Apple Silicon and Intel Macs.
Download the latest release for macOS 14+.
Open the DMG, drag Thermal Pilot into Applications, and launch it from your menu bar.
Most system monitors overwhelm you with numbers.
Thermal Pilot focuses on the information that actually matters:
- Is your Mac healthy?
- What's causing slowdowns?
- Which app is using all your memory?
- Are temperatures becoming a problem?
- Should you close something or leave it alone?
Instead of making you interpret dozens of metrics, Thermal Pilot gives you a clear overview and a simple verdict.
CPU usage, memory pressure, temperatures, fan activity, and system health live in a single panel beside Control Center.
See the largest memory consumers instantly. Chrome tabs are grouped together. Helper processes stay organized under their parent application. Rogue Node, Python, Docker, Ollama, LM Studio, and local AI workloads appear by name.
Memory pressure is often a better indicator of system health than RAM usage alone. Thermal Pilot surfaces it prominently alongside a complete breakdown of Active, Wired, Compressed, Cached, and Free memory.
A built-in slowdown indicator analyzes CPU load, memory pressure, temperatures, and fan activity to explain what's happening in plain English.
No accounts. No analytics. No telemetry. No background services.
Everything runs locally on your Mac using public macOS APIs and hardware sensors.
Lives entirely in your menu bar. No Dock icon. No unnecessary background processes. Fast startup and configurable refresh intervals.
When a sensor isn't available on your Mac, Thermal Pilot tells you. It never invents readings or estimates values it cannot verify.
Current CPU utilization, processor information, and overall system load.
Memory usage, memory pressure, memory composition, and the processes consuming the most RAM.
The hottest available thermal sensors across your system with support for Celsius and Fahrenheit.
Current fan speeds, operating ranges, and utilization percentages where supported.
A simple explanation of whether your Mac is running normally or approaching a bottleneck.
Thermal Pilot never sends data anywhere.
No network traffic. No analytics. No accounts. No tracking.
Your preferences remain stored locally on your Mac.
Thermal Pilot is fully open source under the MIT License.
Contributions, bug reports, and feature suggestions are welcome.
This section covers how Thermal Pilot works under the hood — how each metric is collected, how the data is processed, and how the codebase is structured. Everything here is verifiable by reading the source in Sources/FanUsageCore.
Thermal Pilot is a Swift 6 package split into two targets:
FanUsageCore— the data layer. A dependency-free library containing all metric reading, data models, and business logic. Links onlyIOKit. Has no UI dependencies and is fully unit-tested in isolation.FanUsageApp— the UI layer. A SwiftUIMenuBarExtraapp that consumesFanUsageCore, renders the panel, and handles preferences.
This separation means the entire data pipeline — CPU sampling, memory page accounting, SMC decoding, bottleneck analysis — can be audited and tested without running the app.
CPU usage is measured using host_statistics(HOST_CPU_LOAD_INFO) from the Darwin kernel, which returns cumulative tick counts split into user, system, idle, and nice buckets.
Thermal Pilot takes two samples separated by the configured refresh interval and computes:
usage% = (totalDelta − idleDelta) / totalDelta × 100
This produces a whole-machine figure normalized to 0–100%, regardless of core count. The chip model is read from sysctl machdep.cpu.brand_string and the core count from ProcessInfo.processorCount.
The first reading after launch always shows 0% — there is no previous sample to diff against.
Memory statistics are read from host_statistics64(HOST_VM_INFO64), which exposes the kernel's VM page counters. Page size is read from host_page_size. Total physical RAM comes from ProcessInfo.physicalMemory.
The raw page counts map to display categories as follows:
| Category | Source pages |
|---|---|
| Active | active_count |
| Wired | wire_count |
| Compressed | compressor_page_count |
| Cached | inactive_count + speculative_count |
| Free | free_count |
| Used | active + wired + compressor |
| Available | free + inactive + speculative |
Memory pressure is derived from used-vs-total and compressor pool size, then classified:
| Status | Condition |
|---|---|
| Normal | pressure < 74% and compressed < 10% of total RAM |
| Elevated | pressure ≥ 74%, or compressed > 10% of total RAM |
| High | pressure ≥ 88%, or compressed > 20% of total RAM |
Top memory consumers are read by running /bin/ps -axo pid,ppid,rss,comm and parsing resident set size (RSS). Processes are then grouped by their owning .app bundle — walking the command path up to the outermost .app directory — so all Chrome helpers, Electron workers, and renderer processes collapse under their parent application. Processes with no bundle (bare node, python, ollama, etc.) appear individually by name.
Fan speeds and temperatures are read from the System Management Controller (SMC) over IOKit using the AppleSMC service.
The SMC protocol works by sending key-value requests over IOConnectCallStructMethod. Each key is a four-character code packed into a UInt32. Thermal Pilot reads the following keys:
Fan keys:
| Key | Meaning |
|---|---|
FNum |
Number of fans |
F0Ac, F1Ac, … |
Current RPM |
F0Mn, F1Mn, … |
Minimum RPM |
F0Mx, F1Mx, … |
Maximum RPM |
F0ID, F1ID, … |
Fan label string |
Thermal keys:
| Key | Sensor |
|---|---|
TC0P |
CPU Proximity |
TC0E |
CPU Core 1 |
TC0F |
CPU Core 2 |
Tp09 |
SoC |
Te05 |
Die |
SMC values are encoded in several binary formats. Thermal Pilot decodes all of them:
| SMC type | Encoding | Decoding |
|---|---|---|
flt |
IEEE 754 float, little-endian | Float(bitPattern: UInt32) |
fpe2 |
Unsigned fixed-point, /4 | UInt16 / 4.0 |
sp78 |
Signed fixed-point, /256 | Int16 / 256.0 |
ui8 |
Unsigned 8-bit integer | Direct |
ui16 |
Unsigned 16-bit integer, big-endian | UInt16 |
ui32 |
Unsigned 32-bit integer, big-endian | UInt32 |
Raw thermal reads can spike or glitch. Thermal Pilot applies a stability filter per sensor key: values outside 15–115°C are rejected, jumps greater than 35°C between consecutive reads are suppressed, and the SoC sensor (Tp09) is cross-checked against the die sensor (Te05) to catch implausible drops. When a read is rejected, the last stable value is held until the next valid reading.
If the SMC service cannot be opened — due to sandboxing, permissions, or the key not being exposed on the current hardware — all reads return nil and the UI shows "Unavailable".
The slowdown verdict is computed by BottleneckAnalyzer after every refresh. It evaluates conditions in priority order and returns the first match:
| Verdict | Condition |
|---|---|
| Memory pressure | Memory status is High |
| Thermal limit | Hottest sensor ≥ 88°C |
| CPU-bound | CPU usage ≥ 82% |
| Memory getting tight | Memory status is Elevated |
| Cooling active | Fan load ≥ 72% or hottest sensor ≥ 78°C |
| No obvious bottleneck | None of the above |
Each verdict carries a severity (normal, notice, warning, critical), a plain-English detail line, and a progress value that drives the indicator bar.
MetricsModel runs a refresh loop on the main actor. Every cycle it calls CompositeHardwareProvider.snapshot(), which fans out synchronously to SystemCPUProvider, SystemMemoryProvider, and SMCSensorProvider, assembles a HardwareSnapshot, runs validation, and returns.
The loop counts down second-by-second between refreshes so the UI can display "Next update in Ns". If a refresh is already running when the next interval fires, it is skipped — there is no queue buildup. A snapshot is flagged as stale if the read took longer than 2 × refreshInterval.
Four keys are persisted to UserDefaults. Nothing else is written to disk.
| Key | Default | Controls |
|---|---|---|
refreshInterval |
3.0 |
Sampling cadence in seconds |
temperatureUnit |
celsius |
°C or °F |
menuBarDisplayMode |
cpu |
Metric shown in the menu bar label |
launchAtLogin |
false |
Registered via SMAppService |
Every HardwareSnapshot passes through MetricValidation before being published. It checks for physically impossible values: negative or non-finite fan RPM, inverted fan ranges, CPU usage outside 0–100%, used memory exceeding total RAM, and temperatures outside 0–115°C. Any violations are appended as warnings to the snapshot and surfaced in QA output.
swift test # unit tests
scripts/package-app.sh release # build, ad-hoc sign, produce .app + .dmg + .zip
open "build/Thermal Pilot.app"The unit tests cover CPU tick math, memory page accounting and pressure classification, all six SMC numeric decodings, thermal spike rejection and recovery, display string rounding, validation flagging, and bottleneck precedence rules — without requiring real hardware, using FixtureSMCReader for injectable test data.
# Stream JSONL samples — raw values, display strings, timing, warnings, bottleneck verdict
swift run FanUsage --qa-sample --count 300 --interval 1
# Dump every SMC key read — raw bytes, type tag, decoded value
swift run FanUsage --diagnose-sensorsCross-check with Apple's own tools:
top -l 2 -n 0 # CPU ticks
vm_stat # VM page counters
memory_pressure # system pressure