Skip to content

perf: replace ps spawn with /proc direct reads on Linux#61

Closed
tbouquet wants to merge 2 commits intograykode:mainfrom
tbouquet:perf/proc-ps
Closed

perf: replace ps spawn with /proc direct reads on Linux#61
tbouquet wants to merge 2 commits intograykode:mainfrom
tbouquet:perf/proc-ps

Conversation

@tbouquet
Copy link
Copy Markdown
Contributor

Depends on #60 - apply on top of that PR's branch
Part 2/3 of Phase 1 /proc optimization (#60 -> this -> next)

Summary

On Linux, read /proc/{pid}/stat + /proc/{pid}/cmdline directly instead of spawning ps -ww -eo pid,ppid,rss,%cpu,command. Eliminates one fork+exec per tick (~500 syscalls, ~15ms).

Falls back to ps on macOS where /proc is not available.

What's parsed from /proc

File Fields extracted
/proc/{pid}/stat ppid, utime, stime, starttime, rss (pages)
/proc/{pid}/cmdline Full command (NUL-separated -> space-joined)
/proc/uptime System uptime for CPU% calculation

CPU% is computed as lifetime average (utime+stime) / elapsed_ticks * 100. This differs from ps's instantaneous sample but works correctly for abtop's Working/Waiting threshold (cpu_pct > 1.0) since long-idle processes see their average decline below 1% over time.

New shared utility

Introduces scan_proc_fds(pid) -> Vec<PathBuf> in process.rs - resolves all /proc/{pid}/fd symlinks. Used by both this PR's port discovery and #60's Codex JSONL discovery, eliminating duplicated readdir+readlink loops.

Dependencies

  • Adds libc = "0.2" (Linux-only, for sysconf to get CLK_TCK and page size)

Test plan

  • cargo clippy -- -D warnings passes
  • cargo test - 35/35 pass
  • macOS fallback preserved via #[cfg(not(target_os = "linux"))]

Copy link
Copy Markdown
Owner

@graykode graykode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm — security clean (read-only /proc, libc 0.2 is the canonical FFI crate, the two unsafe sysconf calls are inherently safe). Build/clippy/tests green on macOS fallback path; Linux compile is covered by CI's ubuntu-latest.

Noticed while reviewing:

  • Cargo.lock not updated. Adding libc to Cargo.toml regenerates the lockfile on first build. Worth committing the lockfile change here so CI doesn't see lockfile churn.
  • CPU% semantic change is fine for the Working/Waiting threshold (which is mtime-based, not cpu-based) but is a UI-visible diff vs the old ps instantaneous sample. You already flagged this in the PR body — just confirming it's an acceptable trade.
  • No unit tests for the /proc/{pid}/stat parser, even though it's the trickiest part (the rfind(')') trick around comm with spaces/parens). A couple of parse_stat() tests with crafted strings would catch future regressions cheaply.

None of these block merge.

graykode added a commit that referenced this pull request Apr 21, 2026
> **Depends on #60 + #61** - apply on top of those PRs
> Part 3/3 of Phase 1 /proc optimization (#60 -> #61 -> this)

## Summary

On Linux, parse `/proc/net/tcp` + `/proc/net/tcp6` for LISTEN sockets
and match inodes via `scan_proc_fds()` instead of spawning `lsof -i -P
-n -sTCP:LISTEN`. Eliminates the last fork+exec from the port discovery
path (~400 syscalls, ~30ms).

Falls back to `lsof` on macOS where `/proc` is not available.

## How it works

1. Parse `/proc/net/tcp[6]` - state `0A` = LISTEN, extract port (hex)
and inode
2. Scan `/proc/[pid]/fd` symlinks via `scan_proc_fds()` (shared helper
from #61)
3. Match `socket:[inode]` targets to listening port inodes
4. Build pid -> ports map

## Phase 1 complete

With all three PRs merged, the result on Linux:

| Spawn | Before | After |
|-------|--------|-------|
| `ps` | fork+exec every 2s tick | `/proc` direct read |
| `lsof` (Codex) | fork+exec every tick | `/proc/pid/fd` readlink |
| `lsof` (ports) | fork+exec every 5 ticks | `/proc/net/tcp` parse |
| **Total** | ~1500 syscalls, ~50ms | ~200 syscalls, ~5ms |

Remaining spawns: `git status` (slow tick) and `sqlite3` (OpenCode).

## Test plan

- [x] `cargo clippy -- -D warnings` passes
- [x] `cargo test` - 35/35 pass
- [x] macOS fallback preserved via `#[cfg(not(target_os = "linux"))]`
@graykode
Copy link
Copy Markdown
Owner

Closing as superseded by #62.

Since #62 was stacked on this branch, merging #62 already brought all of this PR's content into main (libc dependency, get_process_info Linux variant, etc.). Confirmed by merging current main back into this branch — auto-resolved with zero net diff against main.

Thanks for the perf work — this and #62 together gave us the full /proc-based path on Linux.

@graykode graykode closed this Apr 21, 2026
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.

2 participants