summaryrefslogtreecommitdiff
path: root/docs/tutorial
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-06 09:42:43 +0300
committerPaul Buetow <paul@buetow.org>2026-05-06 09:42:43 +0300
commitd78a2530da91b76625b71c2aeaf3293abc6c3a4b (patch)
tree5d59a7a1014955564b9f2c30decaf6be257e61cf /docs/tutorial
parentfbb7c9a9ad8d03d5d095ac441a58b37537e0ab8d (diff)
move demo/ to docs/tutorial/, commit assets, consolidate TUI docs
- demo/ renamed to docs/tutorial/ (tapes, scripts, TUTORIAL.md) - docs/tutorial/assets/ added to git (51 MB of GIFs + PNGs); removed /demo/assets/ from .gitignore so images render on Codeberg - docs/tui-reference.md removed; its hotkey tables merged into a new "Hotkey Quick Reference" section at the end of TUTORIAL.md - TUTORIAL.md: updated install section (mage buildDocker, no GOTOOLCHAIN=auto), fixed README relative path (../../README.md), updated internal tapes/scripts/assets path prose - README.md: updated all demo/ image paths and links to docs/tutorial/; TUI and recording-modes links now point to TUTORIAL.md anchors - AGENTS.md: updated demo/ references to docs/tutorial/ - Magefile.go: updated demoDir/demoTapesDir/demoScriptsDir/demoRunTape/ demoSudoKeepers constants to docs/tutorial/ paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'docs/tutorial')
-rw-r--r--docs/tutorial/TUTORIAL.md245
-rw-r--r--docs/tutorial/assets/00-hero-flamegraph.pngbin0 -> 156731 bytes
-rw-r--r--docs/tutorial/assets/00-logo.pngbin0 -> 332693 bytes
-rw-r--r--docs/tutorial/assets/01-launch.gifbin0 -> 4244461 bytes
-rw-r--r--docs/tutorial/assets/02-overview-tab.gifbin0 -> 263106 bytes
-rw-r--r--docs/tutorial/assets/03-syscalls-tab.gifbin0 -> 1091628 bytes
-rw-r--r--docs/tutorial/assets/04-files-tab.gifbin0 -> 1577094 bytes
-rw-r--r--docs/tutorial/assets/05-processes-tab.gifbin0 -> 1034718 bytes
-rw-r--r--docs/tutorial/assets/06-latency-gaps-tab.gifbin0 -> 300466 bytes
-rw-r--r--docs/tutorial/assets/07-stream-live.gifbin0 -> 7063227 bytes
-rw-r--r--docs/tutorial/assets/08-stream-pause-filter.gifbin0 -> 5796596 bytes
-rw-r--r--docs/tutorial/assets/09-stream-regex-search.gifbin0 -> 5367913 bytes
-rw-r--r--docs/tutorial/assets/10-stream-csv-export.gifbin0 -> 4671995 bytes
-rw-r--r--docs/tutorial/assets/11-pid-tid-probe.gifbin0 -> 3584783 bytes
-rw-r--r--docs/tutorial/assets/12-parquet-recording.gifbin0 -> 2353504 bytes
-rw-r--r--docs/tutorial/assets/13-tui-flamegraph.gifbin0 -> 6333590 bytes
-rw-r--r--docs/tutorial/assets/13a-order-by-process.pngbin0 -> 231292 bytes
-rw-r--r--docs/tutorial/assets/13b-order-by-path.pngbin0 -> 218540 bytes
-rw-r--r--docs/tutorial/assets/13c-order-by-syscall.pngbin0 -> 255276 bytes
-rw-r--r--docs/tutorial/assets/13d-order-by-pid.pngbin0 -> 183662 bytes
-rw-r--r--docs/tutorial/assets/13e-order-by-process-paths.pngbin0 -> 214308 bytes
-rw-r--r--docs/tutorial/assets/14-headless-modes.gifbin0 -> 4791550 bytes
-rw-r--r--docs/tutorial/assets/screenshot-dashboard.pngbin0 -> 173558 bytes
-rw-r--r--docs/tutorial/assets/screenshot-files-grouped.pngbin0 -> 207344 bytes
-rw-r--r--docs/tutorial/assets/screenshot-flamegraph.pngbin0 -> 171887 bytes
-rw-r--r--docs/tutorial/assets/screenshot-latency.pngbin0 -> 105203 bytes
-rw-r--r--docs/tutorial/assets/screenshot-overview.pngbin0 -> 167438 bytes
-rw-r--r--docs/tutorial/assets/screenshot-pidpicker-open.pngbin0 -> 122673 bytes
-rw-r--r--docs/tutorial/assets/screenshot-pidpicker.pngbin0 -> 122249 bytes
-rw-r--r--docs/tutorial/assets/screenshot-probes.pngbin0 -> 157287 bytes
-rw-r--r--docs/tutorial/assets/screenshot-processes.pngbin0 -> 191556 bytes
-rw-r--r--docs/tutorial/assets/screenshot-recording-active.pngbin0 -> 188855 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-export.pngbin0 -> 28883 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-filtered.pngbin0 -> 198074 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-live.pngbin0 -> 29593 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-search.pngbin0 -> 169878 bytes
-rw-r--r--docs/tutorial/assets/screenshot-syscalls.pngbin0 -> 226128 bytes
-rw-r--r--docs/tutorial/assets/screenshot-tidpicker.pngbin0 -> 121954 bytes
-rwxr-xr-xdocs/tutorial/scripts/run-tape.sh52
-rwxr-xr-xdocs/tutorial/scripts/sudo-keepalive.sh13
-rwxr-xr-xdocs/tutorial/scripts/workload.sh77
-rw-r--r--docs/tutorial/tapes/01-launch.tape23
-rw-r--r--docs/tutorial/tapes/02-overview-tab.tape23
-rw-r--r--docs/tutorial/tapes/03-syscalls-tab.tape36
-rw-r--r--docs/tutorial/tapes/04-files-tab.tape32
-rw-r--r--docs/tutorial/tapes/05-processes-tab.tape30
-rw-r--r--docs/tutorial/tapes/06-latency-gaps-tab.tape22
-rw-r--r--docs/tutorial/tapes/07-stream-live.tape22
-rw-r--r--docs/tutorial/tapes/08-stream-pause-filter.tape46
-rw-r--r--docs/tutorial/tapes/09-stream-regex-search.tape40
-rw-r--r--docs/tutorial/tapes/10-stream-csv-export.tape28
-rw-r--r--docs/tutorial/tapes/11-pid-tid-probe.tape41
-rw-r--r--docs/tutorial/tapes/12-parquet-recording.tape28
-rw-r--r--docs/tutorial/tapes/13-tui-flamegraph.tape22
-rw-r--r--docs/tutorial/tapes/14-headless-modes.tape38
55 files changed, 818 insertions, 0 deletions
diff --git a/docs/tutorial/TUTORIAL.md b/docs/tutorial/TUTORIAL.md
new file mode 100644
index 0000000..e53013a
--- /dev/null
+++ b/docs/tutorial/TUTORIAL.md
@@ -0,0 +1,245 @@
+# 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.
+
+Every GIF in this document is regenerated from a [VHS](https://github.com/charmbracelet/vhs) tape under [`demo/tapes/`](./tapes). To rebuild them all, run `sudo -v && mage demo` (see [Regenerating the demo](#regenerating-the-demo)).
+
+## Contents
+
+1. [Installing ior](#installing-ior)
+2. [First launch: the PID picker](#first-launch-the-pid-picker)
+3. [Touring the dashboard tabs](#touring-the-dashboard-tabs)
+ - [1 · Flamegraph (default landing tab)](#1--flamegraph-default-landing-tab)
+ - [2 · Overview](#2--overview)
+ - [3 · Syscalls](#3--syscalls)
+ - [4 · Files](#4--files)
+ - [5 · Processes](#5--processes)
+ - [6 · Latency + Gaps](#6--latency--gaps)
+ - [7 · Stream](#7--stream)
+4. [Mastering the Stream tab](#mastering-the-stream-tab)
+ - [Pause + stacked filters](#pause--stacked-filters)
+ - [Regex search](#regex-search)
+ - [CSV export](#csv-export)
+5. [Choosing what to trace](#choosing-what-to-trace)
+6. [Recording for offline analysis](#recording-for-offline-analysis)
+ - [TUI Parquet recording](#tui-parquet-recording)
+ - [Headless modes](#headless-modes)
+7. [Regenerating the demo](#regenerating-the-demo)
+
+## Installing ior
+
+See the [main README](../../README.md) for full install steps. The quickest path from any Docker-capable Linux host:
+
+```shell
+git clone https://codeberg.org/snonux/ior ~/git/ior
+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):
+
+```shell
+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.
+
+## First launch: the PID picker
+
+`sudo ./ior` starts with the **PID picker**. The cursor is on **All PIDs**, so pressing `Enter` traces the whole system. Type into the filter box to narrow the list by PID, comm, or cmdline; arrow keys move the selection.
+
+![Cold start: PID picker, then the dashboard appears](./assets/01-launch.gif)
+
+The same picker can be re-opened later from the dashboard with `p`.
+
+![PID picker default state](./assets/screenshot-pidpicker.png)
+
+## Touring the dashboard tabs
+
+The dashboard has seven tabs, addressable by number key. The default landing tab is **Flamegraph**. `tab` / `shift+tab` step forward / back.
+
+| Key | Tab | What it shows |
+|-----|-------------------|--------------------------------------------------------------------------|
+| `1` | Flamegraph (`Flm`)| Live FlameGraph of the configured stack (`comm`/`path`/`tracepoint`) |
+| `2` | Overview (`Ovr`) | Sparkline + top syscalls + top paths summary |
+| `3` | Syscalls (`Sys`) | Sortable per-syscall counters, latency, byte volume |
+| `4` | Files (`Fil`) | Per-path counters; `d` toggles directory grouping |
+| `5` | Processes (`Pro`) | Per-process / per-comm counters |
+| `6` | Latency (`Lat`) | Latency + inter-syscall gap histograms |
+| `7` | Stream (`Str`) | Live tail of individual traced events |
+
+### 1 · Flamegraph (default landing tab)
+
+The first thing you see after dismissing the PID picker is the **live flamegraph**. Bars grow as new events come in. `o` cycles the stack ordering (e.g. `comm/path/tracepoint` ↔ `comm/tracepoint/path`); `b` toggles the size metric (event count vs. duration vs. gap).
+
+![Live flamegraph rebuilding from real workload](./assets/13-tui-flamegraph.gif)
+
+### 2 · Overview
+
+Press `2`. The Overview tab is the at-a-glance view: a sparkline of recent event volume, the top syscalls, and the top paths.
+
+![Overview tab populating](./assets/02-overview-tab.gif)
+
+### 3 · Syscalls
+
+Press `3`. A sortable table of every traced syscall (count, average latency, total bytes). `j` / `k` (or arrow keys) scroll the rows; `←` / `→` move the selected column; `s` sorts by the selected column using its default direction; `S` reverses.
+
+![Syscalls table with sort + reverse-sort](./assets/03-syscalls-tab.gif)
+
+### 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/...`.
+
+![Files tab toggling directory grouping](./assets/04-files-tab.gif)
+
+### 5 · Processes
+
+Press `5`. Per-process / per-comm view. `S` reverse-sorts; combine with `←` / `→` to pick a column.
+
+![Processes tab](./assets/05-processes-tab.gif)
+
+### 6 · Latency + Gaps
+
+Press `6`. Two histograms: syscall **latency** (how long the syscall ran) and the inter-syscall **gap** (idle time on the same thread between syscalls). The big-write workload running in the background spreads the latency distribution noticeably.
+
+![Latency + gap histograms](./assets/06-latency-gaps-tab.gif)
+
+### 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.
+
+![Stream tab live-tailing rows](./assets/07-stream-live.gif)
+
+## Mastering the Stream tab
+
+Stream has two modes: **Live** (rows scroll past) and **Pause** (`space` toggles). Almost everything interesting happens in pause mode.
+
+### 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.
+
+![Pause, push two filters, undo with Esc](./assets/08-stream-pause-filter.gif)
+
+The filter is reflected in the bottom status line, and matches the same syntax you'd type by hand: `comm~bash`, `syscall~openat`, `latency>=100000`, etc.
+
+### Regex search
+
+`/` opens a forward regex prompt; `?` opens a backward one. `n` jumps to the next match in the same direction; `N` reverses. The search runs against every column on every row in the ring buffer and wraps at the end.
+
+![Regex search with /, n, n, then ?](./assets/09-stream-regex-search.gif)
+
+### CSV export
+
+Three keys, three flavours:
+
+- `e` — quick export of the **current TUI-filter snapshot** to `ior-stream-<timestamp>.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)
+
+If you don't want CSV export at all, start ior with `-tuiExport=false`; the help footer hides the export keys and `e` becomes a no-op.
+
+## Choosing what to trace
+
+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.
+
+![PID, TID, and probe pickers](./assets/11-pid-tid-probe.gif)
+
+Restricting to a single PID is also exposed as a CLI flag (`-pid <n>`), as is comm/path filtering (`-comm`, `-path`). Tracepoint subsetting on the command line uses `-tps <regex>` / `-tpsExclude <regex>`.
+
+## Recording for offline analysis
+
+ior has three persistence flows; each solves a different problem.
+
+| Flow | How | What you get |
+|------------------------|----------------------------------------------|---------------------------------------------------------|
+| TUI Parquet recording | `R` from the dashboard | streaming Parquet of every row that passes your filter |
+| Headless `.ior.zst` | `sudo ./ior -flamegraph -name <name>` | one aggregated native trace artifact (bandwidth-cheap) |
+| Headless Parquet | `sudo ./ior -parquet trace.parquet` | streaming Parquet, full firehose, no TUI |
+| Plain CSV | `sudo ./ior -plain` | one CSV row per event on stdout |
+
+### TUI Parquet recording
+
+Press `R` in the dashboard, accept the default filename (`ior-recording-<timestamp>.parquet`) with `Enter`, and rows start streaming to disk. The footer shows the active recording path (or the last error). Press `R` again to stop.
+
+![Start, run, and stop a parquet recording](./assets/12-parquet-recording.gif)
+
+The recorder follows your *current* TUI global filter — narrow with `p`/`t`/`o` first if you want a focused capture.
+
+### Headless modes
+
+For unattended captures or scripting, skip the TUI entirely. The demo runs all three back-to-back, capped with `-duration` so each terminates on its own.
+
+![Three headless flows in one tape](./assets/14-headless-modes.gif)
+
+`-flamegraph` writes one aggregated `.ior.zst` artifact at shutdown — ideal for `ior`'s native flamegraph and integration workflows. `-parquet` streams every row, so the file grows continuously. `-plain` is the lightest weight: CSV to stdout you can pipe into anything.
+
+## Regenerating the demo
+
+The whole asset pipeline is reproducible:
+
+```shell
+mage installDemoTools # one-time: VHS via go install + ttyd via dnf
+sudo -v # warm the sudo timestamp once
+mage demo # regen all 14 GIFs + screenshots (~10 min)
+```
+
+Or rebuild a single tape after editing it:
+
+```shell
+TAPE=07-stream-live mage demoOne
+```
+
+Tapes live in [`tapes/`](./tapes), the background workload that drives them is [`scripts/workload.sh`](./scripts/workload.sh), and the resulting assets land in [`assets/`](./assets). VHS records headlessly under `ttyd` + Chromium — no real terminal window opens, so `mage demo` is safe to run in the background while you keep working.
+
+## Hotkey Quick Reference
+
+### Global keys
+
+| Key | Action |
+|-----|--------|
+| `tab` / `shift+tab` | next / previous tab |
+| `1`–`6` (`7` = alias for `6`) | jump to tab by number |
+| `H` | toggle bottom help panel |
+| `e` | export filtered stream snapshot to CSV |
+| `R` | start / stop Parquet recording |
+| `p` | re-open PID picker |
+| `t` | open TID picker |
+| `o` | open probe selection dialog |
+| `r` | refresh dashboard snapshot |
+| `q` / `ctrl+c` | quit |
+
+### Tab-specific keys (`2:Syscalls`, `3:Files`, `4:Processes`)
+
+| Key | Action |
+|-----|--------|
+| `s` | sort by selected column (default direction) |
+| `S` | reverse-sort by selected column |
+| `j`/`k` or `↑`/`↓` | scroll list |
+| `d` (Files only) | toggle directory grouping |
+
+### Stream tab (`7:Stream`)
+
+| Key | Action |
+|-----|--------|
+| `space` | toggle live / pause mode |
+| `g` / `G` | jump to top / tail |
+| `j`/`k` or `↑`/`↓` | move row (pause) / scroll (live) |
+| `←`/`→` or `h`/`l` | move selected column (pause only) |
+| `enter` | push cell value as filter (pause) |
+| `esc` | pop most recent filter (LIFO) |
+| `c` | clear all stream filters |
+| `f` | open advanced filter modal |
+| `/` / `?` | regex search forward / backward |
+| `n` / `N` | next / previous search match |
+| `x` | quick CSV export of paused view |
+| `X` | CSV export with filename prompt |
+| `E` | open last CSV export in `$EDITOR` |
diff --git a/docs/tutorial/assets/00-hero-flamegraph.png b/docs/tutorial/assets/00-hero-flamegraph.png
new file mode 100644
index 0000000..d409e79
--- /dev/null
+++ b/docs/tutorial/assets/00-hero-flamegraph.png
Binary files differ
diff --git a/docs/tutorial/assets/00-logo.png b/docs/tutorial/assets/00-logo.png
new file mode 100644
index 0000000..d2da30c
--- /dev/null
+++ b/docs/tutorial/assets/00-logo.png
Binary files differ
diff --git a/docs/tutorial/assets/01-launch.gif b/docs/tutorial/assets/01-launch.gif
new file mode 100644
index 0000000..978b95e
--- /dev/null
+++ b/docs/tutorial/assets/01-launch.gif
Binary files differ
diff --git a/docs/tutorial/assets/02-overview-tab.gif b/docs/tutorial/assets/02-overview-tab.gif
new file mode 100644
index 0000000..56f0e98
--- /dev/null
+++ b/docs/tutorial/assets/02-overview-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/03-syscalls-tab.gif b/docs/tutorial/assets/03-syscalls-tab.gif
new file mode 100644
index 0000000..47c4029
--- /dev/null
+++ b/docs/tutorial/assets/03-syscalls-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/04-files-tab.gif b/docs/tutorial/assets/04-files-tab.gif
new file mode 100644
index 0000000..af18828
--- /dev/null
+++ b/docs/tutorial/assets/04-files-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/05-processes-tab.gif b/docs/tutorial/assets/05-processes-tab.gif
new file mode 100644
index 0000000..a6beed1
--- /dev/null
+++ b/docs/tutorial/assets/05-processes-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/06-latency-gaps-tab.gif b/docs/tutorial/assets/06-latency-gaps-tab.gif
new file mode 100644
index 0000000..b0d9c81
--- /dev/null
+++ b/docs/tutorial/assets/06-latency-gaps-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/07-stream-live.gif b/docs/tutorial/assets/07-stream-live.gif
new file mode 100644
index 0000000..af742bd
--- /dev/null
+++ b/docs/tutorial/assets/07-stream-live.gif
Binary files differ
diff --git a/docs/tutorial/assets/08-stream-pause-filter.gif b/docs/tutorial/assets/08-stream-pause-filter.gif
new file mode 100644
index 0000000..7821cf7
--- /dev/null
+++ b/docs/tutorial/assets/08-stream-pause-filter.gif
Binary files differ
diff --git a/docs/tutorial/assets/09-stream-regex-search.gif b/docs/tutorial/assets/09-stream-regex-search.gif
new file mode 100644
index 0000000..73e068b
--- /dev/null
+++ b/docs/tutorial/assets/09-stream-regex-search.gif
Binary files differ
diff --git a/docs/tutorial/assets/10-stream-csv-export.gif b/docs/tutorial/assets/10-stream-csv-export.gif
new file mode 100644
index 0000000..aa5bb3b
--- /dev/null
+++ b/docs/tutorial/assets/10-stream-csv-export.gif
Binary files differ
diff --git a/docs/tutorial/assets/11-pid-tid-probe.gif b/docs/tutorial/assets/11-pid-tid-probe.gif
new file mode 100644
index 0000000..f7f96c7
--- /dev/null
+++ b/docs/tutorial/assets/11-pid-tid-probe.gif
Binary files differ
diff --git a/docs/tutorial/assets/12-parquet-recording.gif b/docs/tutorial/assets/12-parquet-recording.gif
new file mode 100644
index 0000000..b9baab3
--- /dev/null
+++ b/docs/tutorial/assets/12-parquet-recording.gif
Binary files differ
diff --git a/docs/tutorial/assets/13-tui-flamegraph.gif b/docs/tutorial/assets/13-tui-flamegraph.gif
new file mode 100644
index 0000000..71aece3
--- /dev/null
+++ b/docs/tutorial/assets/13-tui-flamegraph.gif
Binary files differ
diff --git a/docs/tutorial/assets/13a-order-by-process.png b/docs/tutorial/assets/13a-order-by-process.png
new file mode 100644
index 0000000..8a469bf
--- /dev/null
+++ b/docs/tutorial/assets/13a-order-by-process.png
Binary files differ
diff --git a/docs/tutorial/assets/13b-order-by-path.png b/docs/tutorial/assets/13b-order-by-path.png
new file mode 100644
index 0000000..3518412
--- /dev/null
+++ b/docs/tutorial/assets/13b-order-by-path.png
Binary files differ
diff --git a/docs/tutorial/assets/13c-order-by-syscall.png b/docs/tutorial/assets/13c-order-by-syscall.png
new file mode 100644
index 0000000..3eb5058
--- /dev/null
+++ b/docs/tutorial/assets/13c-order-by-syscall.png
Binary files differ
diff --git a/docs/tutorial/assets/13d-order-by-pid.png b/docs/tutorial/assets/13d-order-by-pid.png
new file mode 100644
index 0000000..0d56f12
--- /dev/null
+++ b/docs/tutorial/assets/13d-order-by-pid.png
Binary files differ
diff --git a/docs/tutorial/assets/13e-order-by-process-paths.png b/docs/tutorial/assets/13e-order-by-process-paths.png
new file mode 100644
index 0000000..ccd450a
--- /dev/null
+++ b/docs/tutorial/assets/13e-order-by-process-paths.png
Binary files differ
diff --git a/docs/tutorial/assets/14-headless-modes.gif b/docs/tutorial/assets/14-headless-modes.gif
new file mode 100644
index 0000000..cf245f4
--- /dev/null
+++ b/docs/tutorial/assets/14-headless-modes.gif
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-dashboard.png b/docs/tutorial/assets/screenshot-dashboard.png
new file mode 100644
index 0000000..0eee11f
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-dashboard.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-files-grouped.png b/docs/tutorial/assets/screenshot-files-grouped.png
new file mode 100644
index 0000000..b2a206a
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-files-grouped.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-flamegraph.png b/docs/tutorial/assets/screenshot-flamegraph.png
new file mode 100644
index 0000000..ae79335
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-flamegraph.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-latency.png b/docs/tutorial/assets/screenshot-latency.png
new file mode 100644
index 0000000..43b6cd8
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-latency.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-overview.png b/docs/tutorial/assets/screenshot-overview.png
new file mode 100644
index 0000000..eac3138
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-overview.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-pidpicker-open.png b/docs/tutorial/assets/screenshot-pidpicker-open.png
new file mode 100644
index 0000000..22a7b93
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-pidpicker-open.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-pidpicker.png b/docs/tutorial/assets/screenshot-pidpicker.png
new file mode 100644
index 0000000..4b8f025
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-pidpicker.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-probes.png b/docs/tutorial/assets/screenshot-probes.png
new file mode 100644
index 0000000..df0664a
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-probes.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-processes.png b/docs/tutorial/assets/screenshot-processes.png
new file mode 100644
index 0000000..c1873d2
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-processes.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-recording-active.png b/docs/tutorial/assets/screenshot-recording-active.png
new file mode 100644
index 0000000..e50d588
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-recording-active.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-export.png b/docs/tutorial/assets/screenshot-stream-export.png
new file mode 100644
index 0000000..921330c
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-export.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-filtered.png b/docs/tutorial/assets/screenshot-stream-filtered.png
new file mode 100644
index 0000000..7b85470
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-filtered.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-live.png b/docs/tutorial/assets/screenshot-stream-live.png
new file mode 100644
index 0000000..a71338c
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-live.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-search.png b/docs/tutorial/assets/screenshot-stream-search.png
new file mode 100644
index 0000000..5243f18
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-search.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-syscalls.png b/docs/tutorial/assets/screenshot-syscalls.png
new file mode 100644
index 0000000..7a63ce3
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-syscalls.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-tidpicker.png b/docs/tutorial/assets/screenshot-tidpicker.png
new file mode 100644
index 0000000..baf08c7
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-tidpicker.png
Binary files differ
diff --git a/docs/tutorial/scripts/run-tape.sh b/docs/tutorial/scripts/run-tape.sh
new file mode 100755
index 0000000..c22e2e3
--- /dev/null
+++ b/docs/tutorial/scripts/run-tape.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# One-tape wrapper: starts the background workload, runs vhs <tape>, kills the
+# workload. Tapes themselves don't need to know about the workload — they just
+# launch ior and drive the TUI.
+#
+# vhs is invoked with cwd set to the repo root, so all paths inside tapes
+# (Output, Screenshot, the ./ior binary) are repo-relative.
+#
+# Usage: run-tape.sh <path-to-tape>
+
+set -euo pipefail
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 <tape-file>" >&2
+ exit 2
+fi
+
+TAPE="$(realpath "$1")"
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+WORKLOAD="${ROOT}/demo/scripts/workload.sh"
+
+if [ ! -f "$TAPE" ]; then
+ echo "tape not found: $TAPE" >&2
+ exit 2
+fi
+
+# Pre-flight: vhs and ttyd must be on PATH; sudo timestamp must be live.
+command -v vhs >/dev/null || { echo "vhs not on PATH (run: mage installDemoTools)" >&2; exit 3; }
+command -v ttyd >/dev/null || { echo "ttyd not on PATH (run: mage installDemoTools)" >&2; exit 3; }
+sudo -n true 2>/dev/null || { echo "sudo timestamp expired (run: sudo -v)" >&2; exit 4; }
+
+# Start workload in its own session/process group so we can kill the whole tree.
+setsid "$WORKLOAD" </dev/null >/dev/null 2>&1 &
+WL_PID=$!
+
+cleanup() {
+ if kill -0 "$WL_PID" 2>/dev/null; then
+ # The workload runs `setsid` so its PID == its PGID.
+ kill -TERM -- "-$WL_PID" 2>/dev/null || true
+ sleep 0.5
+ kill -KILL -- "-$WL_PID" 2>/dev/null || true
+ fi
+ # Best-effort: stop any straggling ior processes from this tape.
+ sudo -n pkill -TERM -f '/ior$' 2>/dev/null || true
+}
+trap cleanup EXIT INT TERM
+
+# Give the workload a moment to spool up before recording.
+sleep 2
+
+cd "$ROOT"
+vhs "$TAPE"
diff --git a/docs/tutorial/scripts/sudo-keepalive.sh b/docs/tutorial/scripts/sudo-keepalive.sh
new file mode 100755
index 0000000..4ae1e27
--- /dev/null
+++ b/docs/tutorial/scripts/sudo-keepalive.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+# Refreshes the sudo timestamp every minute so a long demo regen never trips a
+# password prompt. Intended to be backgrounded by `mage demo` and killed at the
+# end of the run.
+
+set -u
+
+trap 'exit 0' TERM INT
+
+while true; do
+ sudo -n true 2>/dev/null || exit 1
+ sleep 60
+done
diff --git a/docs/tutorial/scripts/workload.sh b/docs/tutorial/scripts/workload.sh
new file mode 100755
index 0000000..ebbf58b
--- /dev/null
+++ b/docs/tutorial/scripts/workload.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+# Background workload for demo tapes. Generates a steady mix of file I/O
+# (open/read/close), big writes (fsync/dd), stat-heavy traffic, and ioworkload
+# scenarios so every TUI tab has something interesting to display.
+#
+# Designed to be killed via `kill $!` from the tape wrapper. All children are
+# placed in this script's process group so a single signal cleans them up.
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+IOWORKLOAD="${ROOT}/../ioworkload"
+SCRATCH="$(mktemp -d -t ior-demo-workload-XXXXXX)"
+trap 'rm -rf "$SCRATCH"' EXIT
+
+cleanup() {
+ trap - TERM INT
+ # Kill the entire process group so all background loops stop.
+ kill -- -$$ 2>/dev/null || true
+ exit 0
+}
+trap cleanup TERM INT
+
+# A) walk /usr and read first byte of each file: tons of openat/read/close + varied paths.
+(
+ while true; do
+ find /usr/share /usr/lib -maxdepth 4 -type f 2>/dev/null \
+ | shuf -n 800 \
+ | xargs -r -I{} sh -c 'head -c 1 "{}" >/dev/null 2>&1' || true
+ sleep 1
+ done
+) &
+
+# B) periodic large write with fsync via dd: fills the latency tab with slow writes.
+(
+ while true; do
+ dd if=/dev/zero of="${SCRATCH}/big.bin" bs=1M count=20 conv=fsync status=none 2>/dev/null || true
+ sleep 3
+ rm -f "${SCRATCH}/big.bin"
+ done
+) &
+
+# C) stat-heavy directory crawl: feeds the syscall tab with newfstatat/getdents.
+(
+ while true; do
+ find /etc /var/log -maxdepth 3 >/dev/null 2>&1 || true
+ sleep 2
+ done
+) &
+
+# D) ioworkload scenario rotation if the binary exists: gives us syscall variety
+# beyond what the shell utilities trigger (mmap, dup, fcntl, sync, rename, link).
+if [ -x "$IOWORKLOAD" ]; then
+ (
+ scenarios=(
+ open-basic
+ readwrite-basic
+ stat-basic
+ stat-statx
+ mmap-basic
+ sync-basic
+ dup-basic
+ fcntl-dupfd
+ rename-basic
+ link-basic
+ dir-basic
+ )
+ while true; do
+ for s in "${scenarios[@]}"; do
+ "$IOWORKLOAD" --scenario="$s" >/dev/null 2>&1 || true
+ done
+ done
+ ) &
+fi
+
+# Idle until killed.
+wait
diff --git a/docs/tutorial/tapes/01-launch.tape b/docs/tutorial/tapes/01-launch.tape
new file mode 100644
index 0000000..1e2b558
--- /dev/null
+++ b/docs/tutorial/tapes/01-launch.tape
@@ -0,0 +1,23 @@
+# 01-launch: cold start of ior. Shows the PID picker, then the default landing tab
+# (Flamegraph) once Enter selects "All PIDs".
+Output demo/assets/01-launch.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 4500ms
+Screenshot demo/assets/screenshot-pidpicker.png
+Sleep 1500ms
+Enter
+Sleep 5s
+Screenshot demo/assets/screenshot-dashboard.png
+Sleep 2s
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/02-overview-tab.tape b/docs/tutorial/tapes/02-overview-tab.tape
new file mode 100644
index 0000000..a16a0b8
--- /dev/null
+++ b/docs/tutorial/tapes/02-overview-tab.tape
@@ -0,0 +1,23 @@
+# 02-overview-tab: dashboard Overview tab — sparkline, top syscalls, top paths fill in.
+# Key 2 switches to Overview (key 1 is Flamegraph, the default landing tab).
+Output demo/assets/02-overview-tab.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "2"
+Sleep 6s
+Screenshot demo/assets/screenshot-overview.png
+Sleep 1s
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/03-syscalls-tab.tape b/docs/tutorial/tapes/03-syscalls-tab.tape
new file mode 100644
index 0000000..376f632
--- /dev/null
+++ b/docs/tutorial/tapes/03-syscalls-tab.tape
@@ -0,0 +1,36 @@
+# 03-syscalls-tab: sortable syscalls table, j/k to scroll, s/S to sort/reverse-sort.
+# Key 3 switches to Syscalls.
+Output demo/assets/03-syscalls-tab.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "3"
+Sleep 3s
+Type "j"
+Sleep 400ms
+Type "j"
+Sleep 400ms
+Type "j"
+Sleep 800ms
+Type "k"
+Sleep 600ms
+Right
+Sleep 600ms
+Type "s"
+Sleep 1500ms
+Type "S"
+Sleep 2s
+Screenshot demo/assets/screenshot-syscalls.png
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/04-files-tab.tape b/docs/tutorial/tapes/04-files-tab.tape
new file mode 100644
index 0000000..49adfee
--- /dev/null
+++ b/docs/tutorial/tapes/04-files-tab.tape
@@ -0,0 +1,32 @@
+# 04-files-tab: files tab plus directory-grouped view toggle (`d`).
+# Key 4 switches to Files.
+Output demo/assets/04-files-tab.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "4"
+Sleep 3s
+Type "j"
+Sleep 400ms
+Type "j"
+Sleep 400ms
+Type "j"
+Sleep 800ms
+Type "d"
+Sleep 3s
+Screenshot demo/assets/screenshot-files-grouped.png
+Type "d"
+Sleep 2s
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/05-processes-tab.tape b/docs/tutorial/tapes/05-processes-tab.tape
new file mode 100644
index 0000000..ad5f64e
--- /dev/null
+++ b/docs/tutorial/tapes/05-processes-tab.tape
@@ -0,0 +1,30 @@
+# 05-processes-tab: processes tab, scroll, and reverse-sort.
+# Key 5 switches to Processes.
+Output demo/assets/05-processes-tab.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "5"
+Sleep 3s
+Type "j"
+Sleep 400ms
+Type "j"
+Sleep 400ms
+Type "j"
+Sleep 800ms
+Type "S"
+Sleep 2s
+Screenshot demo/assets/screenshot-processes.png
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/06-latency-gaps-tab.tape b/docs/tutorial/tapes/06-latency-gaps-tab.tape
new file mode 100644
index 0000000..b19ae58
--- /dev/null
+++ b/docs/tutorial/tapes/06-latency-gaps-tab.tape
@@ -0,0 +1,22 @@
+# 06-latency-gaps-tab: latency + inter-syscall gap histograms (driven by dd writes).
+# Key 6 switches to Latency.
+Output demo/assets/06-latency-gaps-tab.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "6"
+Sleep 8s
+Screenshot demo/assets/screenshot-latency.png
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/07-stream-live.tape b/docs/tutorial/tapes/07-stream-live.tape
new file mode 100644
index 0000000..9d7ad4f
--- /dev/null
+++ b/docs/tutorial/tapes/07-stream-live.tape
@@ -0,0 +1,22 @@
+# 07-stream-live: stream tab live-tailing rows.
+# Key 7 switches to Stream.
+Output demo/assets/07-stream-live.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "7"
+Sleep 6s
+Screenshot demo/assets/screenshot-stream-live.png
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/08-stream-pause-filter.tape b/docs/tutorial/tapes/08-stream-pause-filter.tape
new file mode 100644
index 0000000..416830c
--- /dev/null
+++ b/docs/tutorial/tapes/08-stream-pause-filter.tape
@@ -0,0 +1,46 @@
+# 08-stream-pause-filter: pause stream, navigate cells, push two filters onto the stack, undo with Esc.
+# Key 7 switches to Stream.
+Output demo/assets/08-stream-pause-filter.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "7"
+Sleep 4s
+Space
+Sleep 1s
+Down
+Sleep 300ms
+Down
+Sleep 300ms
+Down
+Sleep 800ms
+Right
+Sleep 500ms
+Right
+Sleep 500ms
+Enter
+Sleep 1500ms
+Down
+Sleep 400ms
+Right
+Sleep 400ms
+Enter
+Sleep 2s
+Screenshot demo/assets/screenshot-stream-filtered.png
+Escape
+Sleep 1s
+Escape
+Sleep 1500ms
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/09-stream-regex-search.tape b/docs/tutorial/tapes/09-stream-regex-search.tape
new file mode 100644
index 0000000..b643740
--- /dev/null
+++ b/docs/tutorial/tapes/09-stream-regex-search.tape
@@ -0,0 +1,40 @@
+# 09-stream-regex-search: regex search forward (/), n to advance, ? for backward.
+# Key 7 switches to Stream.
+Output demo/assets/09-stream-regex-search.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "7"
+Sleep 4s
+Space
+Sleep 1s
+Type "/"
+Sleep 600ms
+Type "openat"
+Sleep 700ms
+Enter
+Sleep 1500ms
+Type "n"
+Sleep 800ms
+Type "n"
+Sleep 800ms
+Screenshot demo/assets/screenshot-stream-search.png
+Type "?"
+Sleep 600ms
+Type "stat"
+Sleep 700ms
+Enter
+Sleep 1500ms
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/10-stream-csv-export.tape b/docs/tutorial/tapes/10-stream-csv-export.tape
new file mode 100644
index 0000000..c1f4935
--- /dev/null
+++ b/docs/tutorial/tapes/10-stream-csv-export.tape
@@ -0,0 +1,28 @@
+# 10-stream-csv-export: 'e' to export current filtered stream snapshot, then ls the CSV produced.
+# Key 7 switches to Stream.
+Output demo/assets/10-stream-csv-export.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "7"
+Sleep 4s
+Type "e"
+Sleep 2s
+Screenshot demo/assets/screenshot-stream-export.png
+Sleep 1s
+Type "q"
+Sleep 1s
+Type "ls -lh ior-stream-*.csv"
+Enter
+Sleep 2s
diff --git a/docs/tutorial/tapes/11-pid-tid-probe.tape b/docs/tutorial/tapes/11-pid-tid-probe.tape
new file mode 100644
index 0000000..caccc48
--- /dev/null
+++ b/docs/tutorial/tapes/11-pid-tid-probe.tape
@@ -0,0 +1,41 @@
+# 11-pid-tid-probe: re-open PID picker (p), TID picker (t), probe toggle dialog (o).
+Output demo/assets/11-pid-tid-probe.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "p"
+Sleep 2s
+Down
+Sleep 400ms
+Down
+Sleep 400ms
+Screenshot demo/assets/screenshot-pidpicker-open.png
+Escape
+Sleep 1s
+Type "t"
+Sleep 2s
+Screenshot demo/assets/screenshot-tidpicker.png
+Escape
+Sleep 1s
+Type "o"
+Sleep 2s
+Down
+Sleep 400ms
+Down
+Sleep 400ms
+Screenshot demo/assets/screenshot-probes.png
+Escape
+Sleep 1s
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/12-parquet-recording.tape b/docs/tutorial/tapes/12-parquet-recording.tape
new file mode 100644
index 0000000..c6f3399
--- /dev/null
+++ b/docs/tutorial/tapes/12-parquet-recording.tape
@@ -0,0 +1,28 @@
+# 12-parquet-recording: start parquet recording (R), accept default filename (Enter), dwell, stop (R), exit, ls the file.
+Output demo/assets/12-parquet-recording.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior"
+Enter
+Sleep 5s
+Enter
+Sleep 4s
+Type "R"
+Sleep 1500ms
+Enter
+Sleep 4s
+Screenshot demo/assets/screenshot-recording-active.png
+Type "R"
+Sleep 1500ms
+Type "q"
+Sleep 1s
+Type "ls -lh ior-recording-*.parquet"
+Enter
+Sleep 2s
diff --git a/docs/tutorial/tapes/13-tui-flamegraph.tape b/docs/tutorial/tapes/13-tui-flamegraph.tape
new file mode 100644
index 0000000..7572159
--- /dev/null
+++ b/docs/tutorial/tapes/13-tui-flamegraph.tape
@@ -0,0 +1,22 @@
+# 13-tui-flamegraph: in-TUI flamegraph view. The default landing tab (key `1`) is the
+# live flamegraph. Workload running in the background drives real frame data.
+# `-fields` and `-count` let you reshape stack ordering and the size metric.
+Output demo/assets/13-tui-flamegraph.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "sudo -n /home/paul/git/ior/ior -fields comm,path,tracepoint -count count"
+Enter
+Sleep 5s
+Enter
+Sleep 8s
+Screenshot demo/assets/screenshot-flamegraph.png
+Sleep 2s
+Type "q"
+Sleep 1s
diff --git a/docs/tutorial/tapes/14-headless-modes.tape b/docs/tutorial/tapes/14-headless-modes.tape
new file mode 100644
index 0000000..ce894f6
--- /dev/null
+++ b/docs/tutorial/tapes/14-headless-modes.tape
@@ -0,0 +1,38 @@
+# 14-headless-modes: all three headless flows in one tape — -flamegraph -name (.ior.zst),
+# -parquet (streaming rows), -plain (CSV to stdout). Each capped with -duration so the demo terminates.
+Output demo/assets/14-headless-modes.gif
+Set Shell "bash"
+Set Padding 20
+
+Hide
+Type "cd $(mktemp -d -t ior-demo-XXXX) && clear"
+Enter
+Sleep 300ms
+Show
+
+Type "# 1) Aggregated native trace artifact (.ior.zst)"
+Enter
+Sleep 800ms
+Type "sudo -n /home/paul/git/ior/ior -flamegraph -name demo-trace -duration 5"
+Enter
+Sleep 7s
+Type "ls -lh demo-trace*.ior.zst"
+Enter
+Sleep 2s
+
+Type "# 2) Per-row streaming Parquet"
+Enter
+Sleep 800ms
+Type "sudo -n /home/paul/git/ior/ior -parquet demo.parquet -duration 5"
+Enter
+Sleep 7s
+Type "ls -lh demo.parquet"
+Enter
+Sleep 2s
+
+Type "# 3) Plain CSV to stdout (head only)"
+Enter
+Sleep 800ms
+Type "sudo -n /home/paul/git/ior/ior -plain -duration 3 2>/dev/null | head -n 8"
+Enter
+Sleep 6s