# macOS 27 IOReport Energy Model publication trace (M5 Pro)

Two raw traces of the IOReport "Energy Model" running totals, read without sudo on one
MacBook Pro running macOS 27.0. They show the batching behind "0.00 W CPU" on macOS 27. The
PMGR-owned channels (`CPU Energy`, `DRAM0`) only change when the driver publishes a batch.
`GPU Energy`, which the AGX driver owns, gets a new timestamp on every read.

Page: https://plottypus.com/data/macos27-energy-publication-trace ·
Write-up: https://plottypus.com/blog/macos-27-cpu-power-0w · License: CC BY 4.0

## The traces

| `trace_id` | Started (IST, +05:30) | Cadence | Samples | Span | What it shows |
|---|---|---:|---:|---:|---|
| `2026-09-23-batching-5s` | 2026-09-23 17:52:55 | 5 s | 170 | 847.0 s | 23 distinct `CPU Energy` timestamps: 22 gaps of 2.2 to 108.5 s |
| `2026-09-24-silent-1s` | 2026-09-24 00:27:07 | 1 s | 360 | 362.8 s | no `CPU Energy` publication at all; its age runs from 1,397.96 s to 1,760.74 s |

The machine alternated between idle and parallel builds during the first trace. The second
ran under heavy parallel load (1-minute load average 32.18 at 00:27 and 94.36 at 00:32 in the
same session's logs, on 18 cores).

## Machine

MacBook Pro Mac17,8, Apple M5 Pro (6 Super + 12 Performance cores, 20-core GPU, 24 GB),
macOS 27.0 (build 26A428). No sudo, no entitlements.

## Files

| File | Rows | One row is |
|---|---:|---|
| `samples.csv` | 1,590 | one channel at one sample (3 channels × 530 samples) |
| `samples.ndjson` | 1,590 | the same, one JSON object per line (`null` where the CSV cell is empty) |
| `publications.csv` | 44 | one `CPU Energy` or `DRAM0` publication seen inside a trace, with the rate over its window |
| `publications.ndjson` | 44 | the same, one JSON object per line |

### `samples.csv` columns

| Column | Unit | Meaning |
|---|---|---|
| `trace_id` | | which trace (table above) |
| `sample` | | 0-based sample index within the trace |
| `sample_ticks` | mach ticks | when the sample was taken: the `GPU Energy` driver timestamp plus its printed age (±5 ms) |
| `elapsed_s` | s | seconds since the trace's first sample |
| `unix_time_s` | s | wall clock, UTC epoch. Anchored on the capture file's creation time (see Time below) |
| `channel` | | `CPU Energy`, `DRAM0` or `GPU Energy` |
| `unit` | | unit of `raw_value`: `mJ` for the PMGR channels, `nJ` for `GPU Energy` |
| `raw_value` | `unit` | the running total, `IOReportSimpleGetIntegerValue(channel, 0)` |
| `energy_j` | J | `raw_value` converted to joules (exact) |
| `driver_ts_ticks` | mach ticks | the driver-side timestamp, the `u64` at byte 24 of the channel's `RawElements` (`IOReportElement.timestamp`) |
| `age_s` | s | `(now − driver_ts_ticks) × 125/3 ns`, as the probe printed it (2 decimals) |
| `published` | bool | `driver_ts_ticks` differs from the previous sample's; empty on sample 0 |
| `interval_s` | s | `elapsed_s` minus the previous sample's |
| `delta_j` | J | `energy_j` minus the previous sample's |
| `per_interval_w` | W | `delta_j / interval_s`: what a per-interval sampler prints |
| `held_rate_w` | W | energy over the last complete publication window seen in this trace, divided by that window. Empty until two publications have been seen |
| `held_window_s` | s | the length of that window, from the driver timestamps |
| `stale_over_10s` | bool | `age_s > 10`: plottypus 1.0.0 shows `—` instead of a held value this old |

### `publications.csv` columns

| Column | Unit | Meaning |
|---|---|---|
| `trace_id`, `channel` | | as above |
| `publication` | | 1-based index within the trace (0 would be the value already published when the trace began) |
| `driver_ts_ticks` | mach ticks | timestamp of this publication |
| `energy_j` | J | running total at this publication |
| `first_seen_sample`, `first_seen_elapsed_s` | –, s | the first sample that showed it |
| `window_s` | s | time since the previous publication, from the driver timestamps |
| `window_energy_j` | J | energy over that window |
| `window_rate_w` | W | `window_energy_j / window_s`: the true average power over the batch |

## How it was captured

A small C probe (`rawts`, from the plottypus IOReport spike) subscribes to the whole
"Energy Model" group with `IOReportCreateSubscription`, then in a loop calls
`IOReportCreateSamples`, reads `mach_absolute_time()` right after, and prints value, driver
timestamp and age for `CPU Energy`, `DRAM0` and `GPU Energy`, then sleeps for the cadence.
`mach_timebase_info` is 125/3 on Apple Silicon, so one tick is 41.67 ns and a second is
24,000,000 ticks. The published single-file equivalent is
https://plottypus.com/media/post/cpu-energy-hold.c.

The probe prints raw integers without units. The units (`mJ` for the PMGR channels, `nJ` for
`GPU Energy`) are what `IOReportChannelGetUnitLabel` returns for these channels on this chip.
The SMC calibration dataset cross-checks the `CPU Energy` scale: over four publication spans
its rate and the SMC CPU rails agree to within about 10%.

### Time

Each sample's time comes from the `GPU Energy` driver timestamp plus its printed age, since
that driver refreshes the timestamp on every read. The wall clock (`unix_time_s`) is the capture
file's creation time plus `elapsed_s`. For the 5 s trace, the file's last write lands 0.07 s
after the computed time of the last sample. For the 1 s trace, the creation time falls in the
second printed by `date` just before the probe started (00:27:07 +0530). Treat `unix_time_s`
as good to about ±0.1 s. `elapsed_s` and the tick columns are exact.

## Numbers you can check

From `samples.csv` and `publications.csv`, trace `2026-09-23-batching-5s`, channel `CPU Energy`:

| | Value |
|---|---:|
| samples, 5 s apart | 170 |
| distinct driver timestamps (publications seen, counting the one already there at the start) | 23 |
| gaps between them: min / median / mean / max | 2.23 / 29.04 / 36.16 / 108.48 s |
| intervals with `delta_j` exactly 0 | 147 of 169 |
| largest `per_interval_w` | 588.69 W |
| `window_rate_w` min / max | 2.05 / 44.73 W |

Trace `2026-09-24-silent-1s`: `CPU Energy` and `DRAM0` keep one timestamp for all 360
samples, so all 359 `delta_j` are 0. `GPU Energy` gets a new timestamp on all 359 intervals.
Its value is unchanged on 3 of them (samples 94 to 96).

## Limits

- One machine, one OS build. The batching is a PMGR-driver behaviour, so I expect it on
  other chips. I have not captured them.
- A 5 s sampler can see two publications inside one interval, so gaps under 5 s may be
  undercounted.
- The trigger for a publication is unknown. Load alone does not cause one: the second trace
  ran under heavy load and saw none.

## License and citation

CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). Use it for anything, with credit.
This license covers the data files in this folder only, not the plottypus software.

> Sheth, L. (2026). macOS 27 IOReport Energy Model publication trace (M5 Pro) (Version 1.0)
> [Data set]. plottypus.com. https://plottypus.com/data/macos27-energy-publication-trace

Contact: Lay Sheth, hello@plottypus.com.

## Checksums (SHA-256)

```text
d0e0bd0a3c98379198d6b83757aaf75687bfaab31eae0be0e286ac7fe1ec0029  publications.csv
99f56c57b8a720c0a5c03246f02dbecbd52e31c6ea666aabece84336d5ae50f4  publications.ndjson
0d119f5ea8f20d5156feaa179f1a563237ddbbba7a5369479eb7926903d38cef  samples.csv
28ea86a3c1d93898c67d20b9970b76b5336df1a29d458ea1dd1fd280f27c7151  samples.ndjson
```

Verify with `shasum -a 256 -c` against these lines, or against https://plottypus.com/data/SHA256SUMS.
