From 19201a8390dbe09abd40ef53564ebc8e2f3caa8b Mon Sep 17 00:00:00 2001 From: Oleksij Samorukov Date: Sat, 17 Oct 2026 18:22:04 +0200 Subject: [PATCH 1/2] darwin: add per-process disk I/O columns Add RBYTES, WBYTES, IO_READ_RATE, IO_WRITE_RATE, and IO_RATE process columns on Darwin. macOS has no /proc//io, but proc_pid_rusage(pid, RUSAGE_INFO_V2, ...) provides ri_diskio_bytesread/ri_diskio_byteswritten. Rates are derived from the delta between consecutive scans, using the same formula and PROCESS_FLAG_IO gating as LinuxProcessTable_readIoFile. Field IDs, names, and titles match the Linux platform, so column configuration stays portable. macOS exposes only cumulative byte counters (no RCHAR/WCHAR/SYSCR/SYSCW/ CNCLWB, IO_PRIORITY, or PERCENT_*_DELAY), and the rusage of other users' processes requires root privileges, so those show N/A when unprivileged. The rate delta uses the monotonic clock, so wall-clock adjustments cannot skew the result; LinuxProcessTable_readIoFile still uses the realtime clock for this calculation (to be fixed separately). Signed-off-by: Oleksij Samorukov Assisted-by: DeepSeek V4.1 Flash (opencode) --- darwin/DarwinProcess.c | 49 ++++++++++++++++++++++++++++++++++ darwin/DarwinProcess.h | 15 +++++++++++ darwin/DarwinProcessTable.c | 52 +++++++++++++++++++++++++++++++++++++ darwin/ProcessField.h | 5 ++++ 4 files changed, 121 insertions(+) diff --git a/darwin/DarwinProcess.c b/darwin/DarwinProcess.c index 851a84f540..cced4f2355 100644 --- a/darwin/DarwinProcess.c +++ b/darwin/DarwinProcess.c @@ -10,6 +10,8 @@ in the source distribution for its full text. #include "darwin/DarwinProcess.h" #include +#include +#include #include #include #include @@ -17,7 +19,10 @@ in the source distribution for its full text. #include #include "CRT.h" +#include "Macros.h" #include "Process.h" +#include "Settings.h" +#include "XUtils.h" #include "darwin/DarwinMachine.h" #include "darwin/Platform.h" @@ -52,6 +57,11 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [PROC_EXE] = { .name = "EXE", .title = "EXE ", .description = "Basename of exe of the process from /proc/[pid]/exe", .flags = 0, }, [CWD] = { .name = "CWD", .title = "CWD ", .description = "The current working directory of the process", .flags = PROCESS_FLAG_CWD, }, [TRANSLATED] = { .name = "TRANSLATED", .title = "T ", .description = "Translation info (T translated, N native)", .flags = 0, }, + [RBYTES] = { .name = "RBYTES", .title = " IO_R ", .description = "Bytes of read(2) I/O for the process", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, + [WBYTES] = { .name = "WBYTES", .title = " IO_W ", .description = "Bytes of write(2) I/O for the process", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, + [IO_READ_RATE] = { .name = "IO_READ_RATE", .title = " DISK READ ", .description = "The I/O rate of read(2) in bytes per second for the process", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, + [IO_WRITE_RATE] = { .name = "IO_WRITE_RATE", .title = " DISK WRITE ", .description = "The I/O rate of write(2) in bytes per second for the process", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, + [IO_RATE] = { .name = "IO_RATE", .title = " DISK R/W ", .description = "Total I/O rate in bytes per second", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, }; Process* DarwinProcess_new(const Machine* host) { @@ -63,6 +73,11 @@ Process* DarwinProcess_new(const Machine* host) { this->stime = 0; this->taskAccess = true; this->translated = false; + this->io_read_bytes = 0; + this->io_write_bytes = 0; + this->io_last_scan_time_ms = 0; + this->io_rate_read_bps = NAN; + this->io_rate_write_bps = NAN; this->super.state = UNKNOWN; return (Process*)this; @@ -75,9 +90,24 @@ void Process_delete(Object* cast) { free(this); } +static double DarwinProcess_totalIORate(const DarwinProcess* dp) { + double totalRate = NAN; + if (isNonnegative(dp->io_rate_read_bps)) { + totalRate = dp->io_rate_read_bps; + if (isNonnegative(dp->io_rate_write_bps)) { + totalRate += dp->io_rate_write_bps; + } + } else if (isNonnegative(dp->io_rate_write_bps)) { + totalRate = dp->io_rate_write_bps; + } + return totalRate; +} + static void DarwinProcess_rowWriteField(const Row* super, RichString* str, ProcessField field) { const DarwinProcess* dp = (const DarwinProcess*) super; + const Machine* host = super->host; + bool coloring = host->settings->highlightMegabytes; char buffer[256]; buffer[255] = '\0'; int attr = CRT_colors[DEFAULT_COLOR]; size_t n = sizeof(buffer) - 1; @@ -85,6 +115,11 @@ static void DarwinProcess_rowWriteField(const Row* super, RichString* str, Proce switch (field) { // add Platform-specific fields here case TRANSLATED: xSnprintf(buffer, n, "%c ", dp->translated ? 'T' : 'N'); break; + case RBYTES: Row_printBytes(str, dp->io_read_bytes, coloring); return; + case WBYTES: Row_printBytes(str, dp->io_write_bytes, coloring); return; + case IO_READ_RATE: Row_printRate(str, dp->io_rate_read_bps, coloring); return; + case IO_WRITE_RATE: Row_printRate(str, dp->io_rate_write_bps, coloring); return; + case IO_RATE: Row_printRate(str, DarwinProcess_totalIORate(dp), coloring); return; default: Process_writeField(&dp->super, str, field); return; @@ -101,6 +136,20 @@ static int DarwinProcess_compareByKey(const Process* v1, const Process* v2, Proc // add Platform-specific fields here case TRANSLATED: return SPACESHIP_NUMBER(p1->translated, p2->translated); + case RBYTES: + if (p1->io_read_bytes == ULLONG_MAX || p2->io_read_bytes == ULLONG_MAX) + return (p1->io_read_bytes != ULLONG_MAX) - (p2->io_read_bytes != ULLONG_MAX); + return SPACESHIP_NUMBER(p1->io_read_bytes, p2->io_read_bytes); + case WBYTES: + if (p1->io_write_bytes == ULLONG_MAX || p2->io_write_bytes == ULLONG_MAX) + return (p1->io_write_bytes != ULLONG_MAX) - (p2->io_write_bytes != ULLONG_MAX); + return SPACESHIP_NUMBER(p1->io_write_bytes, p2->io_write_bytes); + case IO_READ_RATE: + return compareRealNumbers(p1->io_rate_read_bps, p2->io_rate_read_bps); + case IO_WRITE_RATE: + return compareRealNumbers(p1->io_rate_write_bps, p2->io_rate_write_bps); + case IO_RATE: + return compareRealNumbers(DarwinProcess_totalIORate(p1), DarwinProcess_totalIORate(p2)); default: return Process_compareByKey_Base(v1, v2, key); } diff --git a/darwin/DarwinProcess.h b/darwin/DarwinProcess.h index a5d66b4906..e38591bc4c 100644 --- a/darwin/DarwinProcess.h +++ b/darwin/DarwinProcess.h @@ -22,6 +22,21 @@ typedef struct DarwinProcess_ { uint64_t stime; bool taskAccess; bool translated; + + /* Storage data read (in bytes) */ + uint64_t io_read_bytes; + + /* Storage data written (in bytes) */ + uint64_t io_write_bytes; + + /* Point in time of last io scan (monotonic milliseconds) */ + uint64_t io_last_scan_time_ms; + + /* Storage data read (in bytes per second) */ + double io_rate_read_bps; + + /* Storage data written (in bytes per second) */ + double io_rate_write_bps; } DarwinProcess; extern const ProcessClass DarwinProcess_class; diff --git a/darwin/DarwinProcessTable.c b/darwin/DarwinProcessTable.c index 9a2f6d5d0c..7b14302f94 100644 --- a/darwin/DarwinProcessTable.c +++ b/darwin/DarwinProcessTable.c @@ -9,6 +9,7 @@ in the source distribution for its full text. #include #include +#include #include #include #include @@ -19,7 +20,9 @@ in the source distribution for its full text. #include #include "CRT.h" +#include "Macros.h" #include "ProcessTable.h" +#include "Settings.h" #include "darwin/DarwinMachine.h" #include "darwin/DarwinProcess.h" #include "darwin/Platform.h" @@ -69,6 +72,51 @@ void ProcessTable_delete(Object* cast) { free(this); } +/* + * Read per-process disk I/O counters (proc_pid_rusage). + * + * Unlike Linux, macOS does not expose separate char/syscall counters; it only + * provides cumulative storage bytes read and written. Rates are computed from + * the difference between consecutive scans, mirroring LinuxProcessTable_readIoFile. + */ +static void DarwinProcessTable_readProcRusage(DarwinProcess* proc) { + const Machine* host = proc->super.super.host; + + struct rusage_info_v2 ri; + if (proc_pid_rusage(Process_getPid(&proc->super), RUSAGE_INFO_V2, (rusage_info_t *)&ri) != 0) { + proc->io_rate_read_bps = NAN; + proc->io_rate_write_bps = NAN; + proc->io_read_bytes = ULLONG_MAX; + proc->io_write_bytes = ULLONG_MAX; + proc->io_last_scan_time_ms = host->monotonicMs; + return; + } + + uint64_t last_read = proc->io_read_bytes; + uint64_t last_write = proc->io_write_bytes; + + /* Only compute a rate once a previous valid sample exists; otherwise the + * delta is meaningless on the first scan and zero after a failed scan. + * Machine_scanTables seeds monotonicMs with 1 on its first scan, so treat + * that sentinel as "no timestamp yet". Use the monotonic clock so that + * wall-clock adjustments cannot produce a zero or inflated time delta. */ + bool haveBaseline = proc->io_last_scan_time_ms > 1 && last_read != ULLONG_MAX; + uint64_t time_delta = saturatingSub(host->monotonicMs, proc->io_last_scan_time_ms); + + proc->io_read_bytes = ri.ri_diskio_bytesread; + proc->io_write_bytes = ri.ri_diskio_byteswritten; + + if (haveBaseline && time_delta) { + proc->io_rate_read_bps = saturatingSub(proc->io_read_bytes, last_read) * /*ms to s*/1000. / time_delta; + proc->io_rate_write_bps = saturatingSub(proc->io_write_bytes, last_write) * /*ms to s*/1000. / time_delta; + } else { + proc->io_rate_read_bps = NAN; + proc->io_rate_write_bps = NAN; + } + + proc->io_last_scan_time_ms = host->monotonicMs; +} + void ProcessTable_goThroughEntries(ProcessTable* super) { const Machine* host = super->super.host; const DarwinMachine* dhost = (const DarwinMachine*) host; @@ -103,6 +151,10 @@ void ProcessTable_goThroughEntries(ProcessTable* super) { DarwinProcess_setFromKInfoProc(&proc->super, &ps[i], preExisting); DarwinProcess_setFromLibprocPidinfo(proc, dpt, time_interval_ns); + if (host->settings->ss->flags & PROCESS_FLAG_IO) { + DarwinProcessTable_readProcRusage(proc); + } + // Deduce further process states not covered in the libproc call above if (ps[i].kp_proc.p_stat == SZOMB) { proc->super.state = ZOMBIE; diff --git a/darwin/ProcessField.h b/darwin/ProcessField.h index 05fbc3ba6c..370637414f 100644 --- a/darwin/ProcessField.h +++ b/darwin/ProcessField.h @@ -10,6 +10,11 @@ in the source distribution for its full text. #define PLATFORM_PROCESS_FIELDS \ TRANSLATED = 100, \ + RBYTES = 107, \ + WBYTES = 108, \ + IO_READ_RATE = 110, \ + IO_WRITE_RATE = 111, \ + IO_RATE = 112, \ \ DUMMY_BUMP_FIELD = CWD, \ // End of list From eb9936795cb0dc05f191a983429deddd4a7f2e71 Mon Sep 17 00:00:00 2001 From: Oleksij Samorukov Date: Sat, 17 Oct 2026 18:22:05 +0200 Subject: [PATCH 2/2] darwin: add I/O screen tab Add a second screen tab ("I/O") on Darwin, mirroring the Linux I/O tab, showing the per-process disk I/O columns: PID USER IO_RATE IO_READ_RATE IO_WRITE_RATE RBYTES WBYTES Command Signed-off-by: Oleksij Samorukov Assisted-by: DeepSeek V4.1 Flash (opencode) --- darwin/Platform.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/darwin/Platform.c b/darwin/Platform.c index 9b7e237918..af18a253c0 100644 --- a/darwin/Platform.c +++ b/darwin/Platform.c @@ -71,6 +71,11 @@ const ScreenDefaults Platform_defaultScreens[] = { .columns = "PID USER PRIORITY NICE M_VIRT M_RESIDENT STATE PERCENT_CPU PERCENT_MEM TIME Command", .sortKey = "PERCENT_CPU", }, + { + .name = "I/O", + .columns = "PID USER IO_RATE IO_READ_RATE IO_WRITE_RATE RBYTES WBYTES Command", + .sortKey = "IO_RATE", + }, }; const unsigned int Platform_numberOfDefaultScreens = ARRAYSIZE(Platform_defaultScreens);