summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md27
-rw-r--r--Dockerfile75
-rw-r--r--Magefile.go11
-rw-r--r--README.md356
-rw-r--r--docs/build-rocky-linux-9.md173
-rw-r--r--docs/tui-reference.md175
-rw-r--r--internal/c/generated_tracepoints.c46
-rwxr-xr-xscripts/build-with-docker.sh50
8 files changed, 557 insertions, 356 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 0d65300..11a3618 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,22 +12,23 @@ git -C ../libbpfgo submodule update --init --recursive
make -C ../libbpfgo libbpfgo-static
```
-If builds/tests fail with missing libbpf headers (for example `bpf/bpf.h` not found), rerun the commands above and then run `env GOTOOLCHAIN=auto mage world`. Prefer Mage targets over raw `go test` for packages that import `libbpfgo`; Mage wires the required `CGO_CFLAGS`, `CGO_LDFLAGS`, and `LIBBPFGO` values.
+If builds/tests fail with missing libbpf headers (for example `bpf/bpf.h` not found), rerun the commands above and then run `mage world`. Prefer Mage targets over raw `go test` for packages that import `libbpfgo`; Mage wires the required `CGO_CFLAGS`, `CGO_LDFLAGS`, and `LIBBPFGO` values.
```bash
-env GOTOOLCHAIN=auto mage all # Build everything (BPF objects and Go binary)
-env GOTOOLCHAIN=auto mage test # Run all tests
-GOTOOLCHAIN=auto TEST_NAME=TestEventloop mage testWithName # Run specific test
-env GOTOOLCHAIN=auto mage integrationTest # Build + run integration tests (default parallelism is capped)
-GOTOOLCHAIN=auto INTEGRATION_PARALLEL=1 mage integrationTest # Force serial integration tests
-env GOTOOLCHAIN=auto mage generate # Generate code (required after modifying tracepoint definitions)
-env GOTOOLCHAIN=auto mage bench # Run benchmarks
-env GOTOOLCHAIN=auto mage prReview # Run PR review baseline: world + benchProf
-env GOTOOLCHAIN=auto mage clean # Clean build artifacts
-env GOTOOLCHAIN=auto mage world # Clean + generate + test + build (recommended reset path)
-env GOTOOLCHAIN=auto mage demo # Regen demo/ GIFs + screenshots (needs vhs+ttyd, sudo -v warmed)
+mage all # Build everything (BPF objects and Go binary)
+mage buildDocker # Build ior inside a Rocky Linux 9 container (writes binary to repo root)
+mage test # Run all tests
+TEST_NAME=TestEventloop mage testWithName # Run specific test
+mage integrationTest # Build + run integration tests (default parallelism is capped)
+INTEGRATION_PARALLEL=1 mage integrationTest # Force serial integration tests
+mage generate # Generate code (required after modifying tracepoint definitions)
+mage bench # Run benchmarks
+mage prReview # Run PR review baseline: world + benchProf
+mage clean # Clean build artifacts
+mage world # Clean + generate + test + build (recommended reset path)
+mage demo # Regen demo/ GIFs + screenshots (needs vhs+ttyd, sudo -v warmed)
TAPE=07-stream-live mage demoOne # Regen one demo tape only
-env GOTOOLCHAIN=auto mage installDemoTools # One-time: install vhs (go install) and ttyd (dnf)
+mage installDemoTools # One-time: install vhs (go install) and ttyd (dnf)
```
## Demo Pipeline
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d190056
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,75 @@
+FROM rockylinux:9-minimal
+
+# Update GO_VERSION here to upgrade the Go toolchain baked into the image.
+ARG GO_VERSION=1.26.2
+
+# Install full dnf and plugin support (minimal image ships only microdnf)
+RUN microdnf install -y dnf dnf-plugins-core && \
+ microdnf clean all
+
+# Enable CRB (ships zlib-static / glibc-static), EPEL, and the BaseOS source
+# repo (provides the elfutils source RPM needed to build libelf.a).
+RUN dnf config-manager --set-enabled crb && \
+ dnf config-manager --set-enabled baseos-source && \
+ dnf install -y epel-release && \
+ dnf clean all
+
+# Build-time toolchain: C compiler, clang/LLVM (for BPF), bpftool, BPF/elf
+# headers, static archives for zlib and glibc, and packaging helpers.
+RUN dnf install -y \
+ gcc clang bpftool \
+ elfutils-libelf-devel \
+ zlib-static glibc-static libzstd-devel \
+ git make cmake wget rpmdevtools && \
+ dnf builddep -y elfutils && \
+ dnf clean all
+
+# Install Go from go.dev — Rocky 9 ships an older release, ior needs 1.26+.
+RUN wget -q "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -O /tmp/go.tar.gz && \
+ tar -C /usr/local -xf /tmp/go.tar.gz && \
+ rm /tmp/go.tar.gz
+
+ENV PATH="/usr/local/go/bin:/root/go/bin:${PATH}"
+ENV GOPATH="/root/go"
+
+# Build libelf.a from the Rocky 9 elfutils source RPM.
+# Rocky 9 does not ship libelf.a (no *-static packages exist in the distro).
+RUN mkdir -p /root/src && cd /root && \
+ dnf download --source elfutils-libelf && \
+ rpm -ivh elfutils-*.src.rpm && \
+ tar -C /root/src -xjf rpmbuild/SOURCES/elfutils-*.tar.bz2 && \
+ cd /root/src/elfutils-* && \
+ ./configure --enable-deterministic-archives --disable-debuginfod --disable-libdebuginfod && \
+ make -C lib -j$(nproc) && \
+ make -C libelf -j$(nproc) && \
+ cp -v libelf/libelf.a /usr/lib64/ && \
+ rm -rf /root/src /root/rpmbuild /root/elfutils-*.src.rpm
+
+# Build libzstd.a from upstream — libzstd-devel does not ship the static archive.
+RUN wget -q https://github.com/facebook/zstd/releases/download/v1.5.5/zstd-1.5.5.tar.gz \
+ -O /tmp/zstd.tar.gz && \
+ tar -C /tmp -xzf /tmp/zstd.tar.gz && \
+ make -C /tmp/zstd-1.5.5/lib -j$(nproc) libzstd.a && \
+ cp -v /tmp/zstd-1.5.5/lib/libzstd.a /usr/lib64/ && \
+ rm -rf /tmp/zstd-1.5.5 /tmp/zstd.tar.gz
+
+# Clone libbpfgo at the required tag and build the static archive.
+# Placed at /git/libbpfgo so it is a sibling of the ior mount at /git/ior,
+# matching the default LIBBPFGO=../libbpfgo path used by Magefile.go.
+RUN mkdir -p /git && \
+ git clone https://github.com/aquasecurity/libbpfgo /git/libbpfgo && \
+ git -C /git/libbpfgo checkout v0.9.2-libbpf-1.5.1 && \
+ git -C /git/libbpfgo submodule update --init --recursive && \
+ make -C /git/libbpfgo libbpfgo-static
+
+# Install the mage build tool
+RUN go install github.com/magefile/mage@latest
+
+# The ior source tree is mounted at /git/ior at runtime (see build-with-docker.sh).
+WORKDIR /git/ior
+
+# Generate kernel-specific tracepoint code then compile the static ior binary.
+# IOR_FORCE_GENERATE=1 skips the strict diff against the committed syscall-coverage
+# audit, which was generated on a different kernel build than the container host.
+# The container runs as root so bpftool and /sys/kernel/tracing are used directly.
+CMD ["sh", "-c", "IOR_FORCE_GENERATE=1 mage generate && mage all"]
diff --git a/Magefile.go b/Magefile.go
index cf1adfb..e9458b5 100644
--- a/Magefile.go
+++ b/Magefile.go
@@ -40,6 +40,7 @@ const (
tracepointsResultNew = "internal/c/generated_tracepoints_result.txt.new"
tracepointsGoPath = "internal/tracepoints/generated_tracepoints.go"
typesGoPath = "internal/types/generated_types.go"
+ dockerBuildScript = "scripts/build-with-docker.sh"
typesHeaderPath = "internal/c/types.h"
VMLINUXPath = "internal/c/vmlinux.h"
benchProfilesDir = "bench-profiles"
@@ -72,6 +73,16 @@ func All() error {
return nil
}
+// BuildDocker builds the ior binary inside a Rocky Linux 9 Docker container
+// and writes the resulting static binary to the repo root. The container
+// image is built on the first run (~15-20 min) and reused thereafter.
+// The Go version baked into the image is taken from go.mod automatically.
+// Requires Docker and a host kernel with tracefs and BTF enabled.
+// Pass --run to skip the image rebuild and only recompile ior.
+func BuildDocker() error {
+ return sh.RunV("bash", dockerBuildScript)
+}
+
// BpfBuild builds the embedded BPF object used by the Go binary.
func BpfBuild() error {
if err := ensureVMLINUX(); err != nil {
diff --git a/README.md b/README.md
index f5fb268..f031fc6 100644
--- a/README.md
+++ b/README.md
@@ -25,357 +25,73 @@ The demo is fully reproducible: `mage installDemoTools` once, then `sudo -v && m
## Requirements
- Go 1.26 or newer (ior relies on cgo via libbpfgo).
+- Linux with a BTF-enabled kernel (`/sys/kernel/btf/vmlinux` present).
-## Local libbpfgo Toolchain
+## Build
-`ior` links against a locally built `libbpfgo` checkout. By default
-`Magefile.go` expects that checkout at `../libbpfgo` relative to this repo; set
-`LIBBPFGO=/absolute/path/to/libbpfgo` if you keep it elsewhere.
+### Docker build (recommended — no toolchain setup required)
-Pin that checkout to `v0.9.2-libbpf-1.5.1` and rebuild the static artifacts
-before running `mage` targets:
+Builds the static `ior` binary inside a Rocky Linux 9 container and writes it
+to the repo root. Requires only Docker and a Linux host with tracefs and BTF:
```shell
-git -C ../libbpfgo checkout v0.9.2-libbpf-1.5.1
-git -C ../libbpfgo submodule update --init --recursive
-make -C ../libbpfgo libbpfgo-static
+mage buildDocker
```
-Validated commands for this pin:
+On first run this takes ~15–20 minutes to build the image. Subsequent runs
+reuse the cached image and finish in under a minute. To skip the image build:
```shell
-env GOTOOLCHAIN=auto mage world
-env GOTOOLCHAIN=auto mage integrationTest
+./scripts/build-with-docker.sh --run
```
-Troubleshooting and rollback:
+### Native build
-- If builds fail with `bpf/bpf.h` missing, re-run the checkout, submodule sync,
- and `make libbpfgo-static` commands above, then retry `env GOTOOLCHAIN=auto mage world`.
-- Prefer Mage targets over raw `go test` for packages that import `libbpfgo`;
- Mage injects the required `CGO_CFLAGS`, `CGO_LDFLAGS`, and `LIBBPFGO` values.
-- To roll back to the previous wrapper state, repin `go.mod` to
- `github.com/aquasecurity/libbpfgo v0.6.0-libbpf-1.3.0.20240111220235-90dbffffbdab`,
- then reset the sibling checkout and rebuild:
+`ior` links against a locally built `libbpfgo`. Clone it as a sibling of this
+repo and build the static archive once:
```shell
-git -C ../libbpfgo checkout 90dbffffbdab
+git clone https://github.com/aquasecurity/libbpfgo ../libbpfgo
+git -C ../libbpfgo checkout v0.9.2-libbpf-1.5.1
git -C ../libbpfgo submodule update --init --recursive
make -C ../libbpfgo libbpfgo-static
```
-## Timing Semantics
-
-Each reported event pair has two timing counters:
-
-- `durationNs`: syscall runtime on the same thread (`exit(current) - enter(current)`).
-- `durationToPrevNs`: inter-syscall gap on the same thread (`enter(current) - exit(previous)`).
-
-Important details:
-
-- `durationToPrevNs` is tracked per `tid` (thread), not globally across all threads.
-- The first observed syscall pair for a thread has `durationToPrevNs = 0` because there is no prior exit timestamp.
-- `durationToPrevNs` is attributed to the current syscall pair (the one whose `enter` closes the gap).
-- There is no separate "idle" pseudo-event bucket; use the `durationToPrev` count field when aggregated flamegraph output should emphasize inter-syscall time.
-
-## Rocky Linux 9
-
-Verified on a fresh Rocky Linux 9.7 install (e.g. kernel `5.14.0-611.5.1.el9_7`,
-exact stamp not required). 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 have
- to be built from source — the elfutils dance is the same as the Fedora section above;
- `libzstd.a` needs an extra `make` from the upstream tarball.
-
-> Historical note. Earlier versions of `ior` typed BPF tracepoint context as
-> `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
-> `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
-> the [bcc fix](https://github.com/iovisor/bcc/pull/4920) and inspektor-gadget),
-> so the stock kernel works with no workaround.
+Then build everything:
```shell
-# 1) Enable repos and install build dependencies (CRB ships static libs).
-sudo dnf config-manager --set-enabled crb
-sudo dnf install -y epel-release
-sudo dnf install -y gcc clang bpftool elfutils-libelf-devel zlib-static \
- glibc-static libzstd-devel git make cmake wget rpmdevtools strace bpftrace
-sudo dnf builddep -y elfutils
-
-# 2) Install Go 1.26 from go.dev (Rocky 9 ships only Go 1.25; ior needs 1.26+).
-cd /tmp
-wget -q https://go.dev/dl/go1.26.2.linux-amd64.tar.gz
-sudo tar -C /usr/local -xf go1.26.2.linux-amd64.tar.gz
-echo 'export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH' | sudo tee /etc/profile.d/go.sh
-source /etc/profile.d/go.sh
-
-# 3) Build libelf.a from elfutils source (same trick as the Fedora section).
-mkdir -p ~/src && cd ~
-dnf download --source elfutils-libelf
-rpm -ivh elfutils-*.src.rpm
-tar -C ~/src -xjf rpmbuild/SOURCES/elfutils-*.tar.bz2
-cd ~/src/elfutils-*
-./configure --enable-deterministic-archives --disable-debuginfod --disable-libdebuginfod
-make -C lib -j$(nproc)
-make -C libelf -j$(nproc)
-sudo cp -v libelf/libelf.a /usr/lib64/
-
-# 4) Build libzstd.a from upstream (libzstd-devel does not ship the static archive).
-cd /tmp
-wget -q https://github.com/facebook/zstd/releases/download/v1.5.5/zstd-1.5.5.tar.gz
-tar xzf zstd-1.5.5.tar.gz
-make -C zstd-1.5.5/lib -j$(nproc) libzstd.a
-sudo cp -v zstd-1.5.5/lib/libzstd.a /usr/lib64/
-
-# 5) Clone ior + libbpfgo, pin libbpfgo, build the static archive, install mage.
-mkdir -p ~/git
-git clone https://codeberg.org/snonux/ior ~/git/ior
-git clone https://github.com/aquasecurity/libbpfgo ~/git/libbpfgo
-git -C ~/git/libbpfgo checkout v0.9.2-libbpf-1.5.1
-git -C ~/git/libbpfgo submodule update --init --recursive
-make -C ~/git/libbpfgo libbpfgo-static
-go install github.com/magefile/mage@latest
-
-# 6) Generate against the live kernel (the syscall-coverage audit is
-# kernel-specific; IOR_FORCE_GENERATE skips the strict diff against the
-# committed audit which was generated on a different kernel build).
-cd ~/git/ior
-env IOR_FORCE_GENERATE=1 GOTOOLCHAIN=auto mage generate
-env GOTOOLCHAIN=auto mage all
-
-# 7) Smoke test.
-sudo ./ior -plain -duration 5
+mage world
```
-If `./ior -plain -duration 5` prints `Probing for 5s` and a stream of CSV rows,
-the install is good.
+For Rocky Linux 9 specific steps (building static libelf/libzstd, installing Go
+1.26) see [docs/build-rocky-linux-9.md](./docs/build-rocky-linux-9.md).
## Compile once, run everywhere
-The full build dance above only has to happen on **one** machine. The resulting
-`ior` binary is portable across Linux hosts: `scp ior other-host:/usr/local/bin/`
-and run it there. 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
- 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 now ElRepo `kernel-ml` build at the time of
- writing — the same `ior` binary runs without recompilation.
-
-So in practice: pick one Rocky 9 / Fedora box, do the build dance once, then
-distribute the 23 MB binary to wherever you want to trace. The build host needs
-all the dev tooling; the trace hosts need only a BTF-enabled kernel and `sudo`.
+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.
-For the eBPF + CO-RE explanation, see Part 2 of the I/O Riot NG blog series:
-[Unveiling I/O Riot NG — Part 2: under the hood](https://foo.zone/gemfeed/unveiling-ior-ng-part-2.html).
+See [docs/build-rocky-linux-9.md](./docs/build-rocky-linux-9.md) for the full
+explanation.
-## TUI Flamegraphs
+## TUI
-Flamegraphs are available only inside the TUI dashboard.
-Use `-fields` to change the stack order and `-count` to choose the metric.
-The default stack order is `comm,path,tracepoint` (bottom to top).
+Press **H** inside the dashboard to toggle the built-in help panel. Tabs are
+reachable with **tab/shift+tab** or number keys **1–6**. Full hotkey reference:
+[docs/tui-reference.md](./docs/tui-reference.md).
## Recording Modes
-`ior` has four distinct output flows. They are intentionally different:
-
-| Mode | How to use it | What it writes | Filter behavior |
-| --- | --- | --- | --- |
-| TUI dashboard | default startup | nothing continuously; data stays in memory unless you export | current TUI/global filters drive what you see |
-| TUI CSV snapshot export | press `e` in the dashboard | one `ior-stream-<timestamp>.csv` snapshot of the current filtered stream view | exports only the currently filtered in-memory rows |
-| Headless `.ior.zst` export | start with `-flamegraph -name <name>` | one aggregated native trace artifact written at shutdown | no TUI filter stack; this is the native trace/integration workflow |
-| Parquet recording | press `R` in the TUI, or start with `-parquet <file>` | a streaming Parquet file of traced syscall rows | TUI mode records rows that pass the active TUI filter; headless `-parquet` records all traced rows |
-
-Important distinction:
-
-- `.ior.zst` output is an aggregated native artifact, not a row-by-row event log.
-- CSV export is a point-in-time snapshot of the ring buffer.
-- Parquet recording is a streaming capture from start to stop.
-- The ring buffer is capped, so CSV export is not a replacement for Parquet recording or `.ior.zst` output.
-
-### Headless Native `.ior.zst` Output
-
-Use `-flamegraph` when you want the native `ior` trace artifact instead of a streaming row log:
-
-```shell
-sudo ./ior -flamegraph -name trace-run -duration 60
-```
-
-Native `.ior.zst` behavior:
-
-- writes one `*.ior.zst` file when the run ends
-- stores aggregated counters for repeated syscall/path/process combinations
-- is intended for `ior`'s native flamegraph and integration-style workflows
-- does not preserve one output row per traced syscall
-
-### TUI Parquet Recording
-
-Start a recording from the dashboard with `R`.
-
-- First `R`: open a filename prompt (`ior-recording-<timestamp>.parquet` by default).
-- `Enter`: start recording to that file.
-- Second `R`: stop and finalize the active Parquet file.
-- Recording stops automatically when you quit the TUI or reselect PID/TID/session scope.
-
-Lifecycle details:
-
-- TUI recording uses the active TUI global filter at emission time.
-- If a filter change restarts tracing, the recorder stays alive and continues writing matching rows after the restart.
-- The dashboard footer shows the active recording path or the last recording error.
-
-### Headless Parquet Recording
-
-Use `-parquet` to skip the TUI and stream traced syscall rows directly to a Parquet file:
-
-```shell
-sudo ./ior -parquet trace.parquet -duration 60
-```
-
-Headless Parquet mode behavior:
-
-- skips the TUI completely
-- records all traced rows
-- rejects content filters such as `-comm`, `-path`, `-pid`, and `-tid`
-- cannot be combined with `-plain`, `-flamegraph`, `--testflames`, or `--testliveflames`
+`ior` has four distinct output flows:
-Use headless mode when you want a full recording, and TUI mode when you want interactive filtering plus optional start/stop recording from the dashboard.
-
-### Choosing Between `.ior.zst` and Parquet
-
-Both formats are useful, but they solve different problems:
-
-| Question | Native `.ior.zst` | Parquet |
+| Mode | How to use it | What it writes |
| --- | --- | --- |
-| Data shape | aggregated counters | one row per traced syscall |
-| Write pattern | collect in memory, write one compressed artifact at the end | stream rows continuously while recording |
-| Best for | `ior`-native trace artifacts, flamegraph workflows, integration assertions | offline analysis in other tools, long captures, preserving per-event detail |
-| Relative write cost | usually lower because repeated events are folded together before file write | usually higher because each traced row is serialized |
-| Detail retained | loses original row order and per-event granularity | keeps per-event timing and syscall fields |
-
-Rule of thumb:
-
-- choose `.ior.zst` when you want the native `ior` artifact and do not need every traced syscall row preserved
-- choose Parquet when you want a full event stream for downstream analysis outside `ior`
-
-## TUI Navigation
-
-The TUI interface provides an in‑screen help panel (toggle with **H**) that lists all available keys. Use this help screen to discover navigation shortcuts.
-
-You can move between dashboard tabs:
-
-- **tab** – next dashboard tab
-- **shift+tab** – previous dashboard tab
-- **1** – Overview
-- **2** – Syscalls
-- **3** – Files
-- **4** – Processes
-- **5** – Latency+Gaps
-- **6** – Stream
-
-The bottom hint shows `press H for help` when the help is hidden.
-
-
-
-The TUI has two key scopes:
-
-- Global hotkeys: available from dashboard screens.
-- Dashboard hotkeys: behavior that depends on the active dashboard tab (especially `6:Stream`).
-
-Help visibility:
-
-- `H`: toggle bottom help sections on/off.
-- By default, help is hidden and the bottom hint shows `press H for help`.
-
-### Global Hotkeys
-
-- `tab`: next dashboard tab.
-- `shift+tab`: previous dashboard tab.
-- `1`: `Overview` tab.
-- `2`: `Syscalls` tab.
-- `3`: `Files` tab.
-- `4`: `Processes` tab.
-- `5`: `Latency+Gaps` tab.
-- `6`: `Stream` tab.
-- `7`: `Stream` tab (alias).
-- `e`: export filtered stream rows to CSV (`ior-stream-<timestamp>.csv`) in current working directory.
-- `R`: start or stop Parquet recording from the TUI dashboard.
-- `p`: re-open process selector (PID selection flow).
-- `t`: open TID selector flow.
-- `o`: open probe selection/toggling dialog.
-- `r`: refresh dashboard snapshot.
-- `q` or `ctrl+c`: quit.
-
-### Dashboard / Tab-Specific Hotkeys
-
-- `d` in `3:Files`: toggle directory-grouped files view.
-- `s` in sortable table tabs (`2:Syscalls`, `3:Files`, `4:Processes`): sort by the selected column using that table's default direction.
-- `S` in sortable table tabs (`2:Syscalls`, `3:Files`, `4:Processes`): reverse-sort by the selected column.
-- `j/k` or `up/down` in list-like tabs (`2:Syscalls`, `3:Files`, `4:Processes`): scroll list.
-
-`left/right` and `h/l` do not switch tabs. In `6:Stream` paused mode they move selected column.
-
-### 6:Stream Hotkeys and Behavior
-
-`6:Stream` has two modes:
-
-- Live mode (`paused=false`): rows update continuously.
-- Pause mode (`paused=true`): selection/cell/filter/search/export workflows are enabled.
-
-Core controls:
-
-- `space`: toggle live/pause.
-- `g`/`G`: jump to top/tail.
-- `c`: clear stream filters.
-- `f`: open advanced filter modal.
-- `j/k` or `up/down`: move selected row in pause mode; scroll in live mode.
-- `left/right` or `h/l`: move selected column in pause mode.
-
-#### Enter-Based Filter Stack (Pause Mode)
-
-In pause mode, `enter` on the selected cell pushes a new filter onto a stack and immediately re-filters the current ring buffer snapshot. Filters are stackable.
-
-- String columns use case-insensitive substring match:
-- `Comm` -> `comm~<value>`
-- `Syscall` -> `syscall~<value>`
-- `File` -> `file~<value>`
-- Numeric exact match:
-- `PID`, `TID`, `FD`, `Ret`, `Bytes`
-- Numeric threshold (`>=`):
-- `Latency` -> `latency>=selected_value`
-- `Gap` -> `gap>=selected_value`
-
-Undo:
-
-- `esc` in pause mode pops the most recent filter from the stack (LIFO).
-- Repeated `esc` keeps undoing until no stacked filters remain.
-
-#### Regex Search (Pause Mode)
-
-- `/`: open regex prompt and search forward.
-- `?`: open regex prompt and search backward.
-- Search checks all stream columns/fields and wraps around ring-buffer rows.
-- `n`: next match in the same direction as last `/` or `?`.
-- `N`: previous match (opposite direction).
-
-#### Stream CSV Export (Pause Mode)
-
-- `x`: quick export filtered stream rows to CSV (`ior-stream-<timestamp>.csv`).
-- `X`: export filtered stream rows to CSV with filename prompt.
-- `E`: open last stream-exported CSV in foreground editor (`EDITOR` -> `VISUAL` -> `SUDO_EDITOR` -> fallback `hx`, else `vi`).
-
-Export behavior:
+| TUI dashboard | default startup | nothing — data stays in memory until export |
+| TUI CSV snapshot | press `e` | `ior-stream-<timestamp>.csv` of filtered stream |
+| Headless `.ior.zst` | `-flamegraph -name <name>` | aggregated native trace artifact |
+| Parquet recording | press `R` in TUI, or `-parquet <file>` | streaming Parquet file |
-- `e` exports a fresh filtered stream snapshot using the current shared TUI filter, even outside paused mode.
-- `x`/`X` export the currently paused stream rows, preserving the stream tab's exact paused view.
+Full details and the `.ior.zst` vs Parquet trade-off:
+[docs/tui-reference.md](./docs/tui-reference.md).
diff --git a/docs/build-rocky-linux-9.md b/docs/build-rocky-linux-9.md
new file mode 100644
index 0000000..424b78e
--- /dev/null
+++ b/docs/build-rocky-linux-9.md
@@ -0,0 +1,173 @@
+# 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.
+
+One build-time caveat: Rocky 9 ships neither `libelf.a` nor `libzstd.a` (no
+`*-static` packages). Both must be built from source.
+
+> Historical note. Earlier versions of `ior` typed BPF tracepoint context as
+> `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
+> `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
+> the [bcc fix](https://github.com/iovisor/bcc/pull/4920) and inspektor-gadget),
+> so the stock kernel works with no workaround.
+
+## Docker build (no Rocky 9 host required)
+
+The easiest path — builds entirely inside a container from any Docker-capable
+Linux host:
+
+```shell
+mage buildDocker
+# or directly:
+./scripts/build-with-docker.sh
+# skip image rebuild on subsequent runs:
+./scripts/build-with-docker.sh --run
+```
+
+`mage buildDocker` builds a `ior-builder:rocky9` image on first run (~15–20 min),
+then runs it with the repo root mounted as a volume so the resulting static
+binary lands at `./ior`.
+
+## Manual build on a Rocky Linux 9 host
+
+```shell
+# 1) Enable repos and install build dependencies (CRB ships static libs).
+sudo dnf config-manager --set-enabled crb
+sudo dnf install -y epel-release
+sudo dnf install -y gcc clang bpftool elfutils-libelf-devel zlib-static \
+ glibc-static libzstd-devel git make cmake wget rpmdevtools strace bpftrace
+sudo dnf builddep -y elfutils
+
+# 2) Install Go 1.26 from go.dev (Rocky 9 ships only Go 1.25; ior needs 1.26+).
+cd /tmp
+wget -q https://go.dev/dl/go1.26.2.linux-amd64.tar.gz
+sudo tar -C /usr/local -xf go1.26.2.linux-amd64.tar.gz
+echo 'export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH' | sudo tee /etc/profile.d/go.sh
+source /etc/profile.d/go.sh
+
+# 3) Build libelf.a from elfutils source.
+mkdir -p ~/src && cd ~
+dnf download --source elfutils-libelf
+rpm -ivh elfutils-*.src.rpm
+tar -C ~/src -xjf rpmbuild/SOURCES/elfutils-*.tar.bz2
+cd ~/src/elfutils-*
+./configure --enable-deterministic-archives --disable-debuginfod --disable-libdebuginfod
+make -C lib -j$(nproc)
+make -C libelf -j$(nproc)
+sudo cp -v libelf/libelf.a /usr/lib64/
+
+# 4) Build libzstd.a from upstream (libzstd-devel does not ship the static archive).
+cd /tmp
+wget -q https://github.com/facebook/zstd/releases/download/v1.5.5/zstd-1.5.5.tar.gz
+tar xzf zstd-1.5.5.tar.gz
+make -C zstd-1.5.5/lib -j$(nproc) libzstd.a
+sudo cp -v zstd-1.5.5/lib/libzstd.a /usr/lib64/
+
+# 5) Clone ior + libbpfgo, pin libbpfgo, build the static archive, install mage.
+mkdir -p ~/git
+git clone https://codeberg.org/snonux/ior ~/git/ior
+git clone https://github.com/aquasecurity/libbpfgo ~/git/libbpfgo
+git -C ~/git/libbpfgo checkout v0.9.2-libbpf-1.5.1
+git -C ~/git/libbpfgo submodule update --init --recursive
+make -C ~/git/libbpfgo libbpfgo-static
+go install github.com/magefile/mage@latest
+
+# 6) Generate against the live kernel and build.
+# IOR_FORCE_GENERATE=1 skips the strict diff against the committed audit
+# (generated on a different kernel build).
+cd ~/git/ior
+IOR_FORCE_GENERATE=1 mage generate
+mage all
+
+# 7) Smoke test.
+sudo ./ior -plain -duration 5
+```
+
+If `./ior -plain -duration 5` prints `Probing for 5s` and a stream of CSV rows,
+the install is good.
+
+## libbpfgo toolchain
+
+`ior` links against a locally built `libbpfgo` checkout. By default
+`Magefile.go` expects that checkout at `../libbpfgo` relative to this repo; set
+`LIBBPFGO=/absolute/path/to/libbpfgo` to override.
+
+Pin that checkout to `v0.9.2-libbpf-1.5.1` and rebuild the static artifacts
+before running `mage` targets:
+
+```shell
+git -C ../libbpfgo checkout v0.9.2-libbpf-1.5.1
+git -C ../libbpfgo submodule update --init --recursive
+make -C ../libbpfgo libbpfgo-static
+```
+
+Validated commands for this pin:
+
+```shell
+mage world
+mage integrationTest
+```
+
+Troubleshooting and rollback:
+
+- If builds fail with `bpf/bpf.h` missing, re-run the checkout, submodule
+ sync, and `make libbpfgo-static` commands above, then retry
+ `mage world`.
+- Prefer Mage targets over raw `go test` for packages that import `libbpfgo`;
+ Mage injects the required `CGO_CFLAGS`, `CGO_LDFLAGS`, and `LIBBPFGO` values.
+- To roll back to the previous pin, reset to commit `90dbffffbdab`
+ (`v0.6.0-libbpf-1.3.0.20240111220235-90dbffffbdab`) and rebuild:
+
+```shell
+git -C ../libbpfgo checkout 90dbffffbdab
+git -C ../libbpfgo submodule update --init --recursive
+make -C ../libbpfgo libbpfgo-static
+```
+
+## Compile once, run everywhere
+
+The full build dance above only has to happen on **one** machine. The resulting
+`ior` binary is portable across Linux hosts: `scp ior other-host:/usr/local/bin/`
+and run it there.
+
+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
+ 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
+ recompilation.
+
+Pick one Rocky 9 / Fedora box, do the build dance once, then distribute the
+23 MB binary to wherever you want to trace. The build host needs all the dev
+tooling; the trace hosts need only a BTF-enabled kernel and `sudo`.
+
+## Timing semantics
+
+Each reported event pair has two timing counters:
+
+- `durationNs`: syscall runtime on the same thread (`exit(current) - enter(current)`).
+- `durationToPrevNs`: inter-syscall gap on the same thread (`enter(current) - exit(previous)`).
+
+Important details:
+
+- `durationToPrevNs` is tracked per `tid` (thread), not globally across all threads.
+- The first observed syscall pair for a thread has `durationToPrevNs = 0` because
+ there is no prior exit timestamp.
+- `durationToPrevNs` is attributed to the current syscall pair (the one whose
+ `enter` closes the gap).
+- There is no separate "idle" pseudo-event bucket; use the `durationToPrev` count
+ field when aggregated flamegraph output should emphasize inter-syscall time.
diff --git a/docs/tui-reference.md b/docs/tui-reference.md
new file mode 100644
index 0000000..d6a7266
--- /dev/null
+++ b/docs/tui-reference.md
@@ -0,0 +1,175 @@
+# TUI Reference
+
+## TUI Flamegraphs
+
+Flamegraphs are available only inside the TUI dashboard.
+Use `-fields` to change the stack order and `-count` to choose the metric.
+The default stack order is `comm,path,tracepoint` (bottom to top).
+
+## Recording Modes
+
+`ior` has four distinct output flows:
+
+| Mode | How to use it | What it writes | Filter behavior |
+| --- | --- | --- | --- |
+| TUI dashboard | default startup | nothing continuously; data stays in memory unless you export | current TUI/global filters drive what you see |
+| TUI CSV snapshot export | press `e` in the dashboard | one `ior-stream-<timestamp>.csv` snapshot of the current filtered stream view | exports only the currently filtered in-memory rows |
+| Headless `.ior.zst` export | start with `-flamegraph -name <name>` | one aggregated native trace artifact written at shutdown | no TUI filter stack; this is the native trace/integration workflow |
+| Parquet recording | press `R` in the TUI, or start with `-parquet <file>` | a streaming Parquet file of traced syscall rows | TUI mode records rows that pass the active TUI filter; headless `-parquet` records all traced rows |
+
+Important distinction:
+
+- `.ior.zst` output is an aggregated native artifact, not a row-by-row event log.
+- CSV export is a point-in-time snapshot of the ring buffer.
+- Parquet recording is a streaming capture from start to stop.
+- The ring buffer is capped, so CSV export is not a replacement for Parquet recording or `.ior.zst` output.
+
+### Headless Native `.ior.zst` Output
+
+Use `-flamegraph` when you want the native `ior` trace artifact instead of a streaming row log:
+
+```shell
+sudo ./ior -flamegraph -name trace-run -duration 60
+```
+
+Native `.ior.zst` behavior:
+
+- writes one `*.ior.zst` file when the run ends
+- stores aggregated counters for repeated syscall/path/process combinations
+- is intended for `ior`'s native flamegraph and integration-style workflows
+- does not preserve one output row per traced syscall
+
+### TUI Parquet Recording
+
+Start a recording from the dashboard with `R`.
+
+- First `R`: open a filename prompt (`ior-recording-<timestamp>.parquet` by default).
+- `Enter`: start recording to that file.
+- Second `R`: stop and finalize the active Parquet file.
+- Recording stops automatically when you quit the TUI or reselect PID/TID/session scope.
+
+Lifecycle details:
+
+- TUI recording uses the active TUI global filter at emission time.
+- If a filter change restarts tracing, the recorder stays alive and continues writing matching rows after the restart.
+- The dashboard footer shows the active recording path or the last recording error.
+
+### Headless Parquet Recording
+
+Use `-parquet` to skip the TUI and stream traced syscall rows directly to a Parquet file:
+
+```shell
+sudo ./ior -parquet trace.parquet -duration 60
+```
+
+Headless Parquet mode behavior:
+
+- skips the TUI completely
+- records all traced rows
+- rejects content filters such as `-comm`, `-path`, `-pid`, and `-tid`
+- cannot be combined with `-plain`, `-flamegraph`, `--testflames`, or `--testliveflames`
+
+Use headless mode when you want a full recording, and TUI mode when you want interactive filtering plus optional start/stop recording from the dashboard.
+
+### Choosing Between `.ior.zst` and Parquet
+
+| Question | Native `.ior.zst` | Parquet |
+| --- | --- | --- |
+| Data shape | aggregated counters | one row per traced syscall |
+| Write pattern | collect in memory, write one compressed artifact at the end | stream rows continuously while recording |
+| Best for | `ior`-native trace artifacts, flamegraph workflows, integration assertions | offline analysis in other tools, long captures, preserving per-event detail |
+| Relative write cost | usually lower because repeated events are folded together before file write | usually higher because each traced row is serialized |
+| Detail retained | loses original row order and per-event granularity | keeps per-event timing and syscall fields |
+
+Rule of thumb:
+
+- choose `.ior.zst` when you want the native `ior` artifact and do not need every traced syscall row preserved
+- choose Parquet when you want a full event stream for downstream analysis outside `ior`
+
+## TUI Navigation
+
+The TUI has an in-screen help panel (toggle with **H**) that lists all available
+keys. Use it to discover shortcuts without consulting this document.
+
+Dashboard tabs:
+
+- **tab** / **shift+tab** — next / previous tab
+- **1** — Overview
+- **2** — Syscalls
+- **3** — Files
+- **4** — Processes
+- **5** — Latency+Gaps
+- **6** — Stream
+
+The TUI has two key scopes:
+
+- Global hotkeys: available from any dashboard screen.
+- Dashboard hotkeys: behavior that depends on the active tab (especially `6:Stream`).
+
+### Global Hotkeys
+
+- `tab` / `shift+tab`: cycle tabs.
+- `1`–`6`: jump to tab by number (`7` is an alias for `6`).
+- `e`: export filtered stream rows to CSV (`ior-stream-<timestamp>.csv`).
+- `R`: start or stop Parquet recording.
+- `p`: re-open process selector (PID selection flow).
+- `t`: open TID selector flow.
+- `o`: open probe selection/toggling dialog.
+- `r`: refresh dashboard snapshot.
+- `H`: toggle bottom help sections on/off.
+- `q` or `ctrl+c`: quit.
+
+### Dashboard / Tab-Specific Hotkeys
+
+- `d` in `3:Files`: toggle directory-grouped files view.
+- `s` in sortable tabs (`2:Syscalls`, `3:Files`, `4:Processes`): sort by selected column.
+- `S` in sortable tabs: reverse-sort by selected column.
+- `j/k` or `up/down` in list tabs: scroll list.
+
+`left/right` and `h/l` do not switch tabs. In `6:Stream` paused mode they move the selected column.
+
+### 6:Stream Hotkeys and Behavior
+
+`6:Stream` has two modes:
+
+- Live mode (`paused=false`): rows update continuously.
+- Pause mode (`paused=true`): selection/cell/filter/search/export workflows are enabled.
+
+Core controls:
+
+- `space`: toggle live/pause.
+- `g`/`G`: jump to top/tail.
+- `c`: clear stream filters.
+- `f`: open advanced filter modal.
+- `j/k` or `up/down`: move selected row (pause) or scroll (live).
+- `left/right` or `h/l`: move selected column in pause mode.
+
+#### Enter-Based Filter Stack (Pause Mode)
+
+In pause mode, `enter` on the selected cell pushes a filter onto a stack and
+immediately re-filters the current ring buffer snapshot. Filters are stackable.
+
+- String columns use case-insensitive substring match:
+ - `Comm` → `comm~<value>`
+ - `Syscall` → `syscall~<value>`
+ - `File` → `file~<value>`
+- Numeric exact match: `PID`, `TID`, `FD`, `Ret`, `Bytes`
+- Numeric threshold (`>=`): `Latency` → `latency>=selected_value`, `Gap` → `gap>=selected_value`
+
+`esc` in pause mode pops the most recent filter (LIFO); repeated `esc` undoes
+all stacked filters.
+
+#### Regex Search (Pause Mode)
+
+- `/`: search forward; `?`: search backward.
+- Search checks all stream columns and wraps around the ring buffer.
+- `n` / `N`: next / previous match.
+
+#### Stream CSV Export (Pause Mode)
+
+- `x`: quick export filtered stream rows to CSV.
+- `X`: export with filename prompt.
+- `E`: open last stream-exported CSV in foreground editor (`EDITOR` → `VISUAL` → `SUDO_EDITOR` → `hx` → `vi`).
+
+`e` (global) exports a fresh filtered snapshot even outside paused mode; `x`/`X`
+export the exact paused view.
diff --git a/internal/c/generated_tracepoints.c b/internal/c/generated_tracepoints.c
index 5bc3110..1633966 100644
--- a/internal/c/generated_tracepoints.c
+++ b/internal/c/generated_tracepoints.c
@@ -1,7 +1,7 @@
// Code generated - don't change manually!
-/// Ignoring sys_enter_accept4 sys_exit_accept4 as possibly not file I/O related
/// Ignoring sys_enter_accept sys_exit_accept as possibly not file I/O related
+/// Ignoring sys_enter_accept4 sys_exit_accept4 as possibly not file I/O related
/// Ignoring sys_enter_acct sys_exit_acct as possibly not file I/O related
/// Ignoring sys_enter_add_key sys_exit_add_key as possibly not file I/O related
/// Ignoring sys_enter_adjtimex sys_exit_adjtimex as possibly not file I/O related
@@ -17,20 +17,20 @@
/// Ignoring sys_enter_clock_gettime sys_exit_clock_gettime as possibly not file I/O related
/// Ignoring sys_enter_clock_nanosleep sys_exit_clock_nanosleep as possibly not file I/O related
/// Ignoring sys_enter_clock_settime sys_exit_clock_settime as possibly not file I/O related
-/// Ignoring sys_enter_clone3 sys_exit_clone3 as possibly not file I/O related
/// Ignoring sys_enter_clone sys_exit_clone as possibly not file I/O related
+/// Ignoring sys_enter_clone3 sys_exit_clone3 as possibly not file I/O related
/// Ignoring sys_enter_connect sys_exit_connect as possibly not file I/O related
/// Ignoring sys_enter_delete_module sys_exit_delete_module as possibly not file I/O related
-/// Ignoring sys_enter_epoll_create1 sys_exit_epoll_create1 as possibly not file I/O related
/// Ignoring sys_enter_epoll_create sys_exit_epoll_create as possibly not file I/O related
+/// Ignoring sys_enter_epoll_create1 sys_exit_epoll_create1 as possibly not file I/O related
/// Ignoring sys_enter_epoll_ctl sys_exit_epoll_ctl as possibly not file I/O related
-/// Ignoring sys_enter_epoll_pwait2 sys_exit_epoll_pwait2 as possibly not file I/O related
/// Ignoring sys_enter_epoll_pwait sys_exit_epoll_pwait as possibly not file I/O related
+/// Ignoring sys_enter_epoll_pwait2 sys_exit_epoll_pwait2 as possibly not file I/O related
/// Ignoring sys_enter_epoll_wait sys_exit_epoll_wait as possibly not file I/O related
-/// Ignoring sys_enter_eventfd2 sys_exit_eventfd2 as possibly not file I/O related
/// Ignoring sys_enter_eventfd sys_exit_eventfd as possibly not file I/O related
-/// Ignoring sys_enter_execveat sys_exit_execveat as possibly not file I/O related
+/// Ignoring sys_enter_eventfd2 sys_exit_eventfd2 as possibly not file I/O related
/// Ignoring sys_enter_execve sys_exit_execve as possibly not file I/O related
+/// Ignoring sys_enter_execveat sys_exit_execveat as possibly not file I/O related
/// Ignoring sys_enter_exit sys_exit_exit as possibly not file I/O related
/// Ignoring sys_enter_exit_group sys_exit_exit_group as possibly not file I/O related
/// Ignoring sys_enter_fanotify_init sys_exit_fanotify_init as possibly not file I/O related
@@ -42,13 +42,14 @@
/// Ignoring sys_enter_futex_wait sys_exit_futex_wait as possibly not file I/O related
/// Ignoring sys_enter_futex_waitv sys_exit_futex_waitv as possibly not file I/O related
/// Ignoring sys_enter_futex_wake sys_exit_futex_wake as possibly not file I/O related
+/// Ignoring sys_enter_get_mempolicy sys_exit_get_mempolicy as possibly not file I/O related
+/// Ignoring sys_enter_get_robust_list sys_exit_get_robust_list as possibly not file I/O related
/// Ignoring sys_enter_getcpu sys_exit_getcpu as possibly not file I/O related
/// Ignoring sys_enter_getegid sys_exit_getegid as possibly not file I/O related
/// Ignoring sys_enter_geteuid sys_exit_geteuid as possibly not file I/O related
/// Ignoring sys_enter_getgid sys_exit_getgid as possibly not file I/O related
/// Ignoring sys_enter_getgroups sys_exit_getgroups as possibly not file I/O related
/// Ignoring sys_enter_getitimer sys_exit_getitimer as possibly not file I/O related
-/// Ignoring sys_enter_get_mempolicy sys_exit_get_mempolicy as possibly not file I/O related
/// Ignoring sys_enter_getpeername sys_exit_getpeername as possibly not file I/O related
/// Ignoring sys_enter_getpgid sys_exit_getpgid as possibly not file I/O related
/// Ignoring sys_enter_getpgrp sys_exit_getpgrp as possibly not file I/O related
@@ -59,7 +60,6 @@
/// Ignoring sys_enter_getresgid sys_exit_getresgid as possibly not file I/O related
/// Ignoring sys_enter_getresuid sys_exit_getresuid as possibly not file I/O related
/// Ignoring sys_enter_getrlimit sys_exit_getrlimit as possibly not file I/O related
-/// Ignoring sys_enter_get_robust_list sys_exit_get_robust_list as possibly not file I/O related
/// Ignoring sys_enter_getrusage sys_exit_getrusage as possibly not file I/O related
/// Ignoring sys_enter_getsid sys_exit_getsid as possibly not file I/O related
/// Ignoring sys_enter_getsockname sys_exit_getsockname as possibly not file I/O related
@@ -69,8 +69,8 @@
/// Ignoring sys_enter_getuid sys_exit_getuid as possibly not file I/O related
/// Ignoring sys_enter_init_module sys_exit_init_module as possibly not file I/O related
/// Ignoring sys_enter_inotify_add_watch sys_exit_inotify_add_watch as possibly not file I/O related
-/// Ignoring sys_enter_inotify_init1 sys_exit_inotify_init1 as possibly not file I/O related
/// Ignoring sys_enter_inotify_init sys_exit_inotify_init as possibly not file I/O related
+/// Ignoring sys_enter_inotify_init1 sys_exit_inotify_init1 as possibly not file I/O related
/// Ignoring sys_enter_inotify_rm_watch sys_exit_inotify_rm_watch as possibly not file I/O related
/// Ignoring sys_enter_ioperm sys_exit_ioperm as possibly not file I/O related
/// Ignoring sys_enter_iopl sys_exit_iopl as possibly not file I/O related
@@ -98,11 +98,11 @@
/// Ignoring sys_enter_memfd_secret sys_exit_memfd_secret as possibly not file I/O related
/// Ignoring sys_enter_migrate_pages sys_exit_migrate_pages as possibly not file I/O related
/// Ignoring sys_enter_mincore sys_exit_mincore as possibly not file I/O related
-/// Ignoring sys_enter_mknodat sys_exit_mknodat as possibly not file I/O related
/// Ignoring sys_enter_mknod sys_exit_mknod as possibly not file I/O related
+/// Ignoring sys_enter_mknodat sys_exit_mknodat as possibly not file I/O related
+/// Ignoring sys_enter_mlock sys_exit_mlock as possibly not file I/O related
/// Ignoring sys_enter_mlock2 sys_exit_mlock2 as possibly not file I/O related
/// Ignoring sys_enter_mlockall sys_exit_mlockall as possibly not file I/O related
-/// Ignoring sys_enter_mlock sys_exit_mlock as possibly not file I/O related
/// Ignoring sys_enter_modify_ldt sys_exit_modify_ldt as possibly not file I/O related
/// Ignoring sys_enter_mount sys_exit_mount as possibly not file I/O related
/// Ignoring sys_enter_move_mount sys_exit_move_mount as possibly not file I/O related
@@ -120,8 +120,8 @@
/// Ignoring sys_enter_msgget sys_exit_msgget as possibly not file I/O related
/// Ignoring sys_enter_msgrcv sys_exit_msgrcv as possibly not file I/O related
/// Ignoring sys_enter_msgsnd sys_exit_msgsnd as possibly not file I/O related
-/// Ignoring sys_enter_munlockall sys_exit_munlockall as possibly not file I/O related
/// Ignoring sys_enter_munlock sys_exit_munlock as possibly not file I/O related
+/// Ignoring sys_enter_munlockall sys_exit_munlockall as possibly not file I/O related
/// Ignoring sys_enter_munmap sys_exit_munmap as possibly not file I/O related
/// Ignoring sys_enter_nanosleep sys_exit_nanosleep as possibly not file I/O related
/// Ignoring sys_enter_newuname sys_exit_newuname as possibly not file I/O related
@@ -130,8 +130,8 @@
/// Ignoring sys_enter_personality sys_exit_personality as possibly not file I/O related
/// Ignoring sys_enter_pidfd_open sys_exit_pidfd_open as possibly not file I/O related
/// Ignoring sys_enter_pidfd_send_signal sys_exit_pidfd_send_signal as possibly not file I/O related
-/// Ignoring sys_enter_pipe2 sys_exit_pipe2 as possibly not file I/O related
/// Ignoring sys_enter_pipe sys_exit_pipe as possibly not file I/O related
+/// Ignoring sys_enter_pipe2 sys_exit_pipe2 as possibly not file I/O related
/// Ignoring sys_enter_pivot_root sys_exit_pivot_root as possibly not file I/O related
/// Ignoring sys_enter_pkey_alloc sys_exit_pkey_alloc as possibly not file I/O related
/// Ignoring sys_enter_pkey_free sys_exit_pkey_free as possibly not file I/O related
@@ -163,11 +163,11 @@
/// Ignoring sys_enter_rt_sigsuspend sys_exit_rt_sigsuspend as possibly not file I/O related
/// Ignoring sys_enter_rt_sigtimedwait sys_exit_rt_sigtimedwait as possibly not file I/O related
/// Ignoring sys_enter_rt_tgsigqueueinfo sys_exit_rt_tgsigqueueinfo as possibly not file I/O related
+/// Ignoring sys_enter_sched_get_priority_max sys_exit_sched_get_priority_max as possibly not file I/O related
+/// Ignoring sys_enter_sched_get_priority_min sys_exit_sched_get_priority_min as possibly not file I/O related
/// Ignoring sys_enter_sched_getaffinity sys_exit_sched_getaffinity as possibly not file I/O related
/// Ignoring sys_enter_sched_getattr sys_exit_sched_getattr as possibly not file I/O related
/// Ignoring sys_enter_sched_getparam sys_exit_sched_getparam as possibly not file I/O related
-/// Ignoring sys_enter_sched_get_priority_max sys_exit_sched_get_priority_max as possibly not file I/O related
-/// Ignoring sys_enter_sched_get_priority_min sys_exit_sched_get_priority_min as possibly not file I/O related
/// Ignoring sys_enter_sched_getscheduler sys_exit_sched_getscheduler as possibly not file I/O related
/// Ignoring sys_enter_sched_rr_get_interval sys_exit_sched_rr_get_interval as possibly not file I/O related
/// Ignoring sys_enter_sched_setaffinity sys_exit_sched_setaffinity as possibly not file I/O related
@@ -185,6 +185,10 @@
/// Ignoring sys_enter_sendmmsg sys_exit_sendmmsg as possibly not file I/O related
/// Ignoring sys_enter_sendmsg sys_exit_sendmsg as possibly not file I/O related
/// Ignoring sys_enter_sendto sys_exit_sendto as possibly not file I/O related
+/// Ignoring sys_enter_set_mempolicy sys_exit_set_mempolicy as possibly not file I/O related
+/// Ignoring sys_enter_set_mempolicy_home_node sys_exit_set_mempolicy_home_node as possibly not file I/O related
+/// Ignoring sys_enter_set_robust_list sys_exit_set_robust_list as possibly not file I/O related
+/// Ignoring sys_enter_set_tid_address sys_exit_set_tid_address as possibly not file I/O related
/// Ignoring sys_enter_setdomainname sys_exit_setdomainname as possibly not file I/O related
/// Ignoring sys_enter_setfsgid sys_exit_setfsgid as possibly not file I/O related
/// Ignoring sys_enter_setfsuid sys_exit_setfsuid as possibly not file I/O related
@@ -192,8 +196,6 @@
/// Ignoring sys_enter_setgroups sys_exit_setgroups as possibly not file I/O related
/// Ignoring sys_enter_sethostname sys_exit_sethostname as possibly not file I/O related
/// Ignoring sys_enter_setitimer sys_exit_setitimer as possibly not file I/O related
-/// Ignoring sys_enter_set_mempolicy sys_exit_set_mempolicy as possibly not file I/O related
-/// Ignoring sys_enter_set_mempolicy_home_node sys_exit_set_mempolicy_home_node as possibly not file I/O related
/// Ignoring sys_enter_setns sys_exit_setns as possibly not file I/O related
/// Ignoring sys_enter_setpgid sys_exit_setpgid as possibly not file I/O related
/// Ignoring sys_enter_setpriority sys_exit_setpriority as possibly not file I/O related
@@ -202,10 +204,8 @@
/// Ignoring sys_enter_setresuid sys_exit_setresuid as possibly not file I/O related
/// Ignoring sys_enter_setreuid sys_exit_setreuid as possibly not file I/O related
/// Ignoring sys_enter_setrlimit sys_exit_setrlimit as possibly not file I/O related
-/// Ignoring sys_enter_set_robust_list sys_exit_set_robust_list as possibly not file I/O related
/// Ignoring sys_enter_setsid sys_exit_setsid as possibly not file I/O related
/// Ignoring sys_enter_setsockopt sys_exit_setsockopt as possibly not file I/O related
-/// Ignoring sys_enter_set_tid_address sys_exit_set_tid_address as possibly not file I/O related
/// Ignoring sys_enter_settimeofday sys_exit_settimeofday as possibly not file I/O related
/// Ignoring sys_enter_setuid sys_exit_setuid as possibly not file I/O related
/// Ignoring sys_enter_shmat sys_exit_shmat as possibly not file I/O related
@@ -214,8 +214,8 @@
/// Ignoring sys_enter_shmget sys_exit_shmget as possibly not file I/O related
/// Ignoring sys_enter_shutdown sys_exit_shutdown as possibly not file I/O related
/// Ignoring sys_enter_sigaltstack sys_exit_sigaltstack as possibly not file I/O related
-/// Ignoring sys_enter_signalfd4 sys_exit_signalfd4 as possibly not file I/O related
/// Ignoring sys_enter_signalfd sys_exit_signalfd as possibly not file I/O related
+/// Ignoring sys_enter_signalfd4 sys_exit_signalfd4 as possibly not file I/O related
/// Ignoring sys_enter_socket sys_exit_socket as possibly not file I/O related
/// Ignoring sys_enter_socketpair sys_exit_socketpair as possibly not file I/O related
/// Ignoring sys_enter_splice sys_exit_splice as possibly not file I/O related
@@ -229,12 +229,12 @@
/// Ignoring sys_enter_time sys_exit_time as possibly not file I/O related
/// Ignoring sys_enter_timer_create sys_exit_timer_create as possibly not file I/O related
/// Ignoring sys_enter_timer_delete sys_exit_timer_delete as possibly not file I/O related
-/// Ignoring sys_enter_timerfd_create sys_exit_timerfd_create as possibly not file I/O related
-/// Ignoring sys_enter_timerfd_gettime sys_exit_timerfd_gettime as possibly not file I/O related
-/// Ignoring sys_enter_timerfd_settime sys_exit_timerfd_settime as possibly not file I/O related
/// Ignoring sys_enter_timer_getoverrun sys_exit_timer_getoverrun as possibly not file I/O related
/// Ignoring sys_enter_timer_gettime sys_exit_timer_gettime as possibly not file I/O related
/// Ignoring sys_enter_timer_settime sys_exit_timer_settime as possibly not file I/O related
+/// Ignoring sys_enter_timerfd_create sys_exit_timerfd_create as possibly not file I/O related
+/// Ignoring sys_enter_timerfd_gettime sys_exit_timerfd_gettime as possibly not file I/O related
+/// Ignoring sys_enter_timerfd_settime sys_exit_timerfd_settime as possibly not file I/O related
/// Ignoring sys_enter_times sys_exit_times as possibly not file I/O related
/// Ignoring sys_enter_tkill sys_exit_tkill as possibly not file I/O related
/// Ignoring sys_enter_umask sys_exit_umask as possibly not file I/O related
diff --git a/scripts/build-with-docker.sh b/scripts/build-with-docker.sh
new file mode 100755
index 0000000..02eb60f
--- /dev/null
+++ b/scripts/build-with-docker.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# Build the ior binary inside a Rocky Linux 9 container and write it to the
+# current directory. The container image is built once and reused on subsequent
+# runs. The ior source tree is mounted as a volume so the resulting binary
+# lands directly in $(pwd)/ior.
+#
+# Usage:
+# ./build-with-docker.sh # build image + compile ior
+# ./build-with-docker.sh --build # force rebuild of the Docker image
+# ./build-with-docker.sh --run # skip image build, only compile ior
+set -euo pipefail
+
+IMAGE="ior-builder:rocky9"
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+# Derive the Go version from go.mod so the Docker image always matches the
+# minimum toolchain declared by the project.
+GO_VERSION="$(grep '^go ' "${REPO_ROOT}/go.mod" | awk '{print $2}')"
+
+BUILD_IMAGE=true
+RUN_BUILD=true
+
+for arg in "$@"; do
+ case "$arg" in
+ --build) BUILD_IMAGE=true; RUN_BUILD=false ;;
+ --run) BUILD_IMAGE=false; RUN_BUILD=true ;;
+ esac
+done
+
+if $BUILD_IMAGE; then
+ echo "==> Building Docker image ${IMAGE} (this takes ~15-20 min on first run)..."
+ docker build --build-arg "GO_VERSION=${GO_VERSION}" -t "${IMAGE}" "${REPO_ROOT}"
+ echo "==> Image build complete."
+fi
+
+if $RUN_BUILD; then
+ echo "==> Compiling ior inside the container..."
+ # --privileged gives full host capabilities.
+ # tracefs (/sys/kernel/tracing) and BTF (/sys/kernel/btf) are not auto-mounted
+ # by Docker even with --privileged, so they are mounted explicitly:
+ # - /sys/kernel/tracing : mage generate reads available syscall tracepoints
+ # - /sys/kernel/btf : mage bpfBuild reads vmlinux BTF for vmlinux.h
+ docker run --rm \
+ --privileged \
+ -v /sys/kernel/tracing:/sys/kernel/tracing \
+ -v /sys/kernel/btf:/sys/kernel/btf \
+ -v "${REPO_ROOT}:/git/ior" \
+ "${IMAGE}"
+ echo "==> Done. Binary written to ${REPO_ROOT}/ior"
+fi