A macOS desktop app that visualizes GPU usage, SoC temperature, power, and fan RPM through a Dock icon and a floating window, designed for Apple Silicon and Intel Macs.
It runs powermetrics as a root LaunchDaemon and talks to it via XPC, while
also pulling data from IOAccelerator, AppleSMC, and IOHIDEventSystem to
collect a sample every second.
- 3D drum-shape pot rendered with cylindrical shading, dome lid, knob, back rim, and chunky loop handles
- Pot color tracks SoC temperature (white at 50 °C → red-orange at 95 °C)
- A layered flame (red-orange / orange / yellow-white core) under the pot scales with GPU usage; a soft halo radiates onto the pot at high load
- Steam puffs rising from the lid grow thicker and busier with fan RPM (or with temperature on fanless Macs)
- Boiling animation (lid bounce + warm-tinted steam) triggers after 5 s of
sustained load or
thermalPressure ≥ Serious - Honors Low Power Mode: drops to 5 fps and disables flame wiggle while LPM is on
- Titled, movable, resizable
NSWindowwith optional float-above-other-windows - Four charts: GPU / Temperature / Fan / Power with fixed Y-axis ranges
- Four metric tiles whose numbers shift white → yellow → red with risk
- nil samples render as gaps (no misleading flat-zero baselines)
- Translucent
NSVisualEffectViewbackground
- Open with Cmd+Shift+H from the menu bar
- Four stacked panels per category — Daily / Weekly / Monthly / Yearly (24 h / 7 d / 31 d / 400 d retention)
- Two categories selectable via the segmented Picker:
- Compute — GPU% (filled green) + Power W (blue line), dual Y axis
- Thermal — SoC °C (filled green) + Fan RPM (blue line), dual Y axis
- Authentic MRTG aesthetic: white plot background, dense fine + coarse vertical gridlines, wall-clock X axis (hour-of-day / day-of-week / day-of-month / month abbrev), Max / Avg / Cur stats footer, navy title bar
- Round-robin SQLite store at
~/Library/Application Support/MacSlowCooker/history.sqlitewith 5-min in-memory buffering and cascading rollups (5 min → 30 min → 2 h → 1 d)
- HTTP endpoint
/metricsin Prometheus text exposition format - Default port 9091, 127.0.0.1 only — toggle "Bind to all interfaces" to allow remote scraping (triggers macOS firewall prompt on first remote hit)
- Exposed metrics:
gpu_usage_ratio,gpu_power_watts,ane_power_watts,temperature_celsius,fan_rpm{fan="N"},thermal_pressure,helper_connected,build_info{version="..."} - No auth (Prometheus convention; front with a reverse proxy if needed)
- Periodically writes 8 PNG snapshots (
compute-{daily,weekly,monthly,yearly}.png,thermal-{daily,weekly,monthly,yearly}.png) plus an auto-refreshingindex.htmlto a chosen folder, the literal MRTG cron-and-PNG workflow - Default output:
~/Library/Application Support/MacSlowCooker/web/; configurable via Preferences with an Open Panel folder picker - Re-rendered every 5 minutes; renders one immediate snapshot on enable
- Serve with any static web server, e.g.
python3 -m http.server -d ~/Library/Application\ Support/MacSlowCooker/web
- Pot style (more styles planned for Phase 2)
- Flame animation: Off / Interpolation / Wiggle / Both
- Boiling trigger: Temperature / Thermal Pressure / Combined
- Float-above-other-windows toggle
- Prometheus exporter: enable / port / bind-all
- PNG export: enable / folder picker / Reveal in Finder
- Live "Low Power Mode is on" status row
MacSlowCooker.app (unprivileged, runs in the user login session)
├── main.swift — sets AppDelegate on NSApplication.shared
├── AppDelegate — XPC connection, settings observation, menus,
│ sleep notifications, exporter lifecycle
├── GPUDataStore — @Observable ring buffer (60 samples)
├── Settings — @Observable + UserDefaults + AsyncStream<Void>
├── XPCClient — NSXPCConnection (.privileged) + exponential
│ backoff reconnect
├── HelperInstaller — SMAppService.daemon registration; detects
│ stale helper binaries and re-registers
├── DockIconAnimator — Timer-driven state machine
│ (interpolation / wiggle / boiling fade)
├── DutchOvenRenderer — Core Graphics drawing of pot, flame, steam
│ (conforms to PotRenderer protocol)
├── PopupView — SwiftUI dashboard (4 charts + 4 metrics)
├── HistoryStore / Ingestor — round-robin SQLite + 5-min in-memory buffer
│ with cascading rollups
├── MRTGGraphView / HistoryView — SwiftUI Canvas MRTG renderer + tabbed root
├── HistoryWindowController — NSWindow host for the history viewer
├── PrometheusExporter — NWListener serving /metrics (opt-in)
├── PNGExporter — ImageRenderer → 8 PNGs + index.html (opt-in)
├── PopupWindowController — NSWindow (titled / closable / resizable)
└── PreferencesWindowController — NSWindow + SwiftUI Form
HelperTool (root LaunchDaemon, Contents/MacOS/HelperTool)
├── main.swift — NSXPCListener; HelperService.shared owns
│ sampling, latest sample isolated by Actor
├── PowerMetricsRunner — keeps /usr/bin/powermetrics running, parses
│ NUL-separated plist stream
├── IOAcceleratorReader — reads IOAccelerator's "Device Utilization %"
│ (matches Activity Monitor)
├── SMCReader — direct AppleSMC user-client; reads FNum and
│ F[i]Ac fan keys (fpe2 / flt formats)
└── TemperatureReader — IOHIDEventSystem-based SoC temperature
Shared (compiled into both targets — types that legitimately cross XPC)
├── GPUSample — Codable data model
├── XPCProtocol — MacSlowCookerHelperProtocol
├── HistoryGranularity / Record — pure value types for the history subsystem
├── HistoryAggregator — pure: bucket alignment, averaging
├── PowerMetricsParser — pure / testable plist parser, supports
│ legacy + macOS 26 + Intel schemas
└── PrometheusFormatter — pure: GPUSample → exposition string
- macOS 14 Sonoma or later (verified on macOS 26 Tahoe)
- Universal Binary: Apple Silicon (M1–M4) and Intel Macs
- Fanless Macs (MacBook Air M-series, etc.): the Fan chart and Fan metric tile auto-hide; the steam falls back to a temperature ramp
- Automatic code signing, Team
K38MBRNKAT
The repository ships pinned to my Apple Developer Team ID (K38MBRNKAT) in
five places: Shared/CodeSigningConfig.swift, HelperTool/Info.plist,
project.yml, and a couple of doc references. The privileged helper will
refuse XPC connections from a binary signed with a different Team, so
forks must swap them in lockstep:
bin/set-team-id.sh ABC1234XYZ # your 10-character Team ID
xcodegen generateAfter that you can build and run normally. If you skip the swap, the helper will install but every XPC call will fail with a code-signing requirement mismatch.
# Regenerate the Xcode project after editing project.yml
xcodegen generate
# Release build (signing is required for the helper to load)
xcodebuild -project MacSlowCooker.xcodeproj -scheme MacSlowCooker \
-configuration Release -derivedDataPath build build \
CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=K38MBRNKAT \
ONLY_ACTIVE_ARCH=NOONLY_ACTIVE_ARCH=NO is what produces a Universal Binary (arm64 + x86_64); a
plain xcodebuild build only emits the host arch.
xcodebuild test -project MacSlowCooker.xcodeproj -scheme MacSlowCooker \
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO114 unit tests covering:
BoilingTriggerTests(13): three trigger modes × edge conditionsDockIconAnimatorTests(15): interpolation, wiggle, boiling fade, timer lifecycle, sleep handling, render dedupDutchOvenRendererTests(3): smoke rendering across five states, crash resistance over the full usage range, and the fanless temperature fallbackPowerMetricsParserTests(12): legacy / macOS 26 Tahoe / Intel schemasIconStateTests(10) +IOAcceleratorSelectionTests(8) +SensorNameMatcherTests(10) +SMCFanDecoderTests(10) +PlistStreamSplitterTests(7): pure helpers extracted from the renderer and the IOKit-bound readersHelperInstallerTests(9): version-comparison rules including the monotonic downgrade-refusal casesHostCPUTests(2),SettingsTests(6): runtime arch detection, persistence, fallback, change stream, reset-to-defaultsGPUSample/GPUDataStore(6): JSON round-trip and ring buffer behavior
# One-time: take ownership so future swaps don't need sudo
sudo chown -R $(whoami):staff /Applications/MacSlowCooker.app
# Deploy cycle
pkill -9 -x MacSlowCooker
ditto build/Build/Products/Release/MacSlowCooker.app /Applications/MacSlowCooker.app
sudo launchctl kickstart -k system/com.macslowcooker.helper # restart helper after binary change
open /Applications/MacSlowCooker.appThe version-sync logic (HelperInstaller.refreshIfStale) auto-recovers from a
stale helper if the bundled CFBundleVersion has been bumped, but the manual
launchctl kickstart is the fastest way during active development.
Known caveats (see CLAUDE.md for the long form)
- macOS 26 changed the powermetrics output schema significantly
(
gpu_active_residency→idle_ratio, ANE power moved underprocessor) powermetrics --samplers smcwas removed in macOS 26 — fan RPM is read directly from AppleSMC instead@mainAppDelegate trap on macOS 26:applicationDidFinishLaunchingnever fires;main.swiftsets the delegate explicitly- HelperTool Info.plist embedding: a
type: tooltarget does not embed Info.plist by default, so codesign rejects it for SMAppService — the workaround isOTHER_LDFLAGS: -sectcreate __TEXT __info_plist SMAppService.notFoundfalse positive: status sometimes reports.notFoundeven with a properly-placed plist; we attemptregister()anyway
Phase 2 work is tracked on GitHub Issues:
- Additional pot styles (oden, curry, wok, etc.) and locale-aware names
- Per-process visualization (ingredients floating in the pot)
- Full
powermetrics→IOReportmigration to drop the spawned process - Faster cold-launch latency
- Other items raised during code review
Apache License 2.0 — commercial use, modification, and
redistribution are permitted. Please preserve the attribution in NOTICE.
The license includes a patent grant: contributors lose their license to any contributed code if they initiate patent litigation against it.
See CONTRIBUTING.md for setup, build/test, and PR-submission guidance. Submitted contributions are taken under the Apache 2.0 license (Apache License Section 5).
MacSlowCooker Cooking Plate — concept product page, not an actual accessory. Your Mac Studio reaches ~65 °C / 149 °F under sustained 100% GPU. Don't actually cook on it.
Cluster Edition — four Mac minis, one plate. Same parody, four times the BTUs. Vertical stacking will trigger thermal throttling and shorten Mac lifespan; please array your LLM training rack horizontally instead.
Mac mini Brew Stand — your Mac mini, now your barista. The cup-warmer cousin of the Cooking Plate. ~50 °C top plate is just enough for a coffee mug, a tumbler, and an espresso glass. Still not a real product. Still don't actually do this.



