Add macOS per-process GPU sorting - #2
Draft
Andrew Gazelka (andrewgazelka) wants to merge 4 commits into
Draft
Conversation
Read each process's real working directory from the kernel (the cwd set by chdir(2)), not the stale $PWD environment variable: - Linux: readlink /proc/<pid>/cwd - macOS: proc_pidinfo(PROC_PIDVNODEPATHINFO) Surface it on the bottom row of the detail box's command area, but only when the command fits in <= 2 rows so a long command is never truncated for it. Other platforms leave cwd empty (field defaults to "").
Adds "io read", "io write", "io total" sort options to the process view, with per-column sort logic and macOS io_read_rate/io_write_rate collection. Rebase note (base d03b839 -> 9527231): upstream moved the Proc::sort_vector definition out of the btop_shared.hpp header into btop_shared.cpp, leaving an `extern` declaration behind. The three io entries are inserted into the relocated .cpp definition between "memory" and "cpu direct" so their positions (indices 6/7/8) line up with the io cases in the sorting switch.
Tree equals the full patch series applied linearly; parents are the patch DAG heads. Built by the jj megamerge migration (index).
Adds a "gpu" sort option to the process view showing per-process GPU
utilization on Apple Silicon, alongside the existing io read/write/total
columns.
macOS exposes no public per-process GPU API, but every Metal client owns an
AGXDeviceUserClient under the IOAccelerator whose AppUsage array carries a
monotonic accumulatedGPUTime in nanoseconds. Delta that against wall time and
you get the same shape as cpu_p. It needs no entitlement and no root, and it
is the same source Activity Monitor's GPU column uses.
Two alternatives were measured and rejected:
- task_info(TASK_POWER_INFO_V2) compiles, returns KERN_SUCCESS with the full
expected count, and its gpu_energy fields read 0 forever on Apple Silicon.
Probed as root against a process holding the GPU at 100% HW active
residency: task_gpu_utilisation = 0. It also needs task_for_pid, which
fails for most pids without root.
- macOS 27's metalperftrace only observes processes that present a
CoreAnimation layer. A headless Metal compute job that drove global GPU
utilization from 59% to 98% appeared zero times in a retroactive trace,
and `metalperftrace listen` emitted nothing at all. It is a frame-pacing
instrument, not a utilization one.
Verified on macOS 27.0 (M5 Max, AGXAcceleratorG17X). A headless compute burner
with no window is attributed correctly and sorts into place; per-process shares
sum to the driver's own device-level "Device Utilization %". One registry walk
covers every pid and costs ~1.5 ms against the 2000 ms default cadence, so it
is sampled once per cycle rather than per process.
Two API traps worth recording: the user clients are !registered, so
IOServiceGetMatchingServices("AGXDeviceUserClient") silently returns an empty
iterator that reads as "no GPU activity" -- they have to be walked under the
IOAccelerator. And IOUserClientCreator is a formatted string whose process name
the kernel truncates to 16 chars, so entries must be joined on pid, never name.
AppUsage/accumulatedGPUTime are undocumented and unversioned, consistent with
the IOReport and IOHIDEvent private APIs the existing macOS GPU backend already
relies on. A pid drops out of the map once it releases its last Metal client,
which is treated as zero rather than as a negative delta.
Like the io columns, "gpu" reuses the Cpu% slot and suppresses the per-process
cpu sparkline while active, since the proc row has no spare width.
Member
Author
|
Differential verification against the unpatched Both binaries driven through a pty into a terminal emulator. 🤖 Generated with Claude Code |
ix-mirror-sync
Bot
force-pushed
the
ix-patched
branch
from
July 30, 2026 16:00
df4c423 to
d1ec1ec
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a
gpusort option to the process view showing per-process GPU utilization on Apple Silicon, next to the existingio read/io write/io totalcolumns.Motivation: btop has device-level GPU support on macOS (
Gpu::AppleSilicon, IOReport) but no per-process GPU accounting on any platform —btop_shared.hppstill carries// vector<proc_info> graphics_processes = {}; // TODO.Live output
Headless Metal compute burner running, sorted by
gpu:Where the data comes from
Every Metal client owns an
AGXDeviceUserClientunder theIOAcceleratorcarrying a monotonic per-process nanosecond counter:Delta over wall time gives the same shape as
cpu_p. No entitlement, no root. This is the source Activity Monitor's GPU column uses (it linkslibIOReportand carries_gpuUsedSecondsDeltaper row).Alternatives measured and rejected
task_info(TASK_POWER_INFO_V2)— compiles, returnsKERN_SUCCESSwith the full expected count, andgpu_energy.task_gpu_utilisationreads 0 forever on Apple Silicon. Probed as root against a process holding the GPU at 100% HW active residency (GPU Power: 54195 mW): still 0. Fleet-wide as root,gpu_util nonzero=0across 270 accessible pids. It also needstask_for_pid, which fails for most pids without root. This one is dangerous because it looks correct on an idle machine.metalperftrace— a frame-pacing instrument, not a utilization one. Its schema is per-CoreAnimation-layer (FPS, frame time, skipped frames). A headless Metal compute job that drove global GPU utilization from 59% → 98% appeared zero times in a retroactivecollect --last 5mtrace, andmetalperftrace listenemitted 0 bytes over 40s even with several apps rendering at 40–120 FPS, with and withoutsetup --enable per-frame-metrics, and as root.Two API traps
!registered, soIOServiceGetMatchingServices("AGXDeviceUserClient")returns an empty iterator with no error — which reads as "no GPU activity". They have to be walked under theIOAccelerator(which does match, via itsAGXAcceleratorG17Xsubclass).IOUserClientCreatoris a formatted string and the kernel truncates the process name to 16 chars ("pid 1019, TextInputMenuAge"). Join on pid, never on name.Cost
One registry walk covers every pid — sampled once per collect cycle, not per process. Measured ~1.5 ms against the 2000 ms default cadence. Cheaper than the existing per-process
proc_pidinfo/proc_pid_rusagesyscalls, which run once per process.Layout
Like the io columns,
gpureuses theCpu%slot and suppresses the per-process cpu sparkline while active, since the proc row has no spare width. Inserted intosort_vectorbetweenio totalandcpu direct; all four positionalswitchblocks renumbered accordingly.Verification
AGXAcceleratorG17X, Apple clang 21.cmake --buildclean, no new warnings;btop_test3/3 passing."Device Utilization %"fromIOAccelerator.PerformanceStatisticsat the same instant (97.18% vs 97).Not verified
AppUsage, the column reads 0% rather than blank — arguably it should distinguish unsupported from idle.cross = truedarwin build, only a native Apple clang build.AppUsage/accumulatedGPUTimeare undocumented and unversioned and can break on any macOS update. This is consistent with the IOReport and IOHIDEvent private APIs the existing macOS GPU backend already depends on, but it is a real risk.🤖 Generated with Claude Code
Note
Add macOS per-process GPU utilization sorting to the process list
collect_gpu_timesfunction in btop_collect.cpp that reads accumulated GPU time per PID from IORegistry (IOAccelerator/AGXDeviceUserClient) without elevated privileges, computinggpu_pas a percentage over each sampling interval.proc_infowithgpu_p,gpu_t, andgpu_sfields and adds"gpu"as a selectable sort key insort_vector,proc_sorter, andtree_sort."Gpu%"in the CPU column header and display per-process GPU usage when GPU sorting is active, suppressing inline CPU graphs (same layout as IO sorting)."gpu"as a valid sort option.Macroscope summarized 25a5a37.