Scripting

JSON output

sh
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_version is 1. 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.
  • null means not measured, on this Mac or not yet. A measured zero is 0. Nothing is ever reported as 0 to fill a gap.
  • Non-finite numbers are never printed: NaN becomes null (or 0 for 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).
  • _bps is a rate per second, and the unit differs: network.rx_bps and tx_bps are bits per second; disk.read_bps, disk.write_bps, memory.swap_in_bps and swap_out_bps are 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.

json
{
  "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#

sh
# 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 .verdict

Keep 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.