Skip to content

Bring the Windows / macOS / FreeBSD columns up to parity, and fix bugs - #976

Open
CarterLi wants to merge 34 commits into
dalance:masterfrom
CarterLi:master
Open

CarterLi wants to merge 34 commits into
dalance:masterfrom
CarterLi:master

Conversation

@CarterLi

@CarterLi CarterLi commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This is a fairly large change set. It started as "Windows: adds external pager support" and grew into a pass over all four platforms: the platforms that had no WorkDir, Env or --thread support now have them, the macOS backend was rewritten to stop using libproc for enumeration and no longer drops system processes, FreeBSD dropped its bsd-kvm dependency, and a handful of correctness bugs found along the way were fixed.

The Column trait signature change (see Row keys below) is the one part that touches almost every file, but it is mechanical.

What's new

Windows

  • --thread now reports thread names set through SetThreadDescription.
  • External pager: [pager] command is launched as an external pager. The built-in pager is still the default and is what you get when command is unset. This is useful because the built-in pager doesn't support horizontal scroll.

macOS

  • The process table now comes from sysctl(CTL_KERN, KERN_PROC, KERN_PROC_ALL) / KERN_PROC_UID instead of libproc. System processes are no longer dropped silently.
  • Env, FileName and WorkDir columns.
  • --thread support, and thread states are mapped onto the same state codes as the other platforms.

FreeBSD

  • WorkDir column
  • --thread support (KERN_PROC_INC_THREAD).

All platforms

show_user_only in the [display] section, and the --user (-u) option, to list the processes of one user.

  • The value is "all" (the default), "myself" — the effective user procs runs as — a user name, or a uid in decimal. On Windows, which has no uid, anything that is not one of the two keywords is read as a full SID (S-1-5-21-...-1001) or as an account name (DOMAIN\name included).
  • The option takes an optional value, so a bare --user (or -u) means "myself". The command line wins over the configuration file, as it does for the other options that have both.
  • The user is resolved once, before the listing starts, and a value that matches no account is an error rather than an empty listing. On Windows the whole SID is what the filter compares, so an account is kept only if it is that account. The filtering happens while the process table is collected, so the per-process reads that follow are skipped for the processes that are dropped.

Implementation notes

Row keys: threads carry their thread id negated, and pid is now i64

Threads and processes share the same HashMap in every column, keyed by pid. Adding thread support meant a thread row needed a key that cannot collide with a pid or with a thread of another process, so a thread row is keyed by -tid (thread_key) and the id is read back out (thread_id). row_sort_key gives the ordering: by the id behind the key, with the process before a thread that was handed the same number, so a thread lands next to its process instead of below every process. pid is i64 throughout because a macOS thread id is 64 bit and does not fit in i32.

The Column trait and View follow the type change; ProcessInfoBase was extracted to hold pid / ppid / interval for all four platforms, so column code still reads proc.pid and a platform's own ProcessInfo keeps its kernel-specific fields.

Dependencies bsd-kvm / bsd-kvm-sys are gone on FreeBSD

Enumeration and the per-process reads go through libc::sysctl and kinfo_proc directly. Two reasons, both of which only got worse once threads were in play:

  • Performance. bsd_kvm::get_process wraps kvm_getargv and kvm_getenvv, so every process had its arguments and environment read on every call. With KERN_PROC_INC_THREAD added — where the kernel emits a process once per thread — that became one kvm_getargv/kvm_getenvv pair per thread of every process, all answering for the same process. Going through sysctl also means the reads are made only by the columns that need them (Command, Env, WorkDir) rather than by the collector, and are skipped when those columns are not displayed.
  • Build reliability. bsd-kvm-sys generates its bindings with bindgen against the running system's headers. FreeBSD's headers move, and a binding that comes out wrong takes the whole build with it — I hit exactly that on my machine.

None of this is meant as a criticism of the crates: kvm_* is a userspace wrapper around the same sysctl calls, and its real strength — reading kernel dumps — is not something procs uses. If you would rather keep them and have the argument and environment reads moved out of get_process instead, I'm happy to reopen that.

Bug fixes

  • Recycled pid. A pid reused between two samples was reported against the previous process's counters, so a rate column would print a bogus spike. All four platforms now compare the start time of the two samples and drop the row when it changed.
  • Gid / Group on macOS reported the real group. They read pbi_gid (p_rgid), which made them a copy of GidReal / GroupReal; they now report the effective group, matching the other platforms.
  • Threads on Windows are now correctly renderred in procs --tree --thread.

Performance (Both are run on real hardware, with default config)

FreeBSD 15.1, amd64

Benchmark 1: procs
  Time (mean ± σ):     112.1 ms ±   1.4 ms    [User: 4.6 ms, System: 7.1 ms]
  Range (min … max):   108.9 ms … 114.5 ms    26 runs
 
Benchmark 2: target/release/procs
  Time (mean ± σ):     108.7 ms ±   1.7 ms    [User: 3.4 ms, System: 5.1 ms]
  Range (min … max):   105.1 ms … 112.1 ms    27 runs
 
Summary
  target/release/procs ran
    1.03 ± 0.02 times faster than procs

Slightly faster due to fewer syscalls. See Implementation notes

macOS 26.6.2, aarch64

Benchmark 1: procs
  Time (mean ± σ):     140.7 ms ±   5.1 ms    [User: 15.5 ms, System: 16.7 ms]
  Range (min … max):   128.5 ms … 152.0 ms    21 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Benchmark 2: target/release/procs
  Time (mean ± σ):     149.8 ms ±   1.4 ms    [User: 20.8 ms, System: 21.1 ms]
  Range (min … max):   146.2 ms … 151.7 ms    19 runs

Summary
  procs ran
    1.06 ± 0.04 times faster than target/release/procs

Slightly slower due to adding system processes. With --user myself the performance is almost the same

Benchmark 1: procs
  Time (mean ± σ):     138.8 ms ±   3.7 ms    [User: 15.4 ms, System: 15.6 ms]
  Range (min … max):   129.5 ms … 147.0 ms    22 runs

Benchmark 2: target/release/procs --user myself
  Time (mean ± σ):     139.0 ms ±   3.5 ms    [User: 16.0 ms, System: 15.4 ms]
  Range (min … max):   126.8 ms … 142.6 ms    20 runs

Summary
  procs ran
    1.00 ± 0.04 times faster than target/release/procs --user myself

Screenshots

FreeBSD

image

macOS

image

Windows

image

CI

macOS is split into macos-15-intel (x86_64) and macos-latest (aarch64) in both workflows, and the Makefile gained release_mac_x86_64 / release_mac_aarch64, with release_mac kept as the aggregate target.

Testing

Verified on real hardware on every platform this touches, including FreeBSD (amd64) and macOS (on both Intel and Apple Silicon). Therefore macOS and FreeBSD are no longer considered experimental.

Disclaimer‌

I made the high-level design (hence the --only-current-user -> --show-other-users -> --user change). AI did the most coding.

CarterLi and others added 28 commits September 10, 2026 20:50
This is very useful since the built-in pager doesn't support horizontal scroll
... to show the processes of the current user / session only
1. remove `--only-current-session` (not useful on POSIX systems)
2. renames `--only-current-user` for `--show-other-users` to match the pattern of `--show-kthread`
3. simplifies code
See `process.rs` for detail
1. over-allocates the buffer of thread list to reduce the chance of truncation

2. uses info from `kinfo_proc` as fallbacks if available

3. skips further syscalls if `pidinfo::<TaskInfo>` fails
@CarterLi CarterLi changed the title Bring the Windows / macOS / FreeBSD columns up to parity, and fix a batch of bugs Bring the Windows / macOS / FreeBSD columns up to parity, and fix bugs Sep 12, 2026
…ce#972)

Consume the second sampling pass lazily and populate every column and the parent maps before advancing to the next process. This avoids retaining one procfs directory handle per process.

Add a CLI regression with a synthetic procfs tree and a child-only file descriptor limit, covering threads, tree output, kernel thread filtering and command reads.

Fixes dalance#766
@CarterLi

Copy link
Copy Markdown
Contributor Author

To avoid future conflicts, I merged #969 #972 into this PR.

@dalance After you have time to review and merge this, I hope a new release to be cut. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant