Scripting
JSON output
plottypus --json # one sample, then exit
plottypus --json-stream # one line per interval (NDJSON) until you stop it
plottypus --json-stream --count 10 -i 1s # ten lines, one a second
plottypus --json --procs 25 # include the top 25 processes (default 10)The field most scripts want is cpu_power, in watts. On the M5 Pro that is the SMC rails. On other chips it is a held IOReport value, or null once the value is older than 10 seconds. Where the watts come from.
Both modes sample once a second unless -i says otherwise. The first sample only primes the rates, so --json takes one interval before it prints. It then waits, for at most 2.5 s in total, until the slow collectors (sensors, battery, IOReport) have a first reading, so a one-shot sample is complete. --json-stream does not wait: its first lines may still carry null for readings that arrive later.
Nothing needs sudo, and nothing is sent anywhere. Output goes to stdout; errors go to stderr.
Rules#
schema_versionis1. Schema 1 only ever gains fields; nothing is renamed or removed. A test pins the field set.- snake_case everywhere. Names follow macmon where macmon has one:
cpu_power,gpu_power,ane_power,ram_power,sys_power,all_power,gpu_freq_mhz,memory.ram_total,memory.ram_usage,memory.swap_total,memory.swap_usage,temp.cpu_temp_avg,temp.gpu_temp_avg. nullmeans not measured, on this Mac or not yet. A measured zero is0. Nothing is ever reported as0to fill a gap.- Non-finite numbers are never printed:
NaNbecomesnull(or0for a usage share). - Units are in the name or in the table:
_bytes,_mhz,_c(°C),_power(watts),usage(a share from 0 to 1). _bpsis a rate per second, and the unit differs:network.rx_bpsandtx_bpsare bits per second;disk.read_bps,disk.write_bps,memory.swap_in_bpsandswap_out_bpsare bytes per second.
An example#
From plottypus --demo --json --procs 2, the canned demo session, so you can reproduce it on any Mac. cpu.cores is cut to two entries here; a real sample lists every logical core.
{
"schema_version": 1,
"unix_ms": 1790190625473,
"interval_ms": 1000,
"primed": true,
"soc": {
"name": "M5 Pro",
"e_cores": 0,
"p_cores": 12,
"s_cores": 6,
"gpu_cores": 20,
"memory_bytes": 25769803776
},
"cpu": {
"usage": 0.101194374,
"scaled": 0.025834152,
"freq_mhz": 1260,
"temp_c": 43.437275,
"clusters": [
{
"kind": "performance",
"cores": 12,
"usage": 0.10454839,
"scaled": 0.025833176,
"freq_mhz": 1020
},
{
"kind": "super",
"cores": 6,
"usage": 0.094486326,
"scaled": 0.025836105,
"freq_mhz": 1260
}
],
"cores": [
{
"kind": "performance",
"index": 0,
"usage": 0.15696236
},
{
"kind": "performance",
"index": 1,
"usage": 0.048249885
}
]
},
"gpu": {
"usage": 0.060781993,
"scaled": 0.017491708,
"freq_mhz": 466,
"temp_c": 38.0,
"mem_in_use_bytes": 943718400,
"mem_alloc_bytes": 1468006400
},
"memory": {
"ram_total": 25769803776,
"ram_usage": 13779774464,
"swap_total": 2147483648,
"swap_usage": 859832320,
"wired_bytes": 3328599552,
"compressed_bytes": 1288490240,
"cache_bytes": 6871947776,
"pressure": "nominal",
"swap_in_bps": 0,
"swap_out_bps": 0,
"free_percent": 47
},
"network": {
"iface": "en0",
"rx_bps": 931840,
"tx_bps": 147756,
"primary": "en0"
},
"disk": {
"read_bps": 2057865,
"write_bps": 427981,
"volumes": [
{
"name": "Macintosh HD",
"mount": "/",
"used_bytes": 512400000000,
"total_bytes": 994662584320
}
]
},
"temp": {
"cpu_temp_avg": 43.437275,
"gpu_temp_avg": 38.0,
"hotspot_c": 46.437275,
"e_cluster_c": null,
"p_cluster_c": 41.437275,
"s_cluster_c": 43.437275
},
"fans": [
{
"name": "Fan 1",
"rpm": 0,
"max_rpm": 6550
},
{
"name": "Fan 2",
"rpm": 0,
"max_rpm": 7150
}
],
"thermal_pressure": "nominal",
"sys_power": 6.8392706,
"all_power": 1.3792578,
"cpu_power": 1.0824988,
"gpu_power": 0.29675904,
"ane_power": null,
"ram_power": 0.6202551,
"gpu_freq_mhz": 466,
"battery": {
"percent": 79.0,
"charging": false,
"on_ac": false,
"watts": -6.8392706,
"minutes_remaining": 501
},
"processes": [
{
"pid": 412,
"ppid": 1,
"name": "WindowServer",
"user": "_windowserver",
"cpu_pct": 11.229062,
"cpu_cores": 0.11229062,
"mem_bytes": 432013312,
"threads": 24,
"energy_w": 0.15720686,
"restricted": false,
"status": ""
},
{
"pid": 2240,
"ppid": 2210,
"name": "Safari Web Content",
"user": "dev",
"cpu_pct": 6.6470704,
"cpu_cores": 0.066470705,
"mem_bytes": 421527552,
"threads": 22,
"energy_w": 0.09305899,
"restricted": false,
"status": ""
}
],
"verdict": {
"kind": "normal",
"headline": "All normal",
"evidence": []
},
"health": {
"battery": "ok",
"cpu": "ok",
"disk": "ok",
"fans": "ok",
"gpu": "ok",
"memory": "ok",
"network": "ok",
"processes": "ok",
"sensors": "ok",
"thermal": "ok"
}
}Fields#
Envelope#
| Field | Type | Meaning |
|---|---|---|
schema_version |
int | 1 |
unix_ms |
int | wall-clock time the object was built |
interval_ms |
int | sampling interval (-i; 1000 by default in headless modes) |
primed |
bool | rates were measured over a real interval; always true in printed samples |
Chip#
| Field | Type | Meaning |
|---|---|---|
soc.name |
string | chip name, e.g. Apple M5 Pro |
soc.e_cores, soc.p_cores, soc.s_cores |
int | Efficiency, Performance and Super core counts |
soc.gpu_cores |
int | GPU core count |
soc.memory_bytes |
int | installed memory |
CPU#
| Field | Type | Meaning |
|---|---|---|
cpu.usage |
0–1 | non-idle share across all cores (what Activity Monitor calls CPU) |
cpu.scaled |
0–1 | usage weighted by frequency: Mach busy × the IOReport clock ratio per core. Equals usage where IOReport residency is unavailable |
cpu.freq_mhz |
int or null | package frequency |
cpu.temp_c |
float or null | CPU temperature |
cpu.clusters[] |
object | {kind, cores, usage, scaled, freq_mhz} per cluster. kind is efficiency, performance or super. freq_mhz is the residency-weighted average of the DVFS states, null until the first residency window lands or where IOReport is unavailable |
cpu.cores[] |
object | {kind, index, usage} per logical core, in Mach order |
GPU#
| Field | Type | Meaning |
|---|---|---|
gpu |
object or null | {usage, scaled, freq_mhz, temp_c, mem_in_use_bytes, mem_alloc_bytes}; null without a GPU reading |
gpu_freq_mhz |
int or null | macmon name; the same value as gpu.freq_mhz |
Memory#
| Field | Type | Meaning |
|---|---|---|
memory.ram_total, memory.ram_usage |
bytes | installed and used memory (used as Activity Monitor counts it) |
memory.swap_total, memory.swap_usage |
bytes | swap size and swap in use |
memory.wired_bytes, memory.compressed_bytes |
bytes | wired and compressed memory |
memory.cache_bytes |
bytes | file-backed plus purgeable: Activity Monitor’s “Cached Files” |
memory.pressure |
string | nominal, warn or critical |
memory.swap_in_bps, memory.swap_out_bps |
bytes/s or null | swap activity; null until two samples exist |
memory.free_percent |
0–100 or null | kern.memorystatus_level, the figure memory_pressure prints |
Network and disk#
| Field | Type | Meaning |
|---|---|---|
network.iface |
string | headline interface: the default-route one, else the busiest physical enN |
network.rx_bps, network.tx_bps |
bits/s | from the exact 64-bit ifmib counters |
network.primary |
string or null | interface carrying the default route (en0, a VPN’s utunN); null without one |
disk.read_bps, disk.write_bps |
bytes/s | disk throughput |
disk.volumes[] |
object | {name, mount, used_bytes, total_bytes} |
Temperature and fans#
| Field | Type | Meaning |
|---|---|---|
temp.cpu_temp_avg, temp.gpu_temp_avg |
°C or null | macmon names |
temp.hotspot_c |
°C or null | the hottest die sensor |
temp.e_cluster_c, temp.p_cluster_c, temp.s_cluster_c |
°C or null | per-cluster zones; null for a cluster the chip does not have |
fans[] |
object | {name, rpm, max_rpm}; an empty array on fanless Macs, never a fan at 0 |
thermal_pressure |
string | nominal, fair, serious or critical |
Power#
| Field | Type | Meaning |
|---|---|---|
sys_power |
W or null | whole-system input power (SMC PSTR); null when the SMC has no PSTR |
cpu_power |
W or null | SMC CPU rails on the M5 Pro (Mac17,8), else the IOReport CPU energy counter |
gpu_power |
W or null | IOReport GPU energy |
ane_power, ram_power |
W or null | IOReport ANE and DRAM energy |
all_power |
W or null | sum of the known cpu_power, gpu_power and ane_power; null when none is known |
IOReport rails are held between publications and turn null until two publications have landed, or once the held value is more than 10 s old. On macOS 27 the CPU, ANE and DRAM counters publish in batches seconds to minutes apart, so expect those three to be null often on Macs without SMC CPU rails. See Power and battery.
Battery#
| Field | Type | Meaning |
|---|---|---|
battery |
object or null | {percent, charging, on_ac, watts, minutes_remaining}; null on Macs without a battery |
battery.watts |
W or null | power through the battery: positive in, negative out |
battery.minutes_remaining |
int or null | macOS’s own estimate; null when it has none (for example on AC) |
Processes#
processes[] holds the top --procs N processes by CPU (default 10):
| Field | Type | Meaning |
|---|---|---|
pid, ppid |
int | process and parent id |
name, user |
string | display name and owner |
cpu_pct |
float or null | per-core percent, like Activity Monitor and top: 100 is one fully busy core, so it can exceed 100 |
cpu_cores |
float or null | cpu_pct / 100 |
mem_bytes |
int or null | physical footprint |
threads |
int or null | thread count |
energy_w |
float or null | the process’s energy use, in watts |
restricted |
bool | true when the kernel withholds this process’s details (another user’s process, without sudo) |
status |
string | stopped, zombie, or empty for everything else |
When restricted is true, cpu_pct, cpu_cores, mem_bytes, threads and energy_w are null, never an invented 0.
Verdict and health#
| Field | Type | Meaning |
|---|---|---|
verdict.kind |
string | normal, busy, memory_tight, throttling or battery_drain |
verdict.headline |
string | All normal, Busy: rustc using 9.1 cores, … |
verdict.evidence[] |
string | the readings behind it |
health |
object | per collector: ok, pending, stale, failed or absent |
battery_drain is part of the schema but the 1.0.0 verdict engine never produces it. The rules are on Diagnose and doctor.
Recipes#
# CPU watts, or "n/a" when the Mac has not published one
plottypus --json | jq -r '.cpu_power // "n/a"'
# Per-cluster load and clock
plottypus --json | jq -r '.cpu.clusters[] | "\(.kind)\t\(.usage * 100 | round)%\t\(.freq_mhz // "-") MHz"'
# The three hungriest processes, per-core percent
plottypus --json --procs 3 | jq -r '.processes[] | "\(.cpu_pct // "-")\t\(.name)"'
# Log system watts every 5 s as CSV
plottypus --json-stream -i 5s | jq -r '[.unix_ms, .sys_power] | @csv' >> watts.csv
# Test a script without a busy Mac
plottypus --demo --json | jq .verdictKeep the // "n/a" style defaults: a null is information, not an error.
Coming in 1.1#
CSV export and a status line for tmux, SketchyBar, Starship and SwiftBar are planned for 1.1. They are not in 1.0.0.