From 681c4bedb35da4eb993cf4dd53126accec6d2aaa Mon Sep 17 00:00:00 2001 From: Alex Samorukov Date: Wed, 7 Oct 2026 21:14:17 +0200 Subject: [PATCH 1/2] FreeBSD: add an I/O screen with per-process I/O operations Linux and PCP provide a second default screen ("I/O") with per-process I/O rate columns, but FreeBSD only ever shipped the "Main" screen. Add an "I/O" screen with IO_OPS, IO_READ_OPS and IO_WRITE_OPS, plus the same three columns to the available fields. The rates are derived from the rusage block-operation counters (ki_rusage.ru_inblock and ki_rusage.ru_oublock) already returned by the kvm_getprocs() call used to enumerate processes, the same source the base system top(1) uses for its "-m io" mode. As these counters report block operations rather than bytes, the columns are named after the operation count, leaving the "rate" suffix to the byte-rate columns. Reuse across platforms is enabled by adding the generic Row_printCountRate() helper to Row.c. The default screen is only created when the configuration has no screens section; existing configurations keep their own set of screens. Assisted-by: DeepSeek V4.1 Flash Signed-off-by: Alex Samorukov --- Row.c | 12 +++++++++-- Row.h | 3 +++ freebsd/FreeBSDProcess.c | 38 +++++++++++++++++++++++++++++++++++ freebsd/FreeBSDProcess.h | 8 ++++++++ freebsd/FreeBSDProcessTable.c | 19 ++++++++++++++++++ freebsd/Platform.c | 5 +++++ freebsd/ProcessField.h | 3 +++ 7 files changed, 86 insertions(+), 2 deletions(-) diff --git a/Row.c b/Row.c index 4fd27797d0..2ac04de791 100644 --- a/Row.c +++ b/Row.c @@ -492,7 +492,7 @@ void Row_printNanoseconds(RichString* str, unsigned long long totalNanoseconds, Row_printTime(str, totalHundredths, coloring); } -void Row_printRate(RichString* str, double rate, bool coloring) { +static void Row_printRateUnit(RichString* str, double rate, bool coloring, char unit) { char buffer[16]; int largeNumberColor = CRT_colors[LARGE_NUMBER]; @@ -525,11 +525,19 @@ void Row_printRate(RichString* str, double rate, bool coloring) { else if (i >= 3) color = largeNumberColor; - char prefix = (i == 0) ? 'B' : unitPrefixes[i - 1]; + char prefix = (i == 0) ? unit : unitPrefixes[i - 1]; int len = xSnprintf(buffer, sizeof(buffer), "%7.2f %c/s ", scaled, prefix); RichString_appendnAscii(str, color, buffer, len); } +void Row_printRate(RichString* str, double rate, bool coloring) { + Row_printRateUnit(str, rate, coloring, 'B'); +} + +void Row_printCountRate(RichString* str, double rate, bool coloring) { + Row_printRateUnit(str, rate, coloring, 'o'); +} + void Row_printLeftAlignedField(RichString* str, int attr, const char* content, unsigned int width) { int columns = width; RichString_appendnWideColumns(str, attr, content, strlen(content), &columns); diff --git a/Row.h b/Row.h index 00a5f88dba..8b378a7032 100644 --- a/Row.h +++ b/Row.h @@ -164,6 +164,9 @@ void Row_printNanoseconds(RichString* str, unsigned long long totalNanoseconds, /* Takes rate in bare unit (base 1024) per second. Prints 12 columns. */ void Row_printRate(RichString* str, double rate, bool coloring); +/* Takes a per-second count (base 1024) of operations. Prints 12 columns. */ +void Row_printCountRate(RichString* str, double rate, bool coloring); + int Row_printPercentage(float val, char* buffer, size_t n, uint8_t width, int* attr); static inline int Row_idEqualCompare(const void* v1, const void* v2) { diff --git a/freebsd/FreeBSDProcess.c b/freebsd/FreeBSDProcess.c index bdbf025279..ccf7081fcf 100644 --- a/freebsd/FreeBSDProcess.c +++ b/freebsd/FreeBSDProcess.c @@ -9,6 +9,7 @@ in the source distribution for its full text. #include "freebsd/FreeBSDProcess.h" +#include #include #include "CRT.h" @@ -16,6 +17,7 @@ in the source distribution for its full text. #include "Process.h" #include "RichString.h" #include "Scheduling.h" +#include "Settings.h" #include "XUtils.h" @@ -57,6 +59,9 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [JAIL] = { .name = "JAIL", .title = "JAIL ", .description = "Jail prison name", .flags = 0, }, [SCHEDCLASS] = { .name = "SCHEDCLASS", .title = "SC", .description = "Scheduling Class (Timesharing, Realtime, Idletime)", .flags = 0, }, [EMULATION] = { .name = "EMULATION", .title = "EMULATION ", .description = "System call emulation environment (ABI)", .flags = 0, }, + [IO_READ_OPS] = { .name = "IO_READ_OPS", .title = " IO READ ", .description = "The I/O read block operations per second for the process", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, + [IO_WRITE_OPS] = { .name = "IO_WRITE_OPS", .title = " IO WRITE ", .description = "The I/O write block operations per second for the process", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, + [IO_OPS] = { .name = "IO_OPS", .title = " IO R/W ", .description = "Total I/O block operations per second", .flags = PROCESS_FLAG_IO, .defaultSortDesc = true, }, }; Process* FreeBSDProcess_new(const Machine* machine) { @@ -82,9 +87,24 @@ static const char FreeBSD_schedclassChars[MAX_SCHEDCLASS] = { [SCHEDCLASS_REALTIME] = 'r', // realtime scheduling }; +static double FreeBSDProcess_totalIOOps(const FreeBSDProcess* fp) { + double totalOps = NAN; + if (isNonnegative(fp->io_read_ops)) { + totalOps = fp->io_read_ops; + if (isNonnegative(fp->io_write_ops)) { + totalOps += fp->io_write_ops; + } + } else if (isNonnegative(fp->io_write_ops)) { + totalOps = fp->io_write_ops; + } + return totalOps; +} + static void FreeBSDProcess_rowWriteField(const Row* super, RichString* str, ProcessField field) { const FreeBSDProcess* fp = (const FreeBSDProcess*) super; + const Machine* host = (const Machine*) super->host; + bool coloring = host->settings->highlightMegabytes; char buffer[256]; buffer[255] = '\0'; char sched_class; int attr = CRT_colors[DEFAULT_COLOR]; @@ -109,6 +129,18 @@ static void FreeBSDProcess_rowWriteField(const Row* super, RichString* str, Proc xSnprintf(buffer, n, " %c", sched_class); break; + case IO_READ_OPS: + Row_printCountRate(str, fp->io_read_ops, coloring); + return; + + case IO_WRITE_OPS: + Row_printCountRate(str, fp->io_write_ops, coloring); + return; + + case IO_OPS: + Row_printCountRate(str, FreeBSDProcess_totalIOOps(fp), coloring); + return; + default: Process_writeField(&fp->super, str, field); return; @@ -131,6 +163,12 @@ static int FreeBSDProcess_compareByKey(const Process* v1, const Process* v2, Pro return SPACESHIP_NULLSTR(p1->emul, p2->emul); case SCHEDCLASS: return SPACESHIP_NUMBER(p1->sched_class, p2->sched_class); + case IO_READ_OPS: + return compareRealNumbers(p1->io_read_ops, p2->io_read_ops); + case IO_WRITE_OPS: + return compareRealNumbers(p1->io_write_ops, p2->io_write_ops); + case IO_OPS: + return compareRealNumbers(FreeBSDProcess_totalIOOps(p1), FreeBSDProcess_totalIOOps(p2)); default: return Process_compareByKey_Base(v1, v2, key); } diff --git a/freebsd/FreeBSDProcess.h b/freebsd/FreeBSDProcess.h index 44c4ceda30..9851ff2270 100644 --- a/freebsd/FreeBSDProcess.h +++ b/freebsd/FreeBSDProcess.h @@ -8,6 +8,7 @@ in the source distribution for its full text. */ #include +#include #include "Object.h" #include "Process.h" @@ -30,6 +31,13 @@ typedef struct FreeBSDProcess_ { char* jname; char* emul; FreeBSDSchedClass sched_class; + + unsigned long long io_last_inblock; /* cumulative block read ops at last sample */ + unsigned long long io_last_oublock; /* cumulative block write ops at last sample */ + unsigned long long io_last_scan_time_ms; /* time of last I/O sample, ms since the Epoch */ + struct timeval io_process_start; /* start time of the sampled process, to detect PID reuse */ + double io_read_ops; /* block read operations per second */ + double io_write_ops; /* block write operations per second */ } FreeBSDProcess; extern const ProcessClass FreeBSDProcess_class; diff --git a/freebsd/FreeBSDProcessTable.c b/freebsd/FreeBSDProcessTable.c index be6356ace8..d34b15a8c9 100644 --- a/freebsd/FreeBSDProcessTable.c +++ b/freebsd/FreeBSDProcessTable.c @@ -306,6 +306,25 @@ void ProcessTable_goThroughEntries(ProcessTable* super) { default: proc->state = UNKNOWN; } + /* Storage I/O rates, derived from the rusage block-operation counters + * (same source as the FreeBSD top(1) "-m io" mode). */ + unsigned long long ioIn = (unsigned long long)kproc->ki_rusage.ru_inblock; + unsigned long long ioOut = (unsigned long long)kproc->ki_rusage.ru_oublock; + unsigned long long ioTimeDelta = saturatingSub(host->realtimeMs, fp->io_last_scan_time_ms); + bool sameProcess = fp->io_process_start.tv_sec == kproc->ki_start.tv_sec && + fp->io_process_start.tv_usec == kproc->ki_start.tv_usec; + if (preExisting && sameProcess && fp->io_last_scan_time_ms && ioTimeDelta) { + fp->io_read_ops = saturatingSub(ioIn, fp->io_last_inblock) * /*ms to s*/1000.0 / ioTimeDelta; + fp->io_write_ops = saturatingSub(ioOut, fp->io_last_oublock) * /*ms to s*/1000.0 / ioTimeDelta; + } else { + fp->io_read_ops = NAN; + fp->io_write_ops = NAN; + } + fp->io_last_inblock = ioIn; + fp->io_last_oublock = ioOut; + fp->io_last_scan_time_ms = host->realtimeMs; + fp->io_process_start = kproc->ki_start; + if (Process_isKernelThread(proc)) super->kernelThreads++; diff --git a/freebsd/Platform.c b/freebsd/Platform.c index ae7f7657b6..e736066775 100644 --- a/freebsd/Platform.c +++ b/freebsd/Platform.c @@ -55,6 +55,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_OPS IO_READ_OPS IO_WRITE_OPS Command", + .sortKey = "IO_OPS", + }, }; const unsigned int Platform_numberOfDefaultScreens = ARRAYSIZE(Platform_defaultScreens); diff --git a/freebsd/ProcessField.h b/freebsd/ProcessField.h index 8bf03b4029..efbf7d3f92 100644 --- a/freebsd/ProcessField.h +++ b/freebsd/ProcessField.h @@ -13,6 +13,9 @@ in the source distribution for its full text. JAIL = 101, \ EMULATION = 102, \ SCHEDCLASS = 103, \ + IO_READ_OPS = 104, \ + IO_WRITE_OPS = 105, \ + IO_OPS = 106, \ \ DUMMY_BUMP_FIELD = CWD, \ // End of list From d1474e7388d077e1bf5640c89f2adff27470606a Mon Sep 17 00:00:00 2001 From: Alex Samorukov Date: Wed, 7 Oct 2026 21:14:17 +0200 Subject: [PATCH 2/2] docs: document the FreeBSD I/O operations columns Add manual entries for IO_READ_OPS, IO_WRITE_OPS and IO_OPS, the FreeBSD columns reporting block operations per second. Assisted-by: DeepSeek V4.1 Flash Signed-off-by: Alex Samorukov --- htop.1.in | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/htop.1.in b/htop.1.in index f140d9ce75..420b75af5b 100644 --- a/htop.1.in +++ b/htop.1.in @@ -603,6 +603,15 @@ The I/O rate of write(2) in bytes per second, for the process. .B IO_RATE (DISK R/W) The I/O rate, IO_READ_RATE + IO_WRITE_RATE (see above). .TP +.B IO_READ_OPS (IO READ) +The I/O rate of block read operations per second, for the process (FreeBSD). +.TP +.B IO_WRITE_OPS (IO WRITE) +The I/O rate of block write operations per second, for the process (FreeBSD). +.TP +.B IO_OPS (IO R/W) +The total I/O rate, IO_READ_OPS + IO_WRITE_OPS (see above) (FreeBSD). +.TP .B CGROUP Which cgroup the process is in. For a shortened view see the CCGROUP column below. .TP