// cpu-energy-hold.c: read IOReport "CPU Energy" without sudo and turn it into watts // two ways: the naive per-interval delta, and a rate held by the driver timestamp. // Build: clang -O2 -Wall -o cpu-energy-hold cpu-energy-hold.c -framework CoreFoundation -lIOReport // Run: ./cpu-energy-hold [interval_ms=1000] [samples=30] // Tested on macOS 27.0 (Mac17,8, M5 Pro). libIOReport is private: these prototypes // match what macmon and other open-source samplers declare, not an Apple header. #include #include #include #include #include #include #include typedef struct IOReportSubscription *IOReportSubscriptionRef; CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef, CFStringRef, uint64_t, uint64_t, uint64_t); IOReportSubscriptionRef IOReportCreateSubscription(void *, CFMutableDictionaryRef, CFMutableDictionaryRef *, uint64_t, CFTypeRef); CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef, CFMutableDictionaryRef, CFTypeRef); CFStringRef IOReportChannelGetChannelName(CFDictionaryRef); CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef); int64_t IOReportSimpleGetIntegerValue(CFDictionaryRef, int32_t); static double joules_per_unit(CFStringRef unit) { if (unit && CFStringCompare(unit, CFSTR("mJ"), 0) == 0) return 1e-3; if (unit && CFStringCompare(unit, CFSTR("nJ"), 0) == 0) return 1e-9; return 1e-6; // "uJ" / "µJ" } // One raw sample of "CPU Energy": running total in joules plus the driver's timestamp. static int read_cpu_energy(IOReportSubscriptionRef sub, CFMutableDictionaryRef subbed, double *joules, uint64_t *driver_ts) { CFDictionaryRef s = IOReportCreateSamples(sub, subbed, NULL); if (!s) return 0; CFArrayRef chans = CFDictionaryGetValue(s, CFSTR("IOReportChannels")); int found = 0; for (CFIndex i = 0; chans && i < CFArrayGetCount(chans); i++) { CFDictionaryRef ch = CFArrayGetValueAtIndex(chans, i); CFStringRef name = IOReportChannelGetChannelName(ch); if (!name || CFStringCompare(name, CFSTR("CPU Energy"), 0) != 0) continue; *joules = (double)IOReportSimpleGetIntegerValue(ch, 0) * joules_per_unit(IOReportChannelGetUnitLabel(ch)); // RawElements holds an IOReportElement: u64 provider_id, u64 channel_id, // 8-byte channel type, then u64 timestamp (mach absolute ticks) at byte 24. CFDataRef raw = CFDictionaryGetValue(ch, CFSTR("RawElements")); *driver_ts = 0; if (raw && CFDataGetLength(raw) >= 32) memcpy(driver_ts, CFDataGetBytePtr(raw) + 24, 8); found = 1; break; } CFRelease(s); return found; } int main(int argc, char **argv) { int ms = argc > 1 ? atoi(argv[1]) : 1000, n = argc > 2 ? atoi(argv[2]) : 30; mach_timebase_info_data_t tb; mach_timebase_info(&tb); double sec_per_tick = (double)tb.numer / tb.denom / 1e9; CFDictionaryRef group = IOReportCopyChannelsInGroup(CFSTR("Energy Model"), NULL, 0, 0, 0); if (!group) return 1; CFMutableDictionaryRef desired = CFDictionaryCreateMutableCopy(NULL, 0, group); CFMutableDictionaryRef subbed = NULL; IOReportSubscriptionRef sub = IOReportCreateSubscription(NULL, desired, &subbed, 0, NULL); if (!sub) return 1; double j_prev = 0, j_pub = 0, held_w = -1; uint64_t ts_pub = 0; if (!read_cpu_energy(sub, subbed, &j_prev, &ts_pub)) return 1; j_pub = j_prev; int pubs = 0; printf("%4s %12s %12s %9s\n", "t_s", "naive_W", "held_W", "age_s"); for (int i = 1; i <= n; i++) { usleep((useconds_t)ms * 1000); double j; uint64_t ts; if (!read_cpu_energy(sub, subbed, &j, &ts)) return 1; double naive_w = (j - j_prev) / (ms / 1e3); // what a per-interval sampler shows j_prev = j; if (ts != ts_pub) { // a new publication: rate over the driver's own window double span = (double)(ts - ts_pub) * sec_per_tick; if (ts > ts_pub && span >= 0.01 && j >= j_pub) { held_w = (j - j_pub) / span; pubs++; } else { held_w = -1; // counter reset or clock went backwards: start over } ts_pub = ts; j_pub = j; } double age = (double)(mach_absolute_time() - ts_pub) * sec_per_tick; if (held_w >= 0 && age <= 10.0) printf("%4d %12.2f %12.2f %9.1f\n", i * ms / 1000, naive_w, held_w, age); else // never a fake zero: no publication yet, or the last one is stale printf("%4d %12.2f %12s %9.1f\n", i * ms / 1000, naive_w, "-", age); } fprintf(stderr, "%d publications in %d samples\n", pubs, n); return 0; }