From ed04ccd8e2297458ec97381806a05dea13090f0f Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 7 May 2026 09:47:05 +0300 Subject: update docs and ascii banner --- README.md | 14 +++++++------- cmd/ior/main.go | 12 ++++++++++-- docs/build-rocky-linux-9.md | 14 +++++++------- docs/parquet-querying.md | 2 +- docs/tutorial/tutorial.md | 32 ++++++++++++++++---------------- internal/flags/flags.go | 4 ++++ internal/flags/version.go | 13 ++++++++----- internal/ior.go | 5 +++++ internal/ior_mode_test.go | 12 ++++++++++++ 9 files changed, 70 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f47f405..532d581 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,23 @@ -I/O Riot NG is an experiments with BPF. This program traces for synchronous I/O syscalls and then analyses the time taken for each of those syscalls. This is especially useful for drawing FlameGraphs like these: +I/O Riot NG is an experiment with BPF. It traces synchronous I/O syscalls and analyses how long each one took. Useful for drawing FlameGraphs like these: -Maybe this is a spiritual successor of one of my previous projects, I/O Riot https://codeberg.org/snonux/ioriot, the latter was based on SystemTap and C. The NG is based on Go, C and BPF (via libbpfgo). +A spiritual successor to one of my previous projects, I/O Riot (https://codeberg.org/snonux/ioriot), which was based on SystemTap and C. The NG is based on Go, C, and BPF (via libbpfgo). -This works only on Linux! +Linux only. ## Demo A short guided tour with animated GIFs of every major surface lives in [`docs/tutorial/tutorial.md`](./docs/tutorial/tutorial.md). Two teasers: -**Startup — the PID picker:** `sudo ./ior` opens a searchable process list. Navigate with arrow keys, filter by typing, and press `Enter` to start tracing. The dashboard appears immediately after. +**Startup, the PID picker:** `sudo ./ior` opens a searchable process list. Navigate with arrow keys, filter by typing, press `Enter` to start tracing. The dashboard appears right after. Cold start: PID picker, then the dashboard appears -**Live flamegraph tab:** Once tracing, tab `1` shows a live flamegraph that rebuilds in real time as I/O events arrive. Bars grow and shift with the workload — this is the default landing tab. +**Live flamegraph tab:** Once tracing, tab `1` shows a live flamegraph that rebuilds in real time as I/O events arrive. Bars grow and shift with the workload. This is the default landing tab. Live in-TUI flamegraph rebuilding from real workload @@ -33,7 +33,7 @@ The demo is fully reproducible: `mage installDemoTools` once, then `sudo -v && m ## Build Builds a fully static `ior` binary inside a Rocky Linux 9 container and writes -it to the repo root — no local Go, clang, or libbpfgo setup required: +it to the repo root. No local Go, clang, or libbpfgo setup required: ```shell mage buildDocker @@ -55,7 +55,7 @@ For contributors who need a native build (Fedora / Rocky Linux 9), see Build on one machine, then `scp ior other-host:/usr/local/bin/` and run it anywhere. The binary is fully statically linked and uses libbpf CO-RE (Compile-Once, Run-Everywhere) to adapt field offsets to the target kernel's -BTF at load time — no recompile per host or kernel version needed. +BTF at load time. No recompile per host or kernel version needed. See [docs/build-rocky-linux-9.md](./docs/build-rocky-linux-9.md) for the full explanation. diff --git a/cmd/ior/main.go b/cmd/ior/main.go index 8d93d28..ebd3ef3 100644 --- a/cmd/ior/main.go +++ b/cmd/ior/main.go @@ -23,9 +23,17 @@ func main() { os.Exit(2) } - // Run the internal logic of the application. // flags.Get() is called here at the CLI boundary so internal code never reads the singleton. - if err := internal.Run(flags.Get()); err != nil { + cfg := flags.Get() + + // Handle -version: print the banner plus version and exit. + if cfg.ShowVersion { + flags.PrintVersion() + return + } + + // Run the internal logic of the application. + if err := internal.Run(cfg); err != nil { fmt.Printf("Failed to run: %v\n", err) os.Exit(2) } diff --git a/docs/build-rocky-linux-9.md b/docs/build-rocky-linux-9.md index 424b78e..f83479f 100644 --- a/docs/build-rocky-linux-9.md +++ b/docs/build-rocky-linux-9.md @@ -1,7 +1,7 @@ # Building ior on Rocky Linux 9 Verified on a fresh Rocky Linux 9.7 install (kernel `5.14.0-611.5.1.el9_7` or -newer). Runs on the **stock RHEL 9 kernel** — no kernel upgrade needed. +newer). Runs on the **stock RHEL 9 kernel**, no kernel upgrade needed. One build-time caveat: Rocky 9 ships neither `libelf.a` nor `libzstd.a` (no `*-static` packages). Both must be built from source. @@ -10,7 +10,7 @@ One build-time caveat: Rocky 9 ships neither `libelf.a` nor `libzstd.a` (no > `struct trace_event_raw_sys_enter`/`_exit` (the BTF-emitted alias). RHEL 9 > backports an `rt`-tree patch that adds `preempt_lazy_count` to `struct > trace_entry`, which widens those aliases by 8 bytes and shifts the `args`/`ret` -> offsets — but the actual context the kernel hands the program is still +> offsets, but the actual context the kernel hands the program is still > `struct syscall_trace_enter`/`_exit`, where the offsets did not move. The > verifier saw the program reading past `max_ctx_offset` and rejected the > attach with `EACCES`. `ior` now uses `syscall_trace_*` directly (matching @@ -19,7 +19,7 @@ One build-time caveat: Rocky 9 ships neither `libelf.a` nor `libzstd.a` (no ## Docker build (no Rocky 9 host required) -The easiest path — builds entirely inside a container from any Docker-capable +The easiest path. Builds entirely inside a container from any Docker-capable Linux host: ```shell @@ -140,15 +140,15 @@ Two reasons it works: - The Go binary is compiled with `-extldflags "-static"` and links libbpf, libelf, libzstd, and zlib as static archives. There is no runtime dependency - on the build host's library versions (a couple of glibc resolver functions — - `getpwnam_r` and friends — fall back to the target's libc, which is fine on + on the build host's library versions (a couple of glibc resolver functions, + `getpwnam_r` and friends, fall back to the target's libc, which is fine on any reasonable distro). - The BPF object inside the binary is built with libbpf's CO-RE (Compile-Once, Run-Everywhere) machinery. Field offsets are not baked into the bytecode; libbpf reads the target kernel's BTF (`/sys/kernel/btf/vmlinux`) at load time and patches the program for that kernel. As long as the target - ships BTF — true on every Debian, Ubuntu, Fedora, Arch, RHEL, and ElRepo - `kernel-ml` build at the time of writing — the same `ior` binary runs without + ships BTF (true on every Debian, Ubuntu, Fedora, Arch, RHEL, and ElRepo + `kernel-ml` build at the time of writing) the same `ior` binary runs without recompilation. Pick one Rocky 9 / Fedora box, do the build dance once, then distribute the diff --git a/docs/parquet-querying.md b/docs/parquet-querying.md index 5372325..4c49baf 100644 --- a/docs/parquet-querying.md +++ b/docs/parquet-querying.md @@ -2,7 +2,7 @@ ior can record I/O events to a Parquet file (press `R` in TUI, or use the `-parquet` flag in headless mode). This document explains how to explore those recordings interactively using `clickhouse local`, which is bundled -inside the standard `clickhouse/clickhouse-server` Docker image — no server, no persistent +inside the standard `clickhouse/clickhouse-server` Docker image. No server, no persistent state, no installation needed beyond Docker. --- diff --git a/docs/tutorial/tutorial.md b/docs/tutorial/tutorial.md index a3695de..01201a2 100644 --- a/docs/tutorial/tutorial.md +++ b/docs/tutorial/tutorial.md @@ -1,6 +1,6 @@ # I/O Riot NG: a guided tour -This tutorial walks through every major surface of `ior` — the dashboard tabs, the live stream, recording, headless modes, and the in-TUI flamegraph — using short animated GIFs so you can *see* what the keys actually do. +This tutorial walks through every major surface of `ior` (the dashboard tabs, the live stream, recording, headless modes, and the in-TUI flamegraph) using short animated GIFs so you can *see* what the keys actually do. Every GIF in this document is regenerated from a [VHS](https://github.com/charmbracelet/vhs) tape under [`tapes/`](./tapes). To rebuild them all, run `sudo -v && mage demo` (see [Regenerating the demo](#regenerating-the-demo)). @@ -36,7 +36,7 @@ cd ~/git/ior mage buildDocker # builds inside a Rocky 9 container, ~15 min on first run ``` -For a native build (libbpfgo must be cloned alongside the repo first — see the README): +For a native build (libbpfgo must be cloned alongside the repo first; see the README): ```shell mage all @@ -44,7 +44,7 @@ mage all ior needs `CAP_BPF`, so every invocation below uses `sudo`. -The build dance only has to happen once: the resulting `ior` binary is fully statically linked and uses CO-RE, so the same binary runs on any BTF-enabled Linux kernel without recompilation. See the [Compile once, run everywhere](../../README.md#compile-once-run-everywhere) section for details. +The build dance only has to happen once. The resulting `ior` binary is fully statically linked and uses CO-RE, so the same binary runs on any BTF-enabled Linux kernel without recompilation. See the [Compile once, run everywhere](../../README.md#compile-once-run-everywhere) section for details. ## First launch: the PID picker @@ -90,7 +90,7 @@ Press `3`. A sortable table of every traced syscall (count, average latency, tot ### 4 · Files -Press `4`. Per-path counters. The most useful key here is `d`, which toggles **directory grouping** — paths roll up into their parent directory, which is essential when one process touches thousands of files in `/usr/share/...`. +Press `4`. Per-path counters. The most useful key here is `d`, which toggles **directory grouping**: paths roll up into their parent directory, which is essential when one process touches thousands of files in `/usr/share/...`. ![Files tab toggling directory grouping](./assets/04-files-tab.gif) @@ -108,7 +108,7 @@ Press `6`. Two histograms: syscall **latency** (how long the syscall ran) and th ### 7 · Stream -Press `7`. A live tail of every traced event row — comm, PID, TID, syscall, file, FD, return value, bytes, latency, gap. This is the workhorse view; the next section explores it in depth. +Press `7`. A live tail of every traced event row: comm, PID, TID, syscall, file, FD, return value, bytes, latency, gap. This is the workhorse view; the next section explores it in depth. ![Stream tab live-tailing rows](./assets/07-stream-live.gif) @@ -118,7 +118,7 @@ Stream has two modes: **Live** (rows scroll past) and **Pause** (`space` toggles ### Pause + stacked filters -In pause mode, navigate with `j`/`k` (rows) and `←` / `→` (columns). Pressing `Enter` on the selected cell **pushes a new filter onto a stack** and immediately re-filters the ring buffer. Filters are stackable, so you can drill down — first by `Comm`, then by `Syscall`, then by `File`. `Esc` pops the most recent filter (LIFO); keep hitting `Esc` to undo all the way back. +In pause mode, navigate with `j`/`k` (rows) and `←` / `→` (columns). Pressing `Enter` on the selected cell **pushes a new filter onto a stack** and immediately re-filters the ring buffer. Filters are stackable, so you can drill down: first by `Comm`, then by `Syscall`, then by `File`. `Esc` pops the most recent filter (LIFO); keep hitting `Esc` to undo all the way back. ![Pause, push two filters, undo with Esc](./assets/08-stream-pause-filter.gif) @@ -134,10 +134,10 @@ The filter is reflected in the bottom status line, and matches the same syntax y Four keys, four flavours: -- `e` — quick export of the **current TUI-filter snapshot** to `ior-stream-.csv` in the current working directory. Works from any tab, not just Stream. -- `x` — quick export of the **paused stream view** specifically (preserves your filter stack). -- `X` — same as `x`, but prompts for a filename first. -- `E` — open the most recent stream-exported CSV in your `$EDITOR` (`hx` / `vi` fallback). +- `e`: quick export of the **current TUI-filter snapshot** to `ior-stream-.csv` in the current working directory. Works from any tab, not just Stream. +- `x`: quick export of the **paused stream view** specifically (preserves your filter stack). +- `X`: same as `x`, but prompts for a filename first. +- `E`: open the most recent stream-exported CSV in your `$EDITOR` (`hx` / `vi` fallback). ![Press 'e', then ls the resulting CSV](./assets/10-stream-csv-export.gif) @@ -147,9 +147,9 @@ If you don't want CSV export at all, start ior with `-tuiExport=false`; the help Three modal pickers reshape what the rest of the TUI sees: -- `p` — **PID picker** (re-opens the launch picker). -- `t` — **TID picker** for thread-level focus. -- `o` — **Probes** dialog: enable / disable individual syscall tracepoints. +- `p`: **PID picker** (re-opens the launch picker). +- `t`: **TID picker** for thread-level focus. +- `o`: **Probes** dialog: enable / disable individual syscall tracepoints. ![PID, TID, and probe pickers](./assets/11-pid-tid-probe.gif) @@ -172,7 +172,7 @@ Press `R` in the dashboard, accept the default filename (`ior-recording-