Skip to content

Commit 71d5cab

Browse files
committed
Document macOS telemetry research
1 parent 49cfee7 commit 71d5cab

1 file changed

Lines changed: 323 additions & 0 deletions

File tree

MACOS-TELEMETRY-RESEARCH.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# macOS Media and GPU Workload Detection Research
2+
3+
Status: research only; no implementation is proposed by this document.
4+
5+
This document records an investigation into two missing areas of the Apple
6+
backend:
7+
8+
- hardware video encoder and decoder detection; and
9+
- graphics-versus-compute detection, especially for individual processes.
10+
11+
The central finding is that macOS exposes useful private activity signals, but
12+
not enough information to populate nvtop's existing percentage and per-process
13+
fields accurately. Media sessions and some system-wide activity can be
14+
detected. Trustworthy encoder/decoder utilization percentages and per-process
15+
graphics/compute classification cannot currently be derived.
16+
17+
## nvtop's data requirements
18+
19+
The generic nvtop data model expects:
20+
21+
- `encoder_rate` and `decoder_rate` to be device utilization percentages;
22+
- `encode_usage` and `decode_usage` to be the percentage of the corresponding
23+
engine used by a process; and
24+
- each process to be classified as unknown, graphical, compute, or both.
25+
26+
These contracts are defined in
27+
[`include/nvtop/extract_gpuinfo_common.h`](include/nvtop/extract_gpuinfo_common.h).
28+
They matter because activity, power, bandwidth, and engine residency are not
29+
interchangeable with a capacity-normalized percentage.
30+
31+
If device-level encoder or decoder rates are absent, the common refresh code
32+
may construct them by summing per-process percentages. This behavior is in
33+
[`src/extract_gpuinfo.c`](src/extract_gpuinfo.c), so inaccurate per-process
34+
values would also produce inaccurate device values.
35+
36+
## Research environment and limitations
37+
38+
The live experiments were performed on:
39+
40+
- Apple M5 Max integrated GPU;
41+
- macOS 26.5.2, build 25F84; and
42+
- an ordinary, unentitled user process.
43+
44+
Controlled 4K60 H.264 encode and decode workloads were run through FFmpeg's
45+
VideoToolbox support. Idle and active samples were compared for I/O Registry
46+
objects and IOReport channels.
47+
48+
The observations establish what is available on this machine. Private class
49+
names, registry properties, channel names, and permissions may differ across
50+
M1 through M5 generations and macOS releases.
51+
52+
## Media engine detection
53+
54+
### VideoToolbox sessions in the I/O Registry
55+
56+
Hardware VideoToolbox sessions create private IOKit user clients. The relevant
57+
classes found during this investigation were:
58+
59+
| User client | Apparent role | Observed information |
60+
| --- | --- | --- |
61+
| `AppleAVDUserClient` | Hardware video decoder | Codec, frame size, chroma format, luma bit depth, usage mode, and creation time |
62+
| `AppleAVE2UserClient` | Hardware video encoder | Creator information; few useful session properties |
63+
| `AppleProResUserClient` | ProRes media engine | Class present, but not exercised by the H.264 workloads |
64+
65+
An open user client proves that a hardware media session exists. It does not
66+
prove that the session processed data during the current sampling interval;
67+
applications can retain idle VideoToolbox sessions.
68+
69+
Apple's public VideoToolbox API lets a client require or request hardware
70+
acceleration for
71+
[encoding](https://developer.apple.com/documentation/videotoolbox/kvtvideoencoderspecification_enablehardwareacceleratedvideoencoder?language=objc)
72+
and
73+
[decoding](https://developer.apple.com/documentation/videotoolbox/kvtvideodecoderspecification_enablehardwareacceleratedvideodecoder),
74+
but it does not provide a system-monitor API for engine utilization.
75+
76+
### System-wide activity signals
77+
78+
The M5 Max IOReport legends advertise several media-related signals:
79+
80+
- an `AVE` energy-model channel;
81+
- `AVE0` and `AVE1` system interconnect bandwidth channels;
82+
- AVD, AVE, and ProRes performance-floor residency;
83+
- AVD and ProRes fabric bandwidth histograms; and
84+
- lower-level media performance counters in the `AMC Stats / Perf Counters`
85+
group.
86+
87+
Not all advertised channels were usable by an ordinary process. Subscribing to
88+
the lower-level AMC performance counters failed without additional privilege
89+
or entitlement, and the advertised AVD bandwidth histograms remained zero
90+
during the decoder test.
91+
92+
The controlled tests produced these results:
93+
94+
| Workload | Registry evidence | IOReport evidence |
95+
| --- | --- | --- |
96+
| Idle | No test media session | `AVE` energy delta approximately 6 mJ over one second; media floors stayed at their minimum state |
97+
| 4K60 H.264 hardware encode | `AppleAVE2UserClient` owned by `VTEncoderXPCServ` | `AVE` energy delta approximately 298 mJ over one second; AVE bandwidth and high performance-floor residency increased |
98+
| 4K60 H.264 hardware decode | `AppleAVDUserClient` owned by `VTDecoderXPCServ` | AVD high performance-floor residency increased; `AVE` energy stayed near idle and AVD bandwidth counters remained unavailable or zero |
99+
100+
These signals are sufficient to infer recent system-wide media activity in
101+
some cases, but none has a documented maximum capacity. Consequently:
102+
103+
- energy is a power input, not an encoder occupancy percentage;
104+
- bandwidth is traffic, not media-engine occupancy;
105+
- performance-floor residency is a requested operating state, not time spent
106+
executing work; and
107+
- session presence is state, not utilization.
108+
109+
Mapping any of these to 0 or 100 percent, or calibrating observed peak activity
110+
as 100 percent, would create workload- and machine-dependent values.
111+
112+
### IOKit busy state is not hardware utilization
113+
114+
`IOServiceGetBusyState` does not provide a missing execution-time counter.
115+
Apple documents IOKit busy state as tracking asynchronous service registration,
116+
matching, and termination work. It propagates through the service tree and is
117+
not a measure of device-engine occupancy.
118+
119+
See Apple's
120+
[`IOKitGetBusyState` documentation](https://developer.apple.com/documentation/iokit/1514460-iokitgetbusystate).
121+
122+
### Per-process media attribution
123+
124+
Media user clients were not owned by the application that submitted the work.
125+
They were owned by shared VideoToolbox XPC helpers:
126+
127+
- `VTEncoderXPCServ` for encoding; and
128+
- `VTDecoderXPCServ` for decoding.
129+
130+
The active decoder user client reported `ClientPID=0`, and its
131+
`IOUserClientCreator` identified the XPC helper rather than FFmpeg. Parent PID
132+
or process-name matching therefore cannot recover the originating application.
133+
A shared helper may also multiplex sessions belonging to multiple clients.
134+
135+
Endpoint Security exposes a responsible-process audit token, but it is not a
136+
practical dependency for nvtop. Creating a general Endpoint Security client
137+
requires an Apple-granted restricted entitlement and user approval through Full
138+
Disk Access. Even then, the responsible process describes responsibility for
139+
the helper process; it does not expose ownership of an individual VideoToolbox
140+
session.
141+
142+
See Apple's documentation for
143+
[`responsible_audit_token`](https://developer.apple.com/documentation/endpointsecurity/es_process_t/responsible_audit_token)
144+
and
145+
[`es_new_client`](https://developer.apple.com/documentation/endpointsecurity/3259700-es_new_client).
146+
147+
Timing correlation, helper ancestry, application names, and a "sole active
148+
client" assumption are not reliable enough for process attribution.
149+
150+
### Media conclusion
151+
152+
The available information supports these distinctions:
153+
154+
| Desired result | Supportable now? | Reason |
155+
| --- | --- | --- |
156+
| A hardware media session exists | Yes, using private IOKit classes | User-client presence |
157+
| The system recently performed some encode work | Partially | Strong energy, bandwidth, and floor signals on the tested M5 Max |
158+
| The system recently performed some decode work | Partially | Session presence and performance-floor changes on the tested M5 Max |
159+
| Device encoder or decoder utilization in percent | No | No capacity-normalized counter |
160+
| Per-process encoder or decoder utilization | No | Work is attributed to shared VideoToolbox helpers |
161+
162+
Media activity should therefore not be written into `encoder_rate`,
163+
`decoder_rate`, `encode_usage`, or `decode_usage` without discovering a new
164+
source that already represents utilization as a percentage or provides both
165+
busy time and a meaningful capacity denominator.
166+
167+
## Graphics-versus-compute detection
168+
169+
### Public Metal APIs
170+
171+
Metal distinguishes render and compute work inside the application submitting
172+
the commands. Applications create distinct render and compute command encoders,
173+
as described in Apple's
174+
[Metal command-structure documentation](https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/setting_up_a_command_structure).
175+
176+
Metal capture, counter sampling, and Xcode's visual timeline can preserve that
177+
distinction while profiling an instrumented application. They are not passive,
178+
cross-process monitoring APIs. `MTLCaptureManager`, for example, captures Metal
179+
commands associated with the profiled application's command buffers.
180+
181+
See
182+
[`MTLCaptureManager`](https://developer.apple.com/documentation/metal/MTLCaptureManager)
183+
and Apple's
184+
[Metal visual timeline documentation](https://developer.apple.com/documentation/xcode/analyzing-apple-gpu-performance-using-a-visual-timeline).
185+
186+
### Per-process AGX information
187+
188+
The Apple backend currently enumerates `AGXDeviceUserClient` objects and sums
189+
each client's `AppUsage[].accumulatedGPUTime`. This supplies a useful aggregate
190+
per-process GPU-time delta.
191+
192+
The observed `AppUsage` entries also contained an `API` string, with these
193+
values:
194+
195+
- `Metal`; and
196+
- `GL/CL`.
197+
198+
Neither value permits graphics/compute classification:
199+
200+
- a Metal command queue may contain render work, compute work, or both; and
201+
- `GL/CL` combines OpenGL and OpenCL, which collapses graphics and compute into
202+
a single value.
203+
204+
No render-pass, compute-pass, pipeline-type, or engine-time property was found
205+
in the per-client registry data. `lastSubmittedTime` describes submission
206+
timing, not command type.
207+
208+
The parser for this information is in
209+
[`src/extract_gpuinfo_apple_utils.m`](src/extract_gpuinfo_apple_utils.m).
210+
211+
### Device-level pipeline signals
212+
213+
The AGX accelerator publishes global values named:
214+
215+
- `Device Utilization %`;
216+
- `Renderer Utilization %`; and
217+
- `Tiler Utilization %`.
218+
219+
Its IOReport legend also advertises global GPU pipeline and context-switch
220+
channels, including names such as `GTP`, `FRG`, `CDM`, `TA`, `3D`, and `CL`.
221+
These can potentially describe the shape of total GPU activity, but they do not
222+
carry a PID. Under concurrent workloads there is no reliable way to assign a
223+
change in one of these global counters to a particular process.
224+
225+
Correlating a global renderer, tiler, or compute-like counter with the process
226+
that happened to accumulate the most GPU time during the same interval would
227+
be a heuristic. It would fail as soon as graphical and compute workloads run
228+
concurrently.
229+
230+
Other macOS monitoring implementations reach the same practical boundary. For
231+
example, [`metop`](https://pypi.org/project/metop/) reports global device,
232+
renderer, and tiler values and derives per-process activity from
233+
`accumulatedGPUTime`; it does not derive a per-process graphics/compute split.
234+
235+
### Current nvtop behavior
236+
237+
The Apple backend currently assigns every discovered process
238+
`gpu_process_graphical_compute`, displayed as `Both G+C`. This occurs in
239+
[`src/extract_gpuinfo_apple_utils.m`](src/extract_gpuinfo_apple_utils.m).
240+
241+
`Both G+C` is not a verified classification, but changing the backend to
242+
`gpu_process_unknown` would currently introduce a worse error in the
243+
interactive interface. The UI displays:
244+
245+
- `Both G+C` for `gpu_process_graphical_compute`;
246+
- `Graphic` for `gpu_process_graphical`; and
247+
- `Compute` for every other value, including `gpu_process_unknown`.
248+
249+
That behavior is in [`src/interface.c`](src/interface.c). Proper unknown-state
250+
rendering is required before the Apple backend can report uncertainty honestly.
251+
252+
The API string should not be used to relabel processes. In particular, Metal
253+
must not be treated as evidence of both graphics and compute, and `GL/CL` must
254+
not be classified from the process name.
255+
256+
### Graphics/compute conclusion
257+
258+
| Desired result | Supportable now? | Reason |
259+
| --- | --- | --- |
260+
| Aggregate per-process GPU activity | Yes | Delta of `accumulatedGPUTime` |
261+
| Process used the Metal API | Yes, privately | `AppUsage[].API` |
262+
| Process performed graphics work | No | No per-client render-engine signal |
263+
| Process performed compute work | No | No per-client compute-engine signal |
264+
| System-wide renderer/tiler activity | Yes, privately | Global AGX performance properties |
265+
| Attribute renderer/tiler activity to a PID | No | Counters are global |
266+
267+
## Implications for a future design
268+
269+
The first design change should make the model represent what macOS actually
270+
provides instead of coercing activity into percentage fields.
271+
272+
1. Add correct interactive display support for `gpu_process_unknown`.
273+
2. Report Apple process type as unknown, or retain `Both G+C` until the UI can
274+
represent unknown without calling it compute.
275+
3. If media detection is added, represent session presence or recent activity
276+
separately from encoder and decoder utilization percentages.
277+
4. Leave `ENC%` and `DEC%` unavailable unless a capacity-normalized source is
278+
found.
279+
5. Consider exposing Renderer and Tiler as separate Apple device-level metrics,
280+
not as evidence for per-process classification.
281+
6. Keep private-property and IOReport channel resolution isolated behind the
282+
Apple backend because names and availability are not stable APIs.
283+
284+
The Apple backend currently sets `encode_decode_shared = true` in
285+
[`src/extract_gpuinfo_apple.m`](src/extract_gpuinfo_apple.m). The observed Apple
286+
media engines are distinct, so this appears to be placeholder behavior rather
287+
than a description of the hardware. It should become false if separate media
288+
metrics or activity indicators are eventually exposed. Changing it alone would
289+
not add usable metrics.
290+
291+
## Validation required before implementation
292+
293+
Any future implementation based on private telemetry should be tested across:
294+
295+
- M1, M2, M3, M4, and M5 families;
296+
- idle sessions versus continuously active sessions;
297+
- low-resolution and 4K workloads;
298+
- H.264, HEVC, AV1, and ProRes where supported;
299+
- simultaneous encode and decode;
300+
- simultaneous graphical and compute GPU workloads; and
301+
- ordinary-user operation without root, entitlements, or additional privacy
302+
permissions.
303+
304+
For each media workload, validation should distinguish session existence,
305+
recent activity, power, bandwidth, performance-state residency, busy time, and
306+
true capacity-normalized utilization. A source should only populate nvtop's
307+
percentage fields if the final category is actually available.
308+
309+
## Summary
310+
311+
- Private IOKit data can identify open Apple decoder, encoder, and ProRes
312+
sessions, subject to chip and OS differences.
313+
- M5 Max IOReport channels can reveal some system-wide media activity, but the
314+
signals are energy, bandwidth, or performance-state residency rather than
315+
utilization percentages.
316+
- VideoToolbox XPC helpers prevent reliable attribution of media sessions to
317+
originating applications.
318+
- Per-process AGX data provides aggregate GPU time and an ambiguous API label,
319+
not render-versus-compute engine time.
320+
- Global Renderer and Tiler values may be useful device metrics but cannot
321+
classify processes.
322+
- The honest near-term behavior is to leave media percentages unavailable and
323+
represent process type as unknown once the interface supports it correctly.

0 commit comments

Comments
 (0)