Good, this is the right way to use me: make me do the thinking, then throw the build at Claude.
Here’s a spec you can hand off directly.
Name (working): memwatch
Purpose: Run an arbitrary command, track memory usage over time for that command and all its child processes, and print a concise summary of peak memory usage + optional detailed metrics.
This is not a top clone. It’s a job-level memory profiler for things like:
cargo test …mpirun,prterun,pytest -n 8- Custom benchmarks or long-running pipelines
-
Track a command and all its descendants (process tree rooted at the spawned command).
-
Periodically sample memory usage (RSS) per process.
-
Compute and print:
- Peak total RSS across all job processes
- Peak RSS per process
- Duration of the job
-
Provide a human-readable summary and optionally machine-readable output (JSON/CSV).
-
Work on macOS and Linux (best-effort parity).
- No interactive TUI like
htop. - No CPU profiling, I/O stats, or perf counters (optional later).
- No kernel module or privileged monitoring.
- No remote monitoring; only local machine.
-
Capacity planning
“If I run
prterun -n 8 cargo test …, how much RAM does the whole job actually use at peak?” -
Scaling experiments
“How does peak RAM change as I vary
-nworkers?” -
Regression detection
“Did this branch increase peak memory by more than 20% vs main?”
Base invocation:
memwatch run -- <command> [args...]Examples:
memwatch run -- cargo test --release -p dis_cq bench_fold_prove_scalability
memwatch run -- prterun -n 8 ./target/release/my_binaryBehavior:
-
memwatchspawns<command> [args...]as a child. -
It tracks that process and all descendants until they all exit.
-
While the job is alive, it periodically samples:
- per-PID RSS
- per-PID command name / short command line
-
After completion, it computes aggregate stats and prints a summary.
-
Track a root PID (the command spawned by
memwatch). -
Repeatedly query current process tree:
-
Linux:
- Prefer
/proc/<pid>/stator/proc/<pid>/statusfor RSS. - Use
/proc/<pid>/taskor parse/proctree to find descendants.
- Prefer
-
macOS:
-
Use
psinitially (simpler v1).- Example:
ps -o pid,ppid,rss,command -ax
- Example:
-
Filter processes where:
- The PID is the root, or
- The process is a descendant (walk parent chain via PPID until root or no parent).
-
(Optional v2: use
libprocAPIs for better performance.)
-
-
-
Sampling should handle process churn:
- New processes appearing between samples.
- Exited processes disappearing between samples.
Configurable sampling interval:
- Default:
--interval 200ms(or 500ms; pick a sane default). - CLI flag:
--interval <ms>
Pseudo-behavior:
-
While at least one process in the job is alive:
-
Sleep
interval_ms. -
Collect snapshot:
-
Set of PIDs in the job.
-
For each PID:
- RSS in KiB
- Simple descriptor (e.g., command name)
-
-
Update metrics (see below).
-
For the whole job:
start_time(wall-clock)end_timeduration(seconds)max_total_rss_kib– max over time ofsum(rss_kib for all PIDs at that sample)samples– total number of polling samples
For each PID (process-level stats):
pidppidcommand(best-effort trimmed command line)max_rss_kibfirst_seentimestamplast_seentimestamp- Optional: number of samples seen
No need to store all samples in memory for v1; only update running maxima and maybe keep a small window of recent data if needed.
After the job exits, print:
Example:
Job: cargo test --release -p dis_cq bench_fold_prove_scalability
Duration: 00:03:21
Samples: 402
Max total RSS: 6.4 GiB (across 8 processes)
Max per process: 912 MiB (pid 8479)
Per-process peak RSS:
pid 8473 534 MiB /Users/alvinkuruvilla/.rustup/.../rustc
pid 8474 612 MiB /Users/alvinkurvilla/.rustup/.../rustc
pid 8475 703 MiB /Users/alvinkurvilla/.rustup/.../rustc
pid 8476 546 MiB /Users/alvinkurvilla/.rustup/.../rustc
...
Formatting rules:
- Convert KiB → human-readable IEC units (KiB, MiB, GiB).
- Align columns nicely.
CLI flag:
memwatch run --json -- cargo test ...JSON structure (example):
{
"command": ["cargo", "test", "--release", "-p", "dis_cq", "bench_fold_prove_scalability"],
"start_time": "2025-11-20T18:02:34Z",
"end_time": "2025-11-20T18:05:55Z",
"duration_seconds": 201.4,
"interval_ms": 200,
"max_total_rss_kib": 6624768,
"max_total_rss_sample_index": 320,
"processes": [
{
"pid": 8473,
"ppid": 8472,
"command": "/Users/.../rustc",
"max_rss_kib": 54656,
"first_seen": "2025-11-20T18:02:40Z",
"last_seen": "2025-11-20T18:05:10Z"
}
]
}User can redirect to a file:
memwatch run --json -- cargo test ... > mem_profile.jsonFlag:
--csv <path>Two possible tables:
- process_peaks.csv – one row per PID with peak RSS.
- (Optional later) timeline.csv – one row per sample with total RSS.
Basic structure:
memwatch <subcommand> [flags] -- <command> [args...]run– main and only required subcommand in v1.
Example full spec:
memwatch run [OPTIONS] -- <command> [args...]
Options:
-i, --interval <ms> Sampling interval in milliseconds (default: 500)
--json Output JSON instead of human-readable text
--quiet Suppress human-readable output (when using --json)
--version Show version
--help Show help
Later extensions (not required now):
memwatch compare <file1.json> <file2.json>– compare two runs (v2 idea).memwatch diff/report, etc.
-
Use
/procdirectly:-
RSS:
/proc/<pid>/statmor/proc/<pid>/status(VmRSS). -
PPID & command:
/proc/<pid>/stat/proc/<pid>/cmdline
-
-
Detect children by:
- Scanning
/proc, readingPPIDfor each process, and building a PID→PPID map. - Determine “in job” if you can reach the root PID walking parent links.
- Scanning
v1 (simpler, slower but acceptable):
-
Shell out to
pswith a fixed format:ps -axo pid,ppid,rss,command
-
Parse the output:
- RSS is already in KiB.
- Build PID→(PPID, RSS, command) map.
- Same parent-chain logic to determine descendants.
This is not blazing fast, but with intervals like 500ms or 1s it’s fine for v1 and avoids fighting C APIs. You can refactor later.
-
Command fails to start (e.g., not found):
-
Print clear error:
Failed to start command: No such file or directory -
Exit non-zero.
-
-
Command exits very quickly (< 1 sample interval):
-
Still record at least:
- start_time
- end_time
- duration
-
A single “best effort” sample right after process exit is okay.
-
-
No processes discovered for root PID:
- Likely a race / quick exit.
- Handle gracefully (empty process list but still print duration, etc.).
-
Long-running process cancellation:
- If user sends SIGINT (Ctrl+C) to
memwatch, forward the signal to the root PID and children, then exit after cleanup.
- If user sends SIGINT (Ctrl+C) to
-
Permissions:
- If some processes can’t be inspected (rare for your use), skip them with a warning.
Phase 1 – Skeleton + subprocess
- Implement CLI parsing.
- Implement spawning
<command> [args...]and waiting for exit. - Add minimal timing (start/end/duration).
Phase 2 – Linux-only sampling (if running on Linux)
- Implement
/proc-based sampler. - Track root + children; log max total RSS + per-PID peaks.
Phase 3 – macOS ps sampler
- Implement
ps -axo pid,ppid,rss,commandpolling. - Integrate same logic.
Phase 4 – Output formats
- Human-readable summary.
- JSON output (
--jsonflag).
Phase 5 – Polish
-
Good error messages.
-
Unit tests for:
- Process-tree detection given a mocked
ps/procsnapshot. - Unit conversion (KiB → MiB/GiB).
- Process-tree detection given a mocked
-
A couple of integration tests using a trivial command like
sleep 1and a small memory hog script. Good catch to say that explicitly. Let’s bolt that into the spec so Claude doesn’t quietly go “Linux only”.
Here’s an add-on section you can append to the spec you already have and hand to Claude:
-
Must support:
target_os = "linux"target_os = "macos"
-
Out of scope for v1: Windows (may be added later as a separate backend).
The CLI, output format, and semantics must be identical between Linux and macOS. Only the internals for process discovery and RSS measurement should differ.
Define a small abstraction so OS-specific code is isolated:
/// One snapshot of a single process at a point in time.
struct ProcessSample {
pid: i32,
ppid: i32,
rss_kib: u64,
command: String, // short command line
}
trait ProcessInspector {
/// Return a map of pid -> ProcessSample for *all* processes on the system.
fn snapshot_all(&self) -> Result<Vec<ProcessSample>, InspectorError>;
}Then the sampling loop can be OS-agnostic:
fn sample_job_tree(
inspector: &impl ProcessInspector,
root_pid: i32,
) -> JobSnapshot { /* same on both OSes */ }Platform-specific implementations:
LinuxProcessInspectorMacProcessInspector
Wired with cfg:
#[cfg(target_os = "linux")]
fn make_inspector() -> LinuxProcessInspector { ... }
#[cfg(target_os = "macos")]
fn make_inspector() -> MacProcessInspector { ... }The rest of the code (CLI, aggregation, JSON/summary output) must not depend on the OS.
Implementation freedom: /proc is preferred, no external commands.
Mandatory behavior:
-
Use
/procto obtain:pid,ppid- RSS in KiB
- command / cmdline (trimmed to reasonable length)
-
RSS sources:
- Either
/proc/<pid>/status(VmRSS) or/proc/<pid>/statm+ page size.
- Either
-
Process tree logic:
-
Build a PID → (PPID, RSS, command) map from
/proc. -
A process belongs to the job if:
pid == root_pid, or- following the PPID chain eventually reaches
root_pid.
-
No shelling out on Linux unless absolutely necessary.
macOS doesn’t have /proc, so v1 can use ps to keep development reasonable.
v1 macOS approach (acceptable and required):
-
Call
pswith a fixed format:ps -axo pid,ppid,rss,command
-
Parse the output into
ProcessSamplerecords:pid→ PIDppid→ parent PIDrss→ resident set size in KiB (macOS ps already reports in KiB)command→ full command line, trimmed
-
Same process-tree logic as Linux:
- Build PID map, walk PPID chain back to root.
Implementation notes:
-
The
psinvocation should be:- Hard-coded format (no locale-dependent output).
- Executed once per sampling interval.
-
Errors from
ps:- If
psfails once, log a warning and skip that sample. - If
psfails repeatedly, exit with a clear error.
- If
v2 (future) macOS note (optional for now):
- Backend can later be swapped to
libprocorsysctlfor speed, but the trait and public behavior must not change.
For the same workload and similar sampling interval:
-
Both Linux and macOS versions must:
- Track the same root PID semantics.
- Include all descendants visible during sampling.
- Produce structurally identical JSON.
-
Numeric differences are okay (different kernel accounting), but:
- RSS should be within reasonable ballpark (no obvious unit errors).
- Units must always be KiB internally, with human-readable conversion only at the presentation layer.
-
Clearly mark in the README and code:
-
Windows builds are not supported in v1.
-
If someone attempts to build on Windows, the binary should:
- Either fail compilation with a clear
cfgerror, or - Start and immediately print:
"memwatch: Windows support is not implemented yet"and exit with non-zero.
- Either fail compilation with a clear
-
-
The abstraction via
ProcessInspectoris intentionally chosen so that a futureWindowsProcessInspectorcan be added without touching the rest of the code.