summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-18 20:54:35 +0200
committerPaul Buetow <paul@buetow.org>2026-03-18 20:54:35 +0200
commitcd554b0af706b5f62b4e1bfde04091052b4aac61 (patch)
treee6d02f1c2a1da27da17386e8832c2d4a3e699cdf
parentb421b2232351049277ee4ad5b31367bb2b6779bb (diff)
cleanup
-rw-r--r--README.md38
-rw-r--r--docs/libbpfgo-upgrade-plan.md154
-rw-r--r--docs/parquet-recording-perf-baseline.md113
-rw-r--r--docs/tui-dashboard-table-sorting-plan.md336
-rw-r--r--docs/tui-flamegraph-behavior.md46
-rw-r--r--docs/tui-flamegraph-plan.md450
-rw-r--r--docs/tui-global-filter-architecture.md160
-rwxr-xr-xflamegraph.testbin12360000 -> 0 bytes
-rw-r--r--internal/c/generated_tracepoints.c46
-rw-r--r--internal/event/event.go2
-rw-r--r--internal/event/pair.go23
-rw-r--r--internal/file/file.go9
-rw-r--r--internal/flags/flags.go26
-rw-r--r--internal/flamegraph/iordata.go40
-rw-r--r--internal/flamegraph/iordata_test.go4
-rw-r--r--internal/flamegraph/livetrie.go18
-rw-r--r--internal/flamegraph/trie.go9
-rw-r--r--internal/generate/codegen.go9
-rw-r--r--internal/generate/typesgo.go2
-rw-r--r--internal/globalfilter/filter.go20
-rw-r--r--internal/parquet/schema.go16
-rw-r--r--internal/probemanager/grouping.go14
-rw-r--r--internal/probemanager/manager.go5
-rw-r--r--internal/statsengine/bench_test.go4
-rw-r--r--internal/statsengine/filerank.go11
-rw-r--r--internal/statsengine/process.go25
-rw-r--r--internal/statsengine/syscall.go25
-rw-r--r--internal/statsengine/syscall_test.go12
-rw-r--r--internal/tui/dashboard/bubbles.go19
-rw-r--r--internal/tui/dashboard/files.go10
-rw-r--r--internal/tui/dashboard/icicle.go15
-rw-r--r--internal/tui/dashboard/treemap.go27
-rw-r--r--internal/tui/flamegraph/model.go20
-rw-r--r--internal/tui/flamegraph/renderer.go19
-rw-r--r--internal/tui/pidpicker/proclist.go19
-rw-r--r--internal/types/generated_types.go18
36 files changed, 301 insertions, 1463 deletions
diff --git a/README.md b/README.md
index 07a8f8a..58d0a47 100644
--- a/README.md
+++ b/README.md
@@ -109,19 +109,36 @@ The default stack order is `comm,path,tracepoint` (bottom to top).
## Recording Modes
-`ior` has three distinct output flows. They are intentionally different:
+`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.
+- 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
@@ -155,6 +172,23 @@ Headless Parquet mode behavior:
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 |
+| --- | --- | --- |
+| 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.
diff --git a/docs/libbpfgo-upgrade-plan.md b/docs/libbpfgo-upgrade-plan.md
deleted file mode 100644
index 1969015..0000000
--- a/docs/libbpfgo-upgrade-plan.md
+++ /dev/null
@@ -1,154 +0,0 @@
-# libbpfgo Upgrade Plan
-
-## Goal
-
-Upgrade `ior` from `github.com/aquasecurity/libbpfgo v0.6.0-libbpf-1.3.0...`
-to the latest tagged upstream release `v0.9.2-libbpf-1.5.1`, and align the
-repo's Go module, local static-link toolchain checkout, build instructions, and
-runtime validation on that same tag.
-
-## Current State
-
-- `go.mod` / `go.sum` now pin
- `github.com/aquasecurity/libbpfgo v0.9.2-libbpf-1.5.1`
-- `Magefile.go` defaults to the sibling checkout at `../libbpfgo` (local path:
- `/home/paul/git/libbpfgo`) and emits rebuild guidance if static
- artifacts are missing
-- The local checkout is currently ahead of the latest tag:
- `v0.9.2-libbpf-1.5.1-23-g9a319d2`
-- `README.md`, `AGENTS.md`, and `integrationtests/README.md` now pin the tag,
- sync the `libbpf` submodule, and document the rebuild or validation workflow
-- Integration coverage now passes again after restoring the legacy
- `-flamegraph` / `-name` compatibility path used by the harness to collect
- `.ior.zst` artifacts
-
-## Upgrade Target
-
-- Upstream tag: `v0.9.2-libbpf-1.5.1`
-- Local checkout to use for static headers/archive:
- `/home/paul/git/libbpfgo`
-- Repo-relative default checkout path used by `Magefile.go`: `../libbpfgo`
-- Override path for local experiments: `LIBBPFGO=/absolute/path/to/libbpfgo`
-- Do not target `libbpfgo` `main` as part of this upgrade unless a tagged
- release blocker is found
-
-## Pinned Source of Truth
-
-- `go.mod` / `go.sum` pin `github.com/aquasecurity/libbpfgo
- v0.9.2-libbpf-1.5.1`
-- `README.md`, `AGENTS.md`, and `integrationtests/README.md` document the same
- checkout, tag, validation commands, and `make libbpfgo-static` workflow
-- `Magefile.go` fails with explicit rebuild guidance when the local
- `libbpfgo` checkout is missing the static artifacts that `ior` expects
-- `internal/ior.go` preserves the legacy `-flamegraph` / `-name` trace-output
- path required by the integration harness while leaving TUI and `-plain`
- behavior unchanged
-
-## Breaking-Change Watchpoints
-
-- `v0.8.0-libbpf-1.5` includes a `BPFProg` API alignment change
-- `v0.9.1-libbpf-1.5.1` changes `AttachUprobe` /
- `AttachURetprobe` signatures
-- `libbpf` minimum version moves from `1.3.x` to `1.5.1`
-- Static builds require `git submodule update --init --recursive` in the local
- `libbpfgo` checkout before `make libbpfgo-static`
-
-`ior` appears to use a narrow subset of APIs:
-
-- module loading (`NewModuleFromFile`, `NewModuleFromBuffer`, `BPFLoadObject`)
-- maps (`GetMap`, `SetMaxEntries`, `InitGlobalVariable`)
-- ringbuf (`InitRingBuf`)
-- program lookup and tracepoint attach (`GetProgram`, `AttachTracepoint`)
-
-The direct API-break risk is therefore expected to be low, but compile/runtime
-validation is still required.
-
-## Implementation Workstreams
-
-1. Align the version source of truth
- - Pin `go.mod` / `go.sum` to `v0.9.2-libbpf-1.5.1`
- - Align the local checkout instructions in `README.md`
- - Align `AGENTS.md` and `Magefile.go` guidance with the same tag and rebuild flow
- - Ensure the local checkout is reset to the exact tag and rebuilt
-
-2. Rebuild the local static toolchain
- - In `/home/paul/git/libbpfgo`:
- - `git checkout v0.9.2-libbpf-1.5.1`
- - `git submodule update --init --recursive`
- - `make libbpfgo-static`
-
-3. Compile and fix `ior`
- - Rebuild `ior` against the upgraded wrapper and static `libbpf`
- - Fix any compile/API regressions in:
- - `internal/ior.go`
- - `internal/bpfsetup.go`
- - `internal/bpfembed.go`
- - any `probemanager` adapter code if signatures changed
-
-4. Validate behavior
- - Run `env GOTOOLCHAIN=auto mage world`
- - Run root-required `env GOTOOLCHAIN=auto mage integrationTest`
- - Specifically verify:
- - embedded `ior.bpf.o` loading still works
- - tracepoint attach/detach still works
- - ring buffer event ingestion still works
- - static build/link flags still work with the rebuilt local checkout
-
-5. Finalize docs and rollback guidance
- - Document the exact `libbpfgo` tag and rebuild commands
- - Mention the local checkout path used by `Magefile.go`
- - Add troubleshooting notes for submodule sync / static rebuild failures
- - Record the rollback target: `go.mod` pseudo-version
- `v0.6.0-libbpf-1.3.0.20240111220235-90dbffffbdab` plus local checkout
- commit `90dbffffbdab`
-
-## Validation Result
-
-- `env GOTOOLCHAIN=auto mage world` passed after the pinning commit
- `f28dab3`
-- `env GOTOOLCHAIN=auto mage integrationTest` passed after compatibility fix
- commit `28338f4`
-- The embedded-object path is covered by
- `env GOTOOLCHAIN=auto TEST_NAME=TestLoadBPFModuleUsesEmbeddedObjectByDefault mage testWithName`
-
-## Troubleshooting
-
-- Missing `bpf/bpf.h` or `libbpf` symbols usually means the sibling checkout is
- not at `v0.9.2-libbpf-1.5.1` or was not rebuilt after a `git checkout`.
-- Raw `go test` can still fail for packages that import `libbpfgo` because it
- does not inherit the `CGO_CFLAGS`, `CGO_LDFLAGS`, and `LIBBPFGO` values that
- `Magefile.go` sets up. Use Mage targets for validated flows.
-- If integration tests fail immediately with unknown `-flamegraph` /
- `-name` flags, rebuild `ior` from a checkout that includes commit `28338f4`.
-
-## Rollback
-
-If the tagged release proves insufficient, revert the `ior` side to
-`github.com/aquasecurity/libbpfgo
-v0.6.0-libbpf-1.3.0.20240111220235-90dbffffbdab`, reset the sibling checkout,
-and rebuild:
-
-```bash
-git -C /home/paul/git/libbpfgo checkout 90dbffffbdab
-git -C /home/paul/git/libbpfgo submodule update --init --recursive
-make -C /home/paul/git/libbpfgo libbpfgo-static
-```
-
-## Validation Commands
-
-- `GOTOOLCHAIN=auto mage test`
-- `GOTOOLCHAIN=auto mage world`
-- `GOTOOLCHAIN=auto mage integrationTest`
-
-## References
-
-- Repo files:
- - `go.mod`
- - `README.md`
- - `AGENTS.md`
- - `Magefile.go`
- - `internal/ior.go`
- - `internal/bpfsetup.go`
- - `internal/bpfembed.go`
-- Local toolchain checkout:
- - `/home/paul/git/libbpfgo`
diff --git a/docs/parquet-recording-perf-baseline.md b/docs/parquet-recording-perf-baseline.md
deleted file mode 100644
index e1731a7..0000000
--- a/docs/parquet-recording-perf-baseline.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Parquet Recording Performance Baseline
-
-Captured on 2026-03-13 from the benchmark task using the current Parquet recording implementation.
-
-## Reproduction
-
-Run the pipeline benchmark profiler:
-
-```bash
-env GOTOOLCHAIN=auto mage benchProf
-```
-
-This writes timestamped pipeline profiles under `bench-profiles/`. The baseline captured for this run was:
-
-- `bench-profiles/pipeline-20260313-054719-cpu.prof`
-- `bench-profiles/pipeline-20260313-054719-mem.prof`
-- `bench-profiles/pipeline-20260313-054719-block.prof`
-
-Useful follow-up commands:
-
-```bash
-env GOTOOLCHAIN=auto go tool pprof -top bench-profiles/pipeline-20260313-054719-cpu.prof
-env GOTOOLCHAIN=auto go tool pprof -top -sample_index=alloc_space bench-profiles/pipeline-20260313-054719-mem.prof
-env GOTOOLCHAIN=auto go tool pprof -top -sample_index=inuse_space bench-profiles/pipeline-20260313-054719-mem.prof
-env GOTOOLCHAIN=auto go tool pprof -top bench-profiles/pipeline-20260313-054719-block.prof
-```
-
-## Baseline Numbers
-
-`mage benchProf` recorded the parquet-specific pipeline benchmarks at:
-
-- `BenchmarkPipelineHeadlessParquetCapture`: `14.20 ms/op`, `2000 pairs/op`, `347159 B/op`, `7212 allocs/op`
-- `BenchmarkPipelineTUIParquetRecording`: `19.13 ms/op`, `2000 pairs/op`, `994016 B/op`, `19873 allocs/op`
-
-Interpretation:
-
-- The TUI recording path is about 35% slower than the headless parquet path for the same synthetic stream.
-- The TUI recording path allocates about 2.9x more memory per operation because it also exercises the stats engine, ring buffer, live trie, and stream fanout path.
-
-## CPU Findings
-
-Top CPU samples were still dominated by the core event-loop path rather than parquet serialization itself:
-
-- `(*eventLoop).processRawEvent` and `(*eventLoop).tracepointExited` were the heaviest cumulative runtime buckets.
-- `file.NewFdWithPid` and `os.Readlink` remained a large cumulative cost in exit handling and fd/path materialization.
-- Channel scheduling (`runtime.chansend`, `runtime.chanrecv`, `runtime.selectgo`) stayed visible, especially in the TUI fanout path.
-- Parquet-specific work was present but secondary: `parquet.(*Recorder).runSession`, `parquet.(*Writer).Close`, parquet-go column flushing, and Zstd compression showed up as meaningful but not dominant contributors.
-
-## Allocation Findings
-
-Allocation-space profile highlights:
-
-- `benchmarkPipelineMix` still accounted for the single largest allocation bucket because it rebuilds the synthetic raw-event stream for each benchmark run.
-- `os.Readlink`, `file.(*FdFile).Dup`, and `file.NewFdWithPid` remained major allocators in the traced event path.
-- TUI-only structures added measurable cost:
- - `tui/eventstream.NewRingBuffer`
- - `parquet.newRecordingSession`
- - `benchmarkPipelineTUIParquet`
-- Parquet writer lifecycle allocations were visible but bounded:
- - parquet-go column buffers
- - Zstd encoder initialization
- - recorder session queue allocation
-
-Retained in-use memory was modest and dominated by parquet-go writer buffers and Zstd encoder state during flush/close:
-
-- `parquet-go/internal/memory.newSlice`
-- parquet column buffer construction
-- Zstd encoder initialization blocks
-
-## Contention Findings
-
-The block profile did not show a recorder lock hotspot. It was dominated by channel waits:
-
-- `runtime.chanrecv2`: about 65.8% of blocked time
-- `runtime.chanrecv1`: about 31.8% of blocked time
-
-Most blocked time came from long-lived background workers waiting on channels, especially comm resolver workers. That means the current parquet path does not yet show a major mutex-contention bottleneck; the bigger costs are work done per event and the extra TUI fanout/allocation load.
-
-## Optimization Targets
-
-These are the highest-value targets for the follow-up optimization task:
-
-- Reduce fd/path resolution overhead in the event loop, especially `Readlink`-driven work in `file.NewFdWithPid`.
-- Lower TUI recording allocations by reusing stream fanout buffers and reducing ring-buffer/session setup churn.
-- Revisit recorder/session and parquet writer setup costs if recordings are started frequently in short sessions.
-- Only optimize parquet compression or flush behavior after confirming they dominate a focused headless profile; they are not currently the primary cost center.
-
-## Verified Follow-up Win
-
-After profiling, the first optimization pass removed the extra TUI `streamEvents` channel hop and pushed directly into the mutex-protected ring buffer.
-
-Re-run command:
-
-```bash
-env GOTOOLCHAIN=auto mage benchProf
-```
-
-Optimized pipeline artifacts:
-
-- `bench-profiles/pipeline-20260313-055321-cpu.prof`
-- `bench-profiles/pipeline-20260313-055321-mem.prof`
-- `bench-profiles/pipeline-20260313-055321-block.prof`
-
-Benchmark comparison for the changed path:
-
-| Benchmark | Before | After | Change |
-| --- | --- | --- | --- |
-| `BenchmarkPipelineTUIParquetRecording` | `19.13 ms/op`, `994016 B/op`, `19873 allocs/op` | `16.51 ms/op`, `992334 B/op`, `19866 allocs/op` | about `13.7%` faster with a small allocation reduction |
-
-Notes:
-
-- `BenchmarkPipelineHeadlessParquetCapture` also moved between runs, but that path was not changed; treat that difference as benchmark noise rather than a verified optimization win.
-- Post-change CPU samples still show the event loop and fd/path resolution dominating overall cost, so the next optimization pass should stay focused on those areas instead of tuning parquet compression first.
diff --git a/docs/tui-dashboard-table-sorting-plan.md b/docs/tui-dashboard-table-sorting-plan.md
deleted file mode 100644
index 0d4586e..0000000
--- a/docs/tui-dashboard-table-sorting-plan.md
+++ /dev/null
@@ -1,336 +0,0 @@
-# TUI Dashboard Table Sorting Plan
-
-## Overview
-
-Add column-driven sorting to the dashboard table views for:
-
-- `3:Syscalls`
-- `4:Files`
-- `5:Processes`
-
-This is a **table-view-only** feature. Bubble, treemap, and icicle modes keep
-their existing ordering rules.
-
-The task wording says "sort by any row", but the current dashboard already
-tracks both a selected row and a selected column. This plan therefore treats
-`s` as **sort by the currently selected column/cell**.
-
-Pressing `s`:
-
-1. on a new selected column enables that column's sort order
-2. again on the same selected column clears the custom sort and restores the
- tab's current default ordering
-
-## Current Behavior
-
-The dashboard already has the key pieces needed for this feature:
-
-- `internal/tui/dashboard/model.go`
- - stores row selection and selected column for Syscalls, Files, and Processes
- - routes table navigation with `left/right` and `h/l`
-- `internal/tui/dashboard/syscalls.go`
- - renders the syscall table from `snap.Syscalls()`
-- `internal/tui/dashboard/files.go`
- - renders both the file table and the grouped-directory table
-- `internal/tui/dashboard/processes.go`
- - renders the process table
-
-The current default ordering comes from the snapshot producers:
-
-- Syscalls: `Count desc`, then `Name asc`
-- Files: `Accesses desc`, then `Path asc`
-- Grouped directories: `Accesses desc`, then `Directory asc`
-- Processes: `Syscalls desc`, then `Bytes desc`, then `PID asc`
-
-That ordering should remain the baseline whenever no custom sort is active.
-
-## Design Goals
-
-- `s` sorts by the selected column in table mode.
-- `s` on the same selected column toggles back to the default ranking.
-- `Enter` continues to act on the row currently visible on screen after sorting.
-- Sorting stays in the dashboard layer; `statsengine` snapshot semantics do not
- change.
-- Selection remains anchored to the same logical entity when sorting changes.
-- Width changes do not corrupt sort state for the Syscalls tab.
-
-## UX Rules
-
-- `s` is active only for sortable dashboard tables:
- - Syscalls table mode
- - Files table mode
- - Files directory-grouped table mode
- - Processes table mode
-- `s` does nothing in:
- - Overview
- - Latency+Gaps
- - Stream
- - Flame
- - bubble/treemap/icicle modes
-- Table footer hints should add `s:sort`.
-- The footer should also show the active sort, for example:
- - `sort: default`
- - `sort: p95 desc`
- - `sort: Path asc`
-- Expanded help should mention `s` so the feature is discoverable.
-
-## State Model
-
-Add dashboard-local sort state per table shape.
-
-Example shape:
-
-```go
-type tableSortState[K comparable] struct {
- active bool
- key K
-}
-```
-
-Recommended fields on `dashboard.Model`:
-
-- `syscallsSort`
-- `filesSort`
-- `filesDirSort`
-- `processesSort`
-
-`Files` needs **two** sort states because the tab has two different table
-schemas:
-
-- file rows
-- grouped directory rows
-
-Those states should persist independently when `d` toggles between files and
-directories.
-
-## Logical Sort Keys
-
-Do **not** store the raw selected column index as the sort identifier.
-
-The Syscalls table changes shape by width:
-
-- narrow layout: `Syscall Count Rate/s Avg p95 p99 Bytes Errors`
-- wide layout: `Syscall Count Rate/s Avg Min Max p50 p95 p99 Bytes Errors`
-
-If sort state stored only a column index, resizing from narrow to wide would
-turn "sort by p95" into "sort by Min". The sort state must therefore use a
-stable logical key enum, and map the current visible column index to that enum
-at keypress time.
-
-Recommended enums:
-
-- `syscallSortKey`
-- `fileSortKey`
-- `fileDirSortKey`
-- `processSortKey`
-
-## Column Ordering Rules
-
-Use a fixed natural direction per logical column. This avoids inventing a
-three-state cycle and matches the task requirement of "sort" plus "toggle back".
-
-### Syscalls
-
-- `Syscall`: `Name asc`
-- `Count`: `Count desc`
-- `Rate/s`: `RatePerSec desc`
-- `Avg`: `LatencyMeanNs desc`
-- `Min`: `LatencyMinNs desc`
-- `Max`: `LatencyMaxNs desc`
-- `p50`: `LatencyP50Ns desc`
-- `p95`: `LatencyP95Ns desc`
-- `p99`: `LatencyP99Ns desc`
-- `Bytes`: `Bytes desc`
-- `Errors`: `Errors desc`
-
-### Files
-
-- `Accesses`: `Accesses desc`
-- `Read`: `BytesRead desc`
-- `Write`: `BytesWritten desc`
-- `Avg Latency`: `AvgLatencyNs desc`
-- `Max Latency`: `MaxLatencyNs desc`
-- `Path`: `Path asc`
-
-### Grouped Directories
-
-- `Accesses`: `Accesses desc`
-- `Read`: `BytesRead desc`
-- `Write`: `BytesWritten desc`
-- `Avg Latency`: `AvgLatencyNs desc`
-- `Max Latency`: `MaxLatencyNs desc`
-- `Files`: `FileCount desc`
-- `Directory`: `Dir asc`
-
-### Processes
-
-- `PID`: `PID asc`
-- `Comm`: `Comm asc`
-- `Syscalls`: `Syscalls desc`
-- `Rate/s`: `RatePerSec desc`
-- `Total Bytes`: `Bytes desc`
-- `Avg Latency`: `AvgLatencyNs desc`
-
-## Comparator Rules
-
-For deterministic output, custom comparators should fall back to the existing
-default ranking for that row type.
-
-Examples:
-
-- `p95 desc`, then syscall default order
-- `Path asc`, then file default order
-- `Comm asc`, then process default order
-
-This keeps ties stable and makes the "toggle back to default" behavior
-predictable.
-
-## Selection Anchoring
-
-Changing sort order must not leave the cursor on the same numeric row index if
-that index now points to a different entity.
-
-Before toggling sort:
-
-1. capture the currently selected logical entity key
-2. recompute the sorted rows
-3. restore the selected row to the same entity in the new order
-4. if the entity no longer exists, clamp as today
-
-Recommended identity keys:
-
-- Syscalls: `Name`
-- Files: `Path`
-- Grouped directories: `Dir`
-- Processes: `PID`
-
-This same anchor logic should run on refresh ticks while custom sorting is
-active so the selected item does not drift unpredictably as live stats change.
-
-## Implementation Shape
-
-Keep the sorting logic in `internal/tui/dashboard`, not in `internal/statsengine`.
-
-Reason:
-
-- snapshot order is part of the existing aggregate ranking behavior
-- only the table presentation needs alternate ordering
-- bubble/treemap/icicle already have their own ordering rules
-
-Recommended implementation split:
-
-- `internal/tui/common/keys.go`
- - add a `Sort` binding for `s`
- - include it in dashboard help output
-- `internal/tui/dashboard/model.go`
- - add per-table sort state
- - handle `s`
- - ignore `s` outside sortable table modes
- - preserve selection anchors when sort changes
- - make `selectedSyscallFilter`, `selectedFileFilter`, and
- `selectedProcessSnapshot` read from the same sorted rows used by rendering
-- `internal/tui/dashboard/syscalls.go`
- - add syscall sort key mapping from visible column index
- - add sorted syscall row helper
- - expose active sort label for footer hints
-- `internal/tui/dashboard/files.go`
- - add file and directory sort key helpers
- - keep file and grouped-directory comparators separate
-- `internal/tui/dashboard/processes.go`
- - add process sort key helpers and sorted row helper
-- `internal/tui/dashboard/table.go`
- - extend footer hints/status rendering as needed for the active sort label
-
-## Rendering/Data Consistency
-
-The most important implementation rule is:
-
-**the rendered rows and the row-selection actions must use the exact same sorted
-slice**
-
-Without this, the UI can show one row while `Enter` filters a different row.
-
-The safest approach is to centralize each table's sorted typed rows in helper
-functions and use those helpers in both:
-
-- render paths
-- selected-row action paths
-
-## Files Tab Details
-
-The Files tab needs one extra rule beyond Syscalls and Processes:
-
-- in plain file mode, sorting operates on `[]statsengine.FileSnapshot`
-- in directory-grouped mode, sorting operates on `[]DirSnapshot`
-
-The two modes should not share a single sort key because their columns differ.
-Switching with `d` should preserve:
-
-- last file-table custom sort
-- last directory-table custom sort
-
-## Interaction With Existing Features
-
-- `Enter`
- - still filters the currently selected visible row
-- `d`
- - only changes Files table shape; custom sort state persists per mode
-- `v`
- - custom sort state persists, but only applies when returning to table mode
-- `b`
- - unaffected; bubble/treemap ordering remains metric-driven
-- terminal resize
- - sort state persists because it stores logical keys, not raw indices
-- trace restart / filter apply
- - sort state should remain as view state
-
-## Testing Plan
-
-Add focused tests in `internal/tui/dashboard` and `internal/tui/common`.
-
-### Model behavior
-
-- `s` on Syscalls enables a column sort.
-- `s` on the same Syscalls column restores default sorting.
-- `s` on Processes does nothing in non-table modes.
-- `s` on Files uses file-mode sort state when `filesDirGrouped == false`.
-- `s` on Files uses directory-mode sort state when `filesDirGrouped == true`.
-- changing sort preserves the selected entity instead of only the row index.
-
-### Width-sensitive syscall behavior
-
-- sorting by `p95` in narrow mode survives a resize into wide mode
-- sorting by `Syscall` or `Count` maps correctly in both layouts
-
-### Selection action consistency
-
-- `selectedSyscallFilter()` uses sorted syscall rows
-- `selectedFileFilter()` uses sorted file or directory rows
-- `selectedProcessSnapshot()` uses sorted process rows in table mode
-
-### Help/footer rendering
-
-- expanded help includes `s`
-- table footer includes `s:sort`
-- active sort label is visible in the table footer
-
-### Negative cases
-
-- `s` does nothing on Overview / Stream / Flame / Latency+Gaps
-- `s` does nothing for bubble / treemap / icicle views
-
-## Recommended Delivery Order
-
-1. add key binding and sort state plumbing in `dashboard.Model`
-2. implement sorted typed-row helpers per tab
-3. switch render paths and selected-row actions to the shared helpers
-4. add footer/help output
-5. add regression tests for sort toggling, width changes, and selected-row
- action consistency
-
-## Non-Goals
-
-- no change to snapshot generation order in `statsengine`
-- no sortable Overview or Latency+Gaps tables
-- no ascending/descending toggle cycle beyond "custom sort" vs "default"
-- no behavior change for bubble/treemap/icicle ordering
diff --git a/docs/tui-flamegraph-behavior.md b/docs/tui-flamegraph-behavior.md
deleted file mode 100644
index cc9bb5d..0000000
--- a/docs/tui-flamegraph-behavior.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# TUI Flamegraph Expected Behavior
-
-This document records the expected interaction and layout behavior for the TUI
-flamegraph. It is intended as a stable reference for regressions and for tests
-under `internal/tui/flamegraph/` and `internal/tui/dashboard/`.
-
-## Interaction
-
-- `space` toggles pause. `p` does not pause the flamegraph and remains reserved
- for the global PID picker at the top-level TUI.
-- `enter` and left-click zoom into the selected or clicked frame.
-- Clicking an ancestor frame in the zoom lineage re-roots the view to that
- ancestor.
-- `u`, `backspace`, and `esc` undo one zoom step.
-- Direct clicks into a deep descendant create a single undo step back to the
- previous zoom root, not an implicit stack of every skipped ancestor.
-- While paused, navigation and zoom must continue to work against the frozen
- snapshot.
-
-## Layout
-
-- The selected frame must not render with underline or a horizontal highlight
- line across the bar.
-- The current zoom root must span the full flamegraph width.
-- The children of the current zoom root must be normalized to the full viewport
- width, even when the zoom root has self time or exclusive weight.
-- Zooming from any direction must produce the same full-width result for the
- newly selected zoom root.
-- The zoom lineage rows shown above the zoomed subtree provide context, but they
- must not steal horizontal space from the zoomed subtree.
-
-## Rendering
-
-- Rendering the dashboard view must not mutate persistent flamegraph state.
-- Redundant same-size viewport updates must be no-ops.
-- In paused mode, repeated renders must not reintroduce stale frame geometry or
- leave artifacts from a previous layout on screen.
-
-## Regression Coverage
-
-These expectations are covered by tests in:
-
-- `internal/tui/flamegraph/renderer_test.go`
-- `internal/tui/flamegraph/model_test.go`
-- `internal/tui/flamegraph/stress_test.go`
-- `internal/tui/dashboard/model_test.go`
diff --git a/docs/tui-flamegraph-plan.md b/docs/tui-flamegraph-plan.md
deleted file mode 100644
index 261f0fb..0000000
--- a/docs/tui-flamegraph-plan.md
+++ /dev/null
@@ -1,450 +0,0 @@
-# TUI Flamegraph Tab - Full Design Plan
-
-## Overview
-
-Add a **7th dashboard tab** (`7:Flame`) that renders a live, interactive flamegraph
-directly in the terminal using lipgloss for layout/styling and **Charm Harmonica**
-for smooth spring-based animations on both zoom transitions and live data refresh.
-The tab consumes data from an embedded `LiveTrie` and
-provides interactive flamegraph navigation directly in-terminal.
-
-## Architecture
-
-```
-BPF events -> eventLoop.printCb
- |
- +-> statsengine.Ingest() (existing tabs 1-5)
- +-> eventstream.Push() (existing tab 6)
- +-> LiveTrie.Ingest() (NEW: tab 7)
-```
-
-The `LiveTrie` is instantiated in the TUI startup path and published via
-`runtimeBindings`, similar to how `SnapshotSource` and `RingBuffer` are already
-wired.
-
-## New Files and Packages
-
-| File/Package | Purpose |
-|---|---|
-| `internal/tui/flamegraph/model.go` | Bubble Tea sub-model: state, Update, View |
-| `internal/tui/flamegraph/renderer.go` | Converts LiveTrie snapshot -> terminal frame layout, renders with lipgloss |
-| `internal/tui/flamegraph/animation.go` | Harmonica spring state for frame width interpolation and zoom transitions |
-| `internal/tui/flamegraph/search.go` | Search/highlight: text input bubble, match filtering, highlight styling |
-| `internal/tui/flamegraph/zoom.go` | Zoom stack management (zoom into subtree, undo zoom, reset zoom) |
-| `internal/tui/flamegraph/controls.go` | Toolbar rendering (status line, field order, keybindings help) |
-
-## Detailed Design
-
-### 1. Data Wiring
-
-**Changes to existing files:**
-
-- `internal/tui/tui.go` -- Add `SetLiveTrie(*flamegraph.LiveTrie)` to
- `TraceRuntimeBindings` interface and `runtimeBindings` struct. The trace starter
- publishes the `LiveTrie` the same way it publishes the stats engine.
-- `internal/ior.go` / trace setup -- When running in TUI mode, create a `LiveTrie`
- alongside the stats engine. In the `eventLoop.printCb`, call
- `liveTrie.Ingest(ep)` in addition to existing stats/stream ingestion. Publish via
- `bindings.SetLiveTrie(lt)`.
-- `internal/tui/dashboard/model.go` -- Add `liveTrie *flamegraph.LiveTrie` field,
- `flamegraphModel flamegraphtui.Model` child model. Wire refresh tick to poll
- `LiveTrie.Version()`.
-
-### 2. Tab Integration
-
-**Changes to `internal/tui/dashboard/tabs.go`:**
-
-```go
-const (
- // ... existing tabs ...
- TabFlame // new 7th tab
-)
-
-var allTabs = []Tab{..., TabFlame}
-```
-
-Tab label: `"Flame"` (short: `"Flm"`). Key binding: `7`.
-
-**Changes to `internal/tui/dashboard/model.go`:**
-
-- Add `flamegraphModel` field of type `flamegraphtui.Model`
-- In `Update()`, on `refreshTickMsg` or a dedicated `flameTickMsg` (200ms like
- stream), poll `LiveTrie.Version()` and push snapshot data into the flamegraph
- model
-- In `handleKey()`, add `key.Matches(msg, m.keys.Seven)` -> `TabFlame`
-- In `renderActiveTab()`, delegate to `flamegraphModel.View(width, height)`
-- On `WindowSizeMsg`, propagate dimensions to `flamegraphModel.SetViewport(w, h)`
- -- this triggers re-layout of all frames to fit new terminal size
-
-### 3. Flamegraph TUI Model (`internal/tui/flamegraph/model.go`)
-
-```go
-type Model struct {
- // Data
- liveTrie *flamegraph.LiveTrie
- lastVersion uint64
- snapshot *flamegraph.trieSnapshot // latest parsed snapshot
-
- // Layout
- frames []tuiFrame // current rendered frames
- targetFrames []tuiFrame // target frames (for animation lerp)
- width, height int
-
- // Interaction
- selectedIdx int // cursor/selected frame index
- zoomStack []zoomState // zoom history for undo
- zoomRoot *flamegraph.trieSnapshot // current zoom root (nil = full view)
-
- // Search
- searchActive bool
- searchInput textinput.Model // from bubbles/textinput
- searchQuery string
- matchIndices map[int]bool // frame indices matching search
-
- // Field ordering
- fieldPresets [][]string
- fieldIndex int
-
- // Animation
- springs []frameSpring // per-frame Harmonica spring state
- animTicker bool // whether animation tick is running
-
- // Flags
- paused bool
- isDark bool
-}
-
-type tuiFrame struct {
- Name string
- Col int // column position (0-based, in terminal cells)
- Row int // row from bottom
- Width int // width in terminal cells
- Total uint64
- Percent float64
- Fill lipgloss.Color
- Depth int
- Path string // full path for zoom identification
-}
-```
-
-### 4. Rendering Strategy (`internal/tui/flamegraph/renderer.go`)
-
-Terminal flamegraphs use a **cell-based layout** rather than pixel coordinates:
-
-1. **BuildTerminalLayout(snapshot, width, height, zoomRoot)** converts trie snapshot
- to `[]tuiFrame`:
- - Width is terminal columns (not 1200px). Each frame width =
- `floor(termWidth * (node.total / rootTotal))`.
- - Height is terminal rows. Each frame is exactly **1 row tall** (not 16px).
- - Rows grow bottom-to-top: root at the bottom, leaves at the top (classic
- flamegraph orientation). If tree depth exceeds available rows, only show the
- deepest `height-2` levels (toolbar + status take 2 rows).
- - Frames narrower than 1 cell are culled (terminal equivalent of `minWidthPx`).
-
-2. **Frame rendering**: Each frame is a **colored block** of text:
- - Use lipgloss background color fill with the existing `frameColor()` warm palette
- - Frame text = truncated function/path name that fits within the frame width
- - Selected frame gets a distinct border/highlight style (e.g., bold + inverted)
- - Search-matched frames get a different highlight color (e.g., red background)
-
-3. **Compositing**: Use `lipgloss.Place()` or the new lipgloss v2 compositor/canvas
- to layer frames at their (col, row) positions. Each row of the flamegraph is
- assembled by joining frame cells horizontally with background fill for gaps.
-
-4. **Auto-resize**: On `WindowSizeMsg`, re-run `BuildTerminalLayout` with new
- dimensions. All frame widths and row counts recalculate. Harmonica springs
- animate from old positions/widths to new ones.
-
-### 5. Subtree Highlighting
-
-When the user selects (navigates to) a frame, the **entire subtree rooted at that
-frame** is visually highlighted so the user can see exactly what would be zoomed
-into on `enter`.
-
-**Visual states for any frame:**
-
-| State | Visual Treatment |
-|---|---|
-| **Selected frame** | Bold text + bright border/underline + slightly lightened background |
-| **Selected subtree** (ancestors + descendants) | Full saturation, normal brightness -- "active" look |
-| **Not in subtree** | **Dimmed**: reduced saturation / lower contrast background, muted text |
-| **Search match** | Red/magenta background overlay (overrides dim but not selection) |
-
-Dimming the *non-subtree* frames makes the selected subtree "pop" naturally.
-
-**Ancestor vs Descendant Distinction:**
-
-| Relationship | Visual |
-|---|---|
-| **Selected frame** | Bold, inverted/bright border |
-| **Descendants** | Full color, normal weight |
-| **Ancestors** | Full color with subtle left-border indicator (breadcrumb trail) |
-| **Unrelated** | Dimmed (lower contrast background, gray text) |
-
-**Subtree membership** computed via `Path` field (the `\x1f`-delimited ancestor
-chain). A frame is in the subtree if:
-- Its path is a **prefix** of the selected frame's path (ancestor), OR
-- The selected frame's path is a **prefix** of its path (descendant), OR
-- It **is** the selected frame
-
-O(n) scan over frames, recomputed each time selection moves.
-
-**Interaction with search**: Search matches outside subtree shown dimmed in match
-color; inside subtree shown bright. Selected frame matching search uses selection
-style.
-
-### 6. Animation with Harmonica (`internal/tui/flamegraph/animation.go`)
-
-```go
-type frameSpring struct {
- widthSpring harmonica.Spring
- colSpring harmonica.Spring
- currentW float64
- currentCol float64
- velocityW float64
- velocityCol float64
-}
-```
-
-**Two animation scenarios:**
-
-1. **Data refresh**: When `LiveTrie` version changes and new frame widths differ
- from current, set new target widths. On each animation tick (~30fps =
- `tea.Tick(33ms)`), call `spring.Update(current, velocity, target)` for each
- frame's width and column. Render at interpolated values. Stop animation tick
- when all frames reach target within epsilon.
-
-2. **Zoom transition**: When user zooms into a subtree, the target layout changes
- (zoomed subtree expands to fill full width). Springs animate column positions
- and widths from pre-zoom to post-zoom. Undo-zoom reverses this.
-
-**Spring configuration**: `harmonica.NewSpring(harmonica.FPS(30), 6.0, 1.0)` --
-critically damped for snappy transitions without oscillation.
-
-### 7. Keybindings
-
-| Key(s) | Action |
-|---|---|
-| `j` / `down` / `arrow-down` | Move selection to frame below (shallower depth) |
-| `k` / `up` / `arrow-up` | Move selection to frame above (deeper / toward leaves) |
-| `h` / `left` / `arrow-left` | Move selection to previous sibling at same depth |
-| `l` / `right` / `arrow-right` | Move selection to next sibling at same depth |
-| `enter` | Zoom into selected frame's subtree |
-| `backspace` / `u` | Undo zoom (pop zoom stack) |
-| `escape` (when zoomed) | Reset zoom to root |
-| `/` | Open search input |
-| `escape` (when searching) | Close search, clear highlights |
-| `n` | Jump to next search match |
-| `N` (shift+n) | Jump to previous search match |
-| `p` | Toggle pause |
-| `r` | Reset baseline |
-| `o` | Cycle field order preset |
-| `?` | Toggle flame-specific help overlay |
-
-Both vim-style (j/k/h/l) and regular cursor keys (arrow keys) are bound to the
-same actions via `key.NewBinding(key.WithKeys("j", "down"))`.
-
-### 8. Search (`internal/tui/flamegraph/search.go`)
-
-- Uses `bubbles/textinput` for inline search input at the bottom of flame view
-- On submit, iterate frames and mark matching indices (case-insensitive substring
- match on frame name)
-- Matching frames rendered with highlight color; non-matching frames dimmed
-- `n`/`N` moves selection to next/previous match
-- Show match count in status line (e.g. `3/12 matches`)
-
-### 8.1 Color Coding (Implemented)
-
-Flame frames now use semantic colors first, with hash-based fallback for unknown labels:
-
-| Category | Match rule | Color (RGBA / hex) |
-|---|---|---|
-| Read I/O | name contains `read`/`pread` | `78,132,201` (`#4E84C9`) |
-| Write I/O | name contains `write`/`pwrite` | `222,122,58` (`#DE7A3A`) |
-| Metadata I/O | name contains `open`, `close`, `stat`, `rename`, `link` | `196,168,72` (`#C4A848`) |
-| Path-oriented nodes | starts with `/`, contains `/`, or `path:` | `88,156,84` (`#589C54`) |
-| Process/thread labels | contains `pid` or `tid` | `67,151,149` (`#439795`) |
-| Other syscall buckets | starts with `sys_` | `191,99,74` (`#BF634A`) |
-| Fallback | anything else | deterministic hash palette |
-
-This keeps common I/O classes visually stable across refreshes while preserving
-distinct colors for uncategorized frames.
-
-### 9. Zoom (`internal/tui/flamegraph/zoom.go`)
-
-- `zoomStack []zoomState` where `zoomState` holds the `path` string of the zoomed
- node and the previous `selectedIdx`
-- On zoom-in: push current state, find subtree node matching selected frame's path,
- set as `zoomRoot`, rebuild layout with subtree as root
-- On undo: pop stack, restore previous root
-- On reset: clear stack, set `zoomRoot = nil`
-
-### 10. Field Order Cycling
-
-Preset cycle:
-```go
-fieldPresets = [][]string{
- {"comm", "path", "tracepoint"},
- {"path", "tracepoint", "comm"},
- {"tracepoint", "comm", "path"},
- {"pid", "path", "tracepoint"},
-}
-```
-
-Pressing `o` calls `LiveTrie.Reconfigure(nextPreset)` which resets the trie and
-starts fresh accumulation.
-
-### 11. Toolbar / Status Line (`internal/tui/flamegraph/controls.go`)
-
-Rendered as a single line above the flamegraph area:
-
-```
-[LIVE] | o:order(comm>path>tp) | /:search | enter:zoom | u:undo | r:reset | p:pause
-```
-
-When paused: `[PAUSED]` in red. When searching: shows search input and match count.
-
-Selected frame info line at the bottom:
-```
-sys_read (1,234 calls, 45.2%) - /usr/bin/myapp > /dev/sda > sys_enter_read
-```
-
-### 12. Dependencies to Add
-
-- `github.com/charmbracelet/harmonica` -- spring animation
-- `charm.land/bubbles/v2/textinput` -- search input (already transitive via bubbles v2)
-
-### 13. Changes to Existing Files (Summary)
-
-| File | Change |
-|---|---|
-| `internal/tui/dashboard/tabs.go` | Add `TabFlame`, update `allTabs`, `String()`, `tabLabel()` |
-| `internal/tui/dashboard/model.go` | Add `flamegraphModel` field, wire refresh, handle key `7`, render in `renderActiveTab()` |
-| `internal/tui/tui.go` | Add `SetLiveTrie()` to bindings interface, propagate to dashboard |
-| `internal/tui/common/keys.go` | Add `Seven` key binding for tab 7 |
-| `internal/ior.go` | Create `LiveTrie` in TUI mode, wire into eventLoop callback, publish via bindings |
-| `internal/flags/flags.go` | Add `-fields` default propagation to TUI mode |
-| `go.mod` | Add `github.com/charmbracelet/harmonica` dependency |
-
-### 14. Risks and Mitigations
-
-1. **Performance at high event rates**: The `LiveTrie.Ingest()` call adds overhead
- to the hot path. Mitigation: TUI render is decoupled via version polling.
-
-2. **Terminal width too narrow**: Flamegraphs with many shallow frames may not
- render meaningfully in 80-column terminals. Mitigation: cull frames below 1 cell,
- show "terminal too narrow" message below ~60 columns.
-
-3. **Animation frame budget**: 30fps animation ticks in a terminal could cause
- flicker on slow terminals. Mitigation: only run animation tick when springs are
- active, stop when settled.
-
-4. **Color support**: Not all terminals support 24-bit color. Mitigation: lipgloss
- v2 auto-downgrades. The warm flamegraph palette degrades gracefully to 256-color.
-
----
-
-## Benchmarking & Profiling Plan
-
-### Goals
-
-1. Quantify render performance at various terminal sizes and trie depths
-2. Measure animation overhead of Harmonica spring ticks at 30fps with N springs
-3. Detect regressions via baseline benchmarks running in CI alongside `mage bench`
-4. Profile hot paths to identify allocations and CPU bottlenecks
-
-### Benchmark Suite
-
-New file: `internal/tui/flamegraph/bench_test.go`
-
-| Benchmark | What it measures |
-|---|---|
-| `BenchmarkBuildTerminalLayout` | trieSnapshot -> []tuiFrame at widths 80/120/200/300 and depths 10/50/100 |
-| `BenchmarkRenderFrame` | Full View() render at 80x24, 120x40, 200x60 |
-| `BenchmarkComputeSubtreeSet` | Subtree membership with 100/1000/5000 frames |
-| `BenchmarkSearchHighlight` | Search match computation across N frames |
-| `BenchmarkSpringUpdate` | harmonica spring.Update() across 100/500/2000 springs |
-| `BenchmarkAnimationTick` | Full tick: update springs + rebuild render output |
-| `BenchmarkZoomTransition` | Layout rebuild on zoom-in |
-| `BenchmarkLiveTrieIngestAndSnapshot` | End-to-end: ingest N events + snapshot + layout |
-| `BenchmarkResizeRelayout` | Layout rebuild at new terminal dimensions |
-
-### Benchmark Fixtures
-
-Synthetic trie generators in `internal/tui/flamegraph/testdata_test.go`:
-
-| Label | Depth | Breadth | Approximate frame count |
-|---|---|---|---|
-| `small` | 5 | 3 | ~120 |
-| `medium` | 10 | 5 | ~2,500 |
-| `large` | 15 | 8 | ~10,000+ |
-| `deep` | 50 | 2 | ~100 (narrow but deep) |
-| `wide` | 3 | 50 | ~5,000 (shallow but very wide) |
-
-### Performance Targets
-
-| Operation | Target | Rationale |
-|---|---|---|
-| `BuildTerminalLayout` (medium, 120-col) | < 500us | Well within one tick interval |
-| `View()` full render (medium, 120x40) | < 2ms | 30fps = 33ms budget |
-| `ComputeSubtreeSet` (1000 frames) | < 100us | Runs on every selection move |
-| Single animation tick (500 springs) | < 1ms | 16ms frame budget headroom |
-| `LiveTrie.Ingest` + `SnapshotJSON` | < 200us | Hot path performance |
-
-### Profiling Integration
-
-#### Built-in profiling flag
-
-When `-pprof` is set in TUI mode:
-- Write `ior-tui-cpu.prof` during session
-- Write `ior-tui-mem.prof` on quit
-- Write `ior-tui-trace.out` for first 10 seconds
-
-#### Mage Targets
-
-| Target | Command |
-|---|---|
-| `mage benchFlame` | `go test ./internal/tui/flamegraph/ -bench=. -benchmem -count=5` |
-| `mage benchFlameProf` | Same + `-cpuprofile` + `-memprofile` |
-| `mage benchFlameCmp` | Compare against saved baseline via `benchstat` |
-
-### Allocation Targets
-
-| Hot path | Strategy |
-|---|---|
-| `BuildTerminalLayout` | Pre-allocate []tuiFrame, reuse across refreshes |
-| `View()` render | strings.Builder with pre-estimated capacity, cache styles |
-| `computeSubtreeSet` | Reuse map[int]bool (clear + repopulate) |
-| Spring updates | Fixed-size []frameSpring, no per-tick allocs |
-
-Target: **zero allocs** in animation tick, **< 5 allocs/op** in full render.
-
-### Stress Tests
-
-New file: `internal/tui/flamegraph/stress_test.go`
-
-- **TestStressHighEventRate**: 100k events from 10 goroutines + concurrent render
-- **TestStressRapidResize**: 100 WindowSizeMsg with random dimensions
-- **TestStressZoomDuringRefresh**: Interleaved zoom/undo with data refresh ticks
-
-All run with `-race`.
-
-### Profiling Workflow (Manual)
-
-```bash
-# Run TUI with profiling
-sudo ior -pprof -pid 1234
-
-# Analyze CPU profile
-go tool pprof -http=:8080 ior-tui-cpu.prof
-
-# Analyze allocations
-go tool pprof -http=:8080 -alloc_space ior-tui-mem.prof
-
-# Analyze execution trace
-go tool trace ior-tui-trace.out
-
-# Benchmark-specific profiling
-mage benchFlameProf
-go tool pprof -http=:8080 flame-cpu.prof
-```
diff --git a/docs/tui-global-filter-architecture.md b/docs/tui-global-filter-architecture.md
deleted file mode 100644
index 386ef75..0000000
--- a/docs/tui-global-filter-architecture.md
+++ /dev/null
@@ -1,160 +0,0 @@
-# TUI Global Filter Architecture
-
-## Overview
-
-Add one global filter flow for the TUI that is accessible from any dashboard
-screen/tab and applies consistently across:
-
-- Flame
-- Overview
-- Syscalls
-- Files
-- Processes
-- Latency+Gaps
-- Stream
-
-The filter UI should reuse the current stream filter concepts, but the filter
-state must move to the top-level TUI model so there is a single source of truth.
-
-## Goals
-
-- One shared filter modal opened from anywhere in the dashboard.
-- One shared filter state owned by the top-level TUI model.
-- Aggregate dashboards must only reflect matching live events.
-- The stream tab must preserve its existing ring buffer across filter changes.
-- Existing stream rows must be re-filtered locally after a filter change.
-- String filters must remain substring-based. File path matching is explicitly a
- partial substring match, not exact-only.
-
-## Supported Filter Fields
-
-The global filter supports the fields currently exposed by the stream filter
-workflow, plus the existing runtime PID/TID controls:
-
-- `syscall`
-- `comm`
-- `file/path`
-- `pid`
-- `tid`
-- `fd`
-- `latency`
-- `gap`
-- `bytes`
-- `retval`
-- `errors only`
-
-## Matching Semantics
-
-- String fields use case-insensitive substring matching.
-- `file/path` uses the same case-insensitive substring matching as the other
- string fields.
-- Numeric fields use the existing comparison operators (`=`, `!=`, `>`, `>=`,
- `<`, `<=`).
-- `errors only` keeps only events with negative return values / error-marked
- events.
-
-## Architecture
-
-```
-BPF events -> eventLoop / print callback
- |
- +-> global runtime matcher
- |
- +-> statsengine.Ingest() (filtered live aggregates)
- +-> liveTrie.Ingest() (filtered flamegraph)
- +-> eventstream.Push() (filtered new stream rows)
-
-TUI state
- top-level model
- |
- +-> owns shared global filter state
- +-> owns global filter modal lifecycle
- +-> restarts tracing when filter changes
- +-> preserves current screen/tab
- +-> asks Stream to re-filter buffered rows in place
-```
-
-## Runtime Behavior
-
-Applying a new global filter does all of the following:
-
-1. Preserve the current screen/tab.
-2. Stop the active trace runtime.
-3. Reset aggregate dashboard state and flamegraph baseline.
-4. Restart tracing with the new global filter.
-5. Keep the stream ring buffer contents intact.
-6. Re-filter existing buffered stream rows locally so the stream updates
- immediately.
-
-This means aggregate tabs only show post-change matching data, while Stream can
-still show matching historical rows from before the restart.
-
-## Ownership and Data Flow
-
-### Top-level TUI model
-
-The top-level TUI model owns:
-
-- active global filter state
-- global filter modal visibility
-- filter apply/cancel/clear behavior
-- trace restart lifecycle
-- publication of filter state to child models that need local re-filtering
-
-### Stream model
-
-The stream model no longer owns the primary filter system. It must:
-
-- accept the shared global filter
-- re-filter its retained `allEvents` slice on demand
-- preserve the ring buffer across filter changes
-- keep regex search as a separate feature
-- drop the stream-local add/undo filter stack
-
-### Runtime / trace startup
-
-The TUI trace context currently carries only PID/TID. It must be expanded to
-carry the full global filter payload. The trace startup path then uses that
-payload to construct a runtime matcher before forwarding events into:
-
-- stats engine
-- flamegraph live trie
-- new stream events
-
-## Key Implementation Areas
-
-- `internal/tui/tui.go`
- - own shared filter state
- - open modal globally
- - restart trace on apply
- - preserve current screen/tab
-- `internal/tui/dashboard/model.go`
- - route global shortcut access cleanly across tabs
- - expose active filter summary in dashboard rendering/help
-- `internal/tui/eventstream/*`
- - refactor modal for reuse
- - keep stream history
- - re-filter buffered rows in place
- - remove stream-local filter stack behavior
-- `internal/ior.go`
- - plumb full filter payload through trace startup
- - apply runtime matcher before aggregate/flame/live stream ingestion
-
-## UX Rules
-
-- `f` opens the global filter modal from any dashboard tab.
-- `Enter` in the modal applies the filter.
-- `Esc` closes the modal without applying.
-- clear action resets to the unfiltered state.
-- active filter summary is visible in dashboard status/help areas.
-- stream regex search (`/`, `?`, `n`, `N`) remains separate from filtering.
-
-## Testing Requirements
-
-- context round-trip of the full global filter payload
-- runtime matcher coverage for all supported fields
-- stream ring buffer retention across filter changes
-- local re-filtering of buffered stream rows
-- file path substring matching coverage
-- aggregate dashboards only reflecting matching live events after restart
-- help/status rendering updates for the shared filter workflow
diff --git a/flamegraph.test b/flamegraph.test
deleted file mode 100755
index 2c68b6c..0000000
--- a/flamegraph.test
+++ /dev/null
Binary files differ
diff --git a/internal/c/generated_tracepoints.c b/internal/c/generated_tracepoints.c
index 06f8c39..b4d4e0f 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_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_accept sys_exit_accept 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_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_clone sys_exit_clone 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_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_create sys_exit_epoll_create 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_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_pwait sys_exit_epoll_pwait 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_eventfd sys_exit_eventfd 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_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_execve sys_exit_execve 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,14 +42,13 @@
/// 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
@@ -60,6 +59,7 @@
/// 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_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_init sys_exit_inotify_init 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
@@ -97,11 +97,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_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_mknod sys_exit_mknod 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
@@ -119,8 +119,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_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_munlock sys_exit_munlock 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
@@ -129,8 +129,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_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_pipe sys_exit_pipe 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
@@ -162,11 +162,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
@@ -184,10 +184,6 @@
/// 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
@@ -195,6 +191,8 @@
/// 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
@@ -203,8 +201,10 @@
/// 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
@@ -213,8 +213,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_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_signalfd sys_exit_signalfd 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
@@ -228,12 +228,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_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_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_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/internal/event/event.go b/internal/event/event.go
index 48bde48..c846e04 100644
--- a/internal/event/event.go
+++ b/internal/event/event.go
@@ -7,7 +7,7 @@ import (
)
var poolOfEventPairs = sync.Pool{
- New: func() interface{} { return &Pair{} },
+ New: func() any { return &Pair{} },
}
// Event is the common contract implemented by decoded syscall trace events.
diff --git a/internal/event/pair.go b/internal/event/pair.go
index 3eb8a16..4d3f342 100644
--- a/internal/event/pair.go
+++ b/internal/event/pair.go
@@ -25,20 +25,13 @@ type Pair struct {
Comm string
Duration uint64
DurationToPrev uint64
- Bytes uint64 // Number of bytes transferred (read/write/transfer syscalls only)
- Equals bool
+ Bytes uint64 // Number of bytes transferred (read/write/transfer syscalls only)
}
func NewPair(enterEv Event) *Pair {
e := poolOfEventPairs.Get().(*Pair)
- e.EnterEv = enterEv
- e.ExitEv = nil
- e.File = nil
- e.Comm = ""
- e.Duration = 0
- e.DurationToPrev = 0
- e.Bytes = 0
- e.Equals = false
+ // Zero all fields via struct literal to prevent stale data from previous pool reuse.
+ *e = Pair{EnterEv: enterEv}
return e
}
@@ -126,13 +119,7 @@ func (e *Pair) Recycle() {
if e.ExitEv != nil {
e.ExitEv.Recycle()
}
- e.EnterEv = nil
- e.ExitEv = nil
- e.File = nil
- e.Comm = ""
- e.Duration = 0
- e.DurationToPrev = 0
- e.Bytes = 0
- e.Equals = false
+ // Zero all fields via struct literal to prevent stale data on pool reuse.
+ *e = Pair{}
poolOfEventPairs.Put(e)
}
diff --git a/internal/file/file.go b/internal/file/file.go
index ab67fe2..fc9c320 100644
--- a/internal/file/file.go
+++ b/internal/file/file.go
@@ -99,8 +99,11 @@ func parseFlagsFromFdInfo(data []byte) (Flags, error) {
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "flags:") {
- flagsStr := strings.Fields(line)[1]
- flags, err := strconv.ParseUint(flagsStr, 8, 32)
+ fields := strings.Fields(line)
+ if len(fields) < 2 {
+ return unknownFlag, fmt.Errorf("malformed flags line in fdinfo: %q", line)
+ }
+ flags, err := strconv.ParseUint(fields[1], 8, 32)
return Flags(flags), err
}
}
@@ -118,7 +121,7 @@ func (f *FdFile) String() string {
var sb strings.Builder
if len(f.name) == 0 {
- sb.WriteString("E:name") // Emtpy name string
+ sb.WriteString("E:name") // Empty name string
} else {
sb.WriteString(f.name)
}
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 0baac9a..9b1c3cf 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -26,7 +26,7 @@ type Config struct {
PprofEnable bool
Duration int
- // Tracepints flags
+ // Tracepoints flags
TracepointsToAttach []*regexp.Regexp
TracepointsToExclude []*regexp.Regexp
@@ -234,6 +234,30 @@ func extractTracepointFlags(tracepoints string) (regexes []*regexp.Regexp, err e
return regexes, nil
}
+// TraceFilter builds a globalfilter.Filter from the config's filter fields.
+// If GlobalFilter is already active, it is returned as-is. Otherwise,
+// individual CLI-level filters (CommFilter, PathFilter, PidFilter, TidFilter)
+// are merged into a new filter.
+func (cfg Config) TraceFilter() globalfilter.Filter {
+ filter := cfg.GlobalFilter.Clone()
+ if filter.IsActive() {
+ return filter
+ }
+ if cfg.CommFilter != "" {
+ filter.Comm = &globalfilter.StringFilter{Pattern: cfg.CommFilter}
+ }
+ if cfg.PathFilter != "" {
+ filter.File = &globalfilter.StringFilter{Pattern: cfg.PathFilter}
+ }
+ if cfg.PidFilter > 0 {
+ filter.PID = globalfilter.NewEqFilter(int64(cfg.PidFilter))
+ }
+ if cfg.TidFilter > 0 {
+ filter.TID = globalfilter.NewEqFilter(int64(cfg.TidFilter))
+ }
+ return filter
+}
+
func (flags Config) ShouldIAttachTracepoint(tracepointName string) bool {
for _, re := range flags.TracepointsToExclude {
if re.MatchString(tracepointName) {
diff --git a/internal/flamegraph/iordata.go b/internal/flamegraph/iordata.go
index 76e438a..36b5103 100644
--- a/internal/flamegraph/iordata.go
+++ b/internal/flamegraph/iordata.go
@@ -14,8 +14,7 @@ import (
"ior/internal/file"
"ior/internal/types"
- // Is there a zstd library part of Go 1.25
- "github.com/DataDog/zstd"
+ "github.com/DataDog/zstd" // Go stdlib does not include zstd; third-party dep required
)
type pathType = string
@@ -61,19 +60,13 @@ func LoadFromFile(filename string) (iter.Seq[IterRecord], error) {
return iod.iter(), nil
}
-func cloneString(s string) string {
- // Clone the string by creating a new string with the same content
- // This is a workaround to avoid using unsafe package
- return string([]byte(s))
-}
-
-func (iod iorData) addEventPair(ev *event.Pair) {
+func (iod *iorData) addEventPair(ev *event.Pair) {
cnt := Counter{Count: 1, Duration: ev.Duration, DurationToPrev: ev.DurationToPrev, Bytes: ev.Bytes}
iod.add(ev.FileName(), ev.EnterEv.GetTraceId(), strings.TrimSpace(ev.Comm), ev.EnterEv.GetPid(),
ev.EnterEv.GetTid(), ev.Flags(), cnt)
}
-func (iod iorData) add(path pathType, traceId traceIdType, comm commType,
+func (iod *iorData) add(path pathType, traceId traceIdType, comm commType,
pid pidType, tid tidType, flags flagsType, addCnt Counter) {
key := recordKey{
@@ -92,14 +85,14 @@ func (iod iorData) add(path pathType, traceId traceIdType, comm commType,
iod.records[key] = cnt.add(addCnt)
}
-func (iod iorData) merge(other iorData) iorData {
+func (iod *iorData) merge(other iorData) *iorData {
for key, cnt := range other.records {
iod.add(key.Path, key.TraceID, key.Comm, key.Pid, key.Tid, key.Flags, cnt)
}
return iod
}
-func (iod iorData) serializeToFile(flamegraphName string) (retErr error) {
+func (iod *iorData) serializeToFile(flamegraphName string) (retErr error) {
hostname, err := hostnameFn()
if err != nil {
return fmt.Errorf("get hostname: %w", err)
@@ -118,24 +111,25 @@ func (iod iorData) serializeToFile(flamegraphName string) (retErr error) {
return fmt.Errorf("create temp file %s: %w", tmpFilename, err)
}
defer func() {
- if err := file.Close(); err != nil {
- retErr = errors.Join(retErr, fmt.Errorf("close temp file %s: %w", tmpFilename, err))
+ // Close file on error paths; on success it is already closed before rename.
+ if retErr != nil {
+ file.Close()
}
}()
encoder := zstd.NewWriter(file)
- defer func() {
- if err := encoder.Close(); err != nil {
- retErr = errors.Join(retErr, fmt.Errorf("close zstd writer for %s: %w", tmpFilename, err))
- }
- }()
gobEncoder := gob.NewEncoder(encoder)
if err := gobEncoder.Encode(iod.records); err != nil {
return fmt.Errorf("encode ior records: %w", err)
}
- if err := encoder.Flush(); err != nil {
- return fmt.Errorf("flush ior records: %w", err)
+ // Close encoder before file to flush the final zstd frame, then close
+ // the file to flush OS buffers. Both must complete before rename.
+ if err := encoder.Close(); err != nil {
+ return fmt.Errorf("close zstd writer for %s: %w", tmpFilename, err)
+ }
+ if err := file.Close(); err != nil {
+ return fmt.Errorf("close temp file %s: %w", tmpFilename, err)
}
if err := os.Rename(tmpFilename, filename); err != nil {
@@ -173,7 +167,7 @@ func (iod *iorData) loadFromFile(filename string) (retErr error) {
return nil
}
-func (iod iorData) serialize() ([]byte, error) {
+func (iod *iorData) serialize() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(iod.records)
@@ -224,7 +218,7 @@ func (ir IterRecord) StringByName(name string) (string, error) {
}
}
-func (iod iorData) iter() iter.Seq[IterRecord] {
+func (iod *iorData) iter() iter.Seq[IterRecord] {
return func(yield func(IterRecord) bool) {
for key, cnt := range iod.records {
record := IterRecord{
diff --git a/internal/flamegraph/iordata_test.go b/internal/flamegraph/iordata_test.go
index 722cad3..f805aab 100644
--- a/internal/flamegraph/iordata_test.go
+++ b/internal/flamegraph/iordata_test.go
@@ -90,7 +90,7 @@ func TestMerge(t *testing.T) {
t.Log("iod2", iod2)
t.Log("iod3", iod3)
t.Log("iod4", iod4)
- merged := iod1.merge(iod2).merge(iod3).merge(iod4)
+ merged := *iod1.merge(iod2).merge(iod3).merge(iod4)
t.Log("merged", merged)
t.Run("Merged correctly", func(t *testing.T) {
@@ -217,7 +217,7 @@ func TestMergeEmpty(t *testing.T) {
})
empty := newIorData()
- merged := iod.merge(empty)
+ merged := *iod.merge(empty)
if len(merged.records) != 1 {
t.Errorf("Expected 1 record, got %d", len(merged.records))
diff --git a/internal/flamegraph/livetrie.go b/internal/flamegraph/livetrie.go
index 600e404..51f3697 100644
--- a/internal/flamegraph/livetrie.go
+++ b/internal/flamegraph/livetrie.go
@@ -1,10 +1,10 @@
package flamegraph
import (
+ "cmp"
"encoding/json"
"fmt"
"slices"
- "sort"
"strings"
"sync"
"sync/atomic"
@@ -277,8 +277,8 @@ type childSnapshotState struct {
func buildSnapshotWithTotal(node *trieNode, depth int, minFraction float64, rootTotal uint64, forceKeep bool) (*trieSnapshot, uint64) {
total := node.value
children := slices.Clone(node.children)
- sort.Slice(children, func(i, j int) bool {
- return children[i].name < children[j].name
+ slices.SortFunc(children, func(a, b *trieNode) int {
+ return cmp.Compare(a.name, b.name)
})
childStates := make([]childSnapshotState, 0, len(children))
@@ -335,13 +335,13 @@ func ensureFallbackVisibleChildren(children []childSnapshotState, depth int, min
candidates = append(candidates, idx)
}
}
- sort.Slice(candidates, func(i, j int) bool {
- left := children[candidates[i]]
- right := children[candidates[j]]
- if left.total == right.total {
- return left.node.name < right.node.name
+ slices.SortFunc(candidates, func(a, b int) int {
+ left := children[a]
+ right := children[b]
+ if left.total != right.total {
+ return cmp.Compare(right.total, left.total)
}
- return left.total > right.total
+ return cmp.Compare(left.node.name, right.node.name)
})
limit := liveTrieMinVisibleChildrenWhenPruned
diff --git a/internal/flamegraph/trie.go b/internal/flamegraph/trie.go
index 022b846..d7790c2 100644
--- a/internal/flamegraph/trie.go
+++ b/internal/flamegraph/trie.go
@@ -1,6 +1,9 @@
package flamegraph
-import "sort"
+import (
+ "cmp"
+ "slices"
+)
type trieNode struct {
name string
@@ -35,8 +38,8 @@ func (t *trie) computeTotals() {
t.maxDepth = depth
}
- sort.Slice(node.children, func(i, j int) bool {
- return node.children[i].name < node.children[j].name
+ slices.SortFunc(node.children, func(a, b *trieNode) int {
+ return cmp.Compare(a.name, b.name)
})
total := node.value
diff --git a/internal/generate/codegen.go b/internal/generate/codegen.go
index 9b9f52c..1c7a9b3 100644
--- a/internal/generate/codegen.go
+++ b/internal/generate/codegen.go
@@ -1,8 +1,9 @@
package generate
import (
+ "cmp"
"fmt"
- "sort"
+ "slices"
"strings"
)
@@ -37,8 +38,8 @@ func GenerateTracepointsC(formats []Format) string {
accepted = append(accepted, tracepoints...)
}
- sort.Slice(accepted, func(i, j int) bool {
- return accepted[i].Format.ID > accepted[j].Format.ID
+ slices.SortFunc(accepted, func(a, b GeneratedTracepoint) int {
+ return cmp.Compare(b.Format.ID, a.Format.ID)
})
b.WriteString("\n")
@@ -147,6 +148,6 @@ func syscallFormatNames(sc Syscall) []string {
if sc.Exit != nil {
names = append(names, sc.Exit.Name)
}
- sort.Strings(names)
+ slices.Sort(names)
return names
}
diff --git a/internal/generate/typesgo.go b/internal/generate/typesgo.go
index dca40fc..9f91981 100644
--- a/internal/generate/typesgo.go
+++ b/internal/generate/typesgo.go
@@ -295,7 +295,7 @@ func writeGetterMethods(b *strings.Builder, goName, selfRef string) {
}
func writeSyncPool(b *strings.Builder, goName, selfRef string) {
- fmt.Fprintf(b, "var poolOf%ss = sync.Pool{\n\tNew: func() interface{} { return &%s{} },\n}\n\n", goName, goName)
+ fmt.Fprintf(b, "var poolOf%ss = sync.Pool{\n\tNew: func() any { return &%s{} },\n}\n\n", goName, goName)
fmt.Fprintf(b, "func New%s(raw []byte) *%s {\n", goName, goName)
fmt.Fprintf(b, "\t%s := poolOf%ss.Get().(*%s)\n", selfRef, goName, goName)
fmt.Fprintf(b, "\tif err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, %s); err != nil {\n", selfRef)
diff --git a/internal/globalfilter/filter.go b/internal/globalfilter/filter.go
index 6d19207..d55c4c1 100644
--- a/internal/globalfilter/filter.go
+++ b/internal/globalfilter/filter.go
@@ -23,6 +23,26 @@ type NumericFilter struct {
Value int64
}
+// NewEqFilter creates an equality NumericFilter for a positive value.
+// Returns nil if value is not positive.
+func NewEqFilter(value int64) *NumericFilter {
+ if value <= 0 {
+ return nil
+ }
+ return &NumericFilter{Op: OpEq, Value: value}
+}
+
+// EqValue returns the filter's positive equality value if the filter
+// represents an exact-match constraint (Op == OpEq and Value > 0).
+// Returns (0, false) when the filter is nil, uses a different operator,
+// or has a non-positive value.
+func (f *NumericFilter) EqValue() (int, bool) {
+ if f == nil || f.Op != OpEq || f.Value <= 0 {
+ return 0, false
+ }
+ return int(f.Value), true
+}
+
type StringFilter struct {
Pattern string
}
diff --git a/internal/parquet/schema.go b/internal/parquet/schema.go
index 2ede444..62d448b 100644
--- a/internal/parquet/schema.go
+++ b/internal/parquet/schema.go
@@ -1,7 +1,9 @@
package parquet
import (
+ "os"
"strconv"
+ "time"
"ior/internal/flags"
"ior/internal/streamrow"
@@ -35,6 +37,20 @@ type FileMetadata struct {
IORVersion string
}
+// NewFileMetadata constructs file-level metadata for a parquet trace file,
+// populating the hostname, timestamp, version, and recording mode.
+func NewFileMetadata(mode string) FileMetadata {
+ meta := FileMetadata{
+ StartedAtUnixNano: uint64(time.Now().UnixNano()),
+ Mode: mode,
+ IORVersion: flags.Version,
+ }
+ if hostname, err := os.Hostname(); err == nil {
+ meta.Hostname = hostname
+ }
+ return meta
+}
+
// RecordFromStream converts one shared stream row into the persisted format.
func RecordFromStream(row streamrow.Row, filterEpoch uint64) Record {
return Record{
diff --git a/internal/probemanager/grouping.go b/internal/probemanager/grouping.go
index aa4f133..be55659 100644
--- a/internal/probemanager/grouping.go
+++ b/internal/probemanager/grouping.go
@@ -35,19 +35,17 @@ func GroupTracepoints(names []string) map[string]TracepointPair {
}
func parseSyscallTracepoint(name string) (base string, isEnter bool, ok bool) {
- if strings.HasPrefix(name, sysEnterPrefix) {
- base = strings.TrimPrefix(name, sysEnterPrefix)
- if base == "" {
+ if after, found := strings.CutPrefix(name, sysEnterPrefix); found {
+ if after == "" {
return "", false, false
}
- return base, true, true
+ return after, true, true
}
- if strings.HasPrefix(name, sysExitPrefix) {
- base = strings.TrimPrefix(name, sysExitPrefix)
- if base == "" {
+ if after, found := strings.CutPrefix(name, sysExitPrefix); found {
+ if after == "" {
return "", false, false
}
- return base, false, true
+ return after, false, true
}
return "", false, false
}
diff --git a/internal/probemanager/manager.go b/internal/probemanager/manager.go
index 4c9dcec..cad755e 100644
--- a/internal/probemanager/manager.go
+++ b/internal/probemanager/manager.go
@@ -1,9 +1,10 @@
package probemanager
import (
+ "cmp"
"errors"
"fmt"
- "sort"
+ "slices"
"strings"
"sync"
)
@@ -244,7 +245,7 @@ func (m *Manager) States() []ProbeState {
}
out = append(out, state)
}
- sort.Slice(out, func(i, j int) bool { return out[i].Syscall < out[j].Syscall })
+ slices.SortFunc(out, func(a, b ProbeState) int { return cmp.Compare(a.Syscall, b.Syscall) })
return out
}
diff --git a/internal/statsengine/bench_test.go b/internal/statsengine/bench_test.go
index 646bdda..99d5c87 100644
--- a/internal/statsengine/bench_test.go
+++ b/internal/statsengine/bench_test.go
@@ -1,7 +1,7 @@
package statsengine
import (
- "math/rand"
+ "math/rand/v2"
"testing"
"time"
@@ -9,7 +9,7 @@ import (
)
func BenchmarkSyscallAccumulatorSnapshot(b *testing.B) {
- acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewSource(123)))
+ acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewPCG(123, 0)))
traceIDs := []types.TraceId{
types.SYS_ENTER_READ,
types.SYS_ENTER_WRITE,
diff --git a/internal/statsengine/filerank.go b/internal/statsengine/filerank.go
index dd83e8d..d24ab93 100644
--- a/internal/statsengine/filerank.go
+++ b/internal/statsengine/filerank.go
@@ -1,8 +1,9 @@
package statsengine
import (
+ "cmp"
"container/heap"
- "sort"
+ "slices"
"ior/internal/event"
"ior/internal/types"
@@ -123,11 +124,11 @@ func buildFileSnapshots(inputs []fileSnapshotInput) []FileSnapshot {
for _, in := range inputs {
out = append(out, in.toSnapshot())
}
- sort.Slice(out, func(i, j int) bool {
- if out[i].Accesses != out[j].Accesses {
- return out[i].Accesses > out[j].Accesses
+ slices.SortFunc(out, func(a, b FileSnapshot) int {
+ if a.Accesses != b.Accesses {
+ return cmp.Compare(b.Accesses, a.Accesses)
}
- return out[i].Path < out[j].Path
+ return cmp.Compare(a.Path, b.Path)
})
return out
}
diff --git a/internal/statsengine/process.go b/internal/statsengine/process.go
index b00a4bb..3bfd019 100644
--- a/internal/statsengine/process.go
+++ b/internal/statsengine/process.go
@@ -1,7 +1,8 @@
package statsengine
import (
- "sort"
+ "cmp"
+ "slices"
"time"
"ior/internal/event"
@@ -115,14 +116,14 @@ func buildProcessSnapshots(inputs []processSnapshotInput, elapsed time.Duration)
for _, in := range inputs {
result = append(result, in.toSnapshot(rateDiv))
}
- sort.Slice(result, func(i, j int) bool {
- if result[i].Syscalls != result[j].Syscalls {
- return result[i].Syscalls > result[j].Syscalls
+ slices.SortFunc(result, func(a, b ProcessSnapshot) int {
+ if a.Syscalls != b.Syscalls {
+ return cmp.Compare(b.Syscalls, a.Syscalls)
}
- if result[i].Bytes != result[j].Bytes {
- return result[i].Bytes > result[j].Bytes
+ if a.Bytes != b.Bytes {
+ return cmp.Compare(b.Bytes, a.Bytes)
}
- return result[i].PID < result[j].PID
+ return cmp.Compare(a.PID, b.PID)
})
return result
}
@@ -136,8 +137,14 @@ func (a *processAccumulator) compactIfNeeded() {
for _, stats := range a.byPID {
ordered = append(ordered, stats)
}
- sort.Slice(ordered, func(i, j int) bool {
- return betterProcessRank(ordered[i], ordered[j])
+ slices.SortFunc(ordered, func(a, b *processStats) int {
+ if betterProcessRank(a, b) {
+ return -1
+ }
+ if betterProcessRank(b, a) {
+ return 1
+ }
+ return 0
})
if len(ordered) > a.topN {
ordered = ordered[:a.topN]
diff --git a/internal/statsengine/syscall.go b/internal/statsengine/syscall.go
index 4feeab2..93931d1 100644
--- a/internal/statsengine/syscall.go
+++ b/internal/statsengine/syscall.go
@@ -1,9 +1,10 @@
package statsengine
import (
+ "cmp"
"math"
- "math/rand"
- "sort"
+ "math/rand/v2"
+ "slices"
"time"
"ior/internal/event"
@@ -55,15 +56,17 @@ type syscallSnapshotInput struct {
}
func newSyscallAccumulator() *syscallAccumulator {
- return newSyscallAccumulatorWithConfig(syscallReservoirSampleCapDefault, rand.New(rand.NewSource(time.Now().UnixNano())))
+ return newSyscallAccumulatorWithConfig(syscallReservoirSampleCapDefault, nil)
}
+// newSyscallAccumulatorWithConfig creates a syscall accumulator with the given
+// sample capacity and optional RNG. A nil rng uses the auto-seeded default.
func newSyscallAccumulatorWithConfig(sampleCap int, rng *rand.Rand) *syscallAccumulator {
if sampleCap <= 0 {
sampleCap = syscallReservoirSampleCapDefault
}
if rng == nil {
- rng = rand.New(rand.NewSource(time.Now().UnixNano()))
+ rng = rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64()))
}
return &syscallAccumulator{
@@ -135,11 +138,11 @@ func buildSyscallSnapshots(inputs []syscallSnapshotInput, elapsed time.Duration)
for _, in := range inputs {
result = append(result, in.toSnapshot(rateDiv))
}
- sort.Slice(result, func(i, j int) bool {
- if result[i].Count != result[j].Count {
- return result[i].Count > result[j].Count
+ slices.SortFunc(result, func(a, b SyscallSnapshot) int {
+ if a.Count != b.Count {
+ return cmp.Compare(b.Count, a.Count)
}
- return result[i].Name < result[j].Name
+ return cmp.Compare(a.Name, b.Name)
})
return result
}
@@ -161,8 +164,8 @@ func (s *syscallStats) addSample(duration uint64, cap int, rng *rand.Rand) {
return
}
- idx := rng.Int63n(int64(s.seenLatencies))
- if idx >= int64(cap) {
+ idx := rng.IntN(int(s.seenLatencies))
+ if idx >= cap {
return
}
s.samples[idx] = duration
@@ -183,7 +186,7 @@ func (s *syscallStats) ensurePercentiles() {
}
sorted := append([]uint64(nil), s.samples...)
- sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
+ slices.Sort(sorted)
s.cachedP50 = samplePercentile(sorted, 0.50)
s.cachedP95 = samplePercentile(sorted, 0.95)
s.cachedP99 = samplePercentile(sorted, 0.99)
diff --git a/internal/statsengine/syscall_test.go b/internal/statsengine/syscall_test.go
index b315bd8..b00582d 100644
--- a/internal/statsengine/syscall_test.go
+++ b/internal/statsengine/syscall_test.go
@@ -2,7 +2,7 @@ package statsengine
import (
"math"
- "math/rand"
+ "math/rand/v2"
"testing"
"time"
@@ -11,7 +11,7 @@ import (
)
func TestSyscallAccumulatorBasicStats(t *testing.T) {
- acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewSource(1)))
+ acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewPCG(1, 0)))
traceID := types.SYS_ENTER_READ
acc.Add(newPair(traceID, 10, 100, 0))
@@ -54,7 +54,7 @@ func TestSyscallAccumulatorBasicStats(t *testing.T) {
}
func TestSyscallAccumulatorSortsByCountThenName(t *testing.T) {
- acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewSource(2)))
+ acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewPCG(2, 0)))
idA := types.SYS_ENTER_OPENAT
idB := types.SYS_ENTER_READ
@@ -76,7 +76,7 @@ func TestSyscallAccumulatorSortsByCountThenName(t *testing.T) {
}
func TestSyscallAccumulatorReservoirPercentilesAccuracy(t *testing.T) {
- acc := newSyscallAccumulatorWithConfig(100, rand.New(rand.NewSource(7)))
+ acc := newSyscallAccumulatorWithConfig(100, rand.New(rand.NewPCG(7, 0)))
traceID := types.SYS_ENTER_WRITE
for d := uint64(1); d <= 10_000; d++ {
@@ -95,7 +95,7 @@ func TestSyscallAccumulatorReservoirPercentilesAccuracy(t *testing.T) {
}
func TestSyscallAccumulatorZeroElapsedRate(t *testing.T) {
- acc := newSyscallAccumulatorWithConfig(32, rand.New(rand.NewSource(9)))
+ acc := newSyscallAccumulatorWithConfig(32, rand.New(rand.NewPCG(9, 0)))
acc.Add(newPair(types.SYS_ENTER_READ, 9, 0, 0))
snap := acc.Snapshot(0)
@@ -108,7 +108,7 @@ func TestSyscallAccumulatorZeroElapsedRate(t *testing.T) {
}
func TestSyscallAccumulatorPercentilesRecomputeAfterThreshold(t *testing.T) {
- acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewSource(11)))
+ acc := newSyscallAccumulatorWithConfig(10_000, rand.New(rand.NewPCG(11, 0)))
traceID := types.SYS_ENTER_READ
for i := 1; i <= 1000; i++ {
diff --git a/internal/tui/dashboard/bubbles.go b/internal/tui/dashboard/bubbles.go
index e6e2909..f50eba8 100644
--- a/internal/tui/dashboard/bubbles.go
+++ b/internal/tui/dashboard/bubbles.go
@@ -1,11 +1,12 @@
package dashboard
import (
+ "cmp"
"fmt"
"hash/fnv"
"image/color"
"math"
- "sort"
+ "slices"
"strings"
"unicode/utf8"
@@ -422,8 +423,8 @@ func (c *bubbleChart) renderBubblesToGrid(grid [][]bubbleCell, width, height int
for idx := range c.nodes {
order = append(order, idx)
}
- sort.Slice(order, func(i, j int) bool {
- return c.nodes[order[i]].radius < c.nodes[order[j]].radius
+ slices.SortFunc(order, func(a, b int) int {
+ return cmp.Compare(c.nodes[a].radius, c.nodes[b].radius)
})
if c.selected >= 0 && c.selected < len(c.nodes) {
filtered := order[:0]
@@ -641,13 +642,13 @@ func buildBubbleTargets(data []bubbleDatum, metric bubbleMetric, width, height i
if len(filtered) == 0 {
return nil
}
- sort.Slice(filtered, func(i, j int) bool {
- vi := bubbleValue(filtered[i], metric)
- vj := bubbleValue(filtered[j], metric)
- if vi != vj {
- return vi > vj
+ slices.SortFunc(filtered, func(a, b bubbleDatum) int {
+ va := bubbleValue(a, metric)
+ vb := bubbleValue(b, metric)
+ if va != vb {
+ return cmp.Compare(vb, va)
}
- return filtered[i].Label < filtered[j].Label
+ return cmp.Compare(a.Label, b.Label)
})
if len(filtered) > bubbleMaxItems {
filtered = filtered[:bubbleMaxItems]
diff --git a/internal/tui/dashboard/files.go b/internal/tui/dashboard/files.go
index f24c87c..df850ab 100644
--- a/internal/tui/dashboard/files.go
+++ b/internal/tui/dashboard/files.go
@@ -1,9 +1,9 @@
package dashboard
import (
+ "cmp"
"path/filepath"
"slices"
- "sort"
"strconv"
"ior/internal/statsengine"
@@ -425,11 +425,11 @@ func aggregateFilesByDir(files []statsengine.FileSnapshot) []DirSnapshot {
out = append(out, s)
}
- sort.Slice(out, func(i, j int) bool {
- if out[i].Accesses != out[j].Accesses {
- return out[i].Accesses > out[j].Accesses
+ slices.SortFunc(out, func(a, b DirSnapshot) int {
+ if a.Accesses != b.Accesses {
+ return cmp.Compare(b.Accesses, a.Accesses)
}
- return out[i].Dir < out[j].Dir
+ return cmp.Compare(a.Dir, b.Dir)
})
return out
}
diff --git a/internal/tui/dashboard/icicle.go b/internal/tui/dashboard/icicle.go
index 92c4834..768783b 100644
--- a/internal/tui/dashboard/icicle.go
+++ b/internal/tui/dashboard/icicle.go
@@ -1,10 +1,11 @@
package dashboard
import (
+ "cmp"
"fmt"
"math"
"path/filepath"
- "sort"
+ "slices"
"strings"
"ior/internal/statsengine"
@@ -169,13 +170,13 @@ func sortedIcicleChildren(node *icicleNode, metric bubbleMetric) []*icicleNode {
for _, child := range node.children {
out = append(out, child)
}
- sort.Slice(out, func(i, j int) bool {
- vi := icicleValue(out[i], metric)
- vj := icicleValue(out[j], metric)
- if vi != vj {
- return vi > vj
+ slices.SortFunc(out, func(a, b *icicleNode) int {
+ va := icicleValue(a, metric)
+ vb := icicleValue(b, metric)
+ if va != vb {
+ return cmp.Compare(vb, va)
}
- return out[i].name < out[j].name
+ return cmp.Compare(a.name, b.name)
})
return out
}
diff --git a/internal/tui/dashboard/treemap.go b/internal/tui/dashboard/treemap.go
index 7193952..dd62d13 100644
--- a/internal/tui/dashboard/treemap.go
+++ b/internal/tui/dashboard/treemap.go
@@ -1,10 +1,11 @@
package dashboard
import (
+ "cmp"
"fmt"
"image/color"
"math"
- "sort"
+ "slices"
"strings"
"unicode/utf8"
@@ -131,11 +132,11 @@ func buildSyscallTreemapItems(snap *statsengine.Snapshot, metric bubbleMetric) [
if len(items) == 0 {
return nil
}
- sort.Slice(items, func(i, j int) bool {
- if items[i].Value != items[j].Value {
- return items[i].Value > items[j].Value
+ slices.SortFunc(items, func(a, b syscallTreemapItem) int {
+ if a.Value != b.Value {
+ return cmp.Compare(b.Value, a.Value)
}
- return items[i].Name < items[j].Name
+ return cmp.Compare(a.Name, b.Name)
})
if len(items) > maxSyscallTreemapItems {
items = items[:maxSyscallTreemapItems]
@@ -174,11 +175,11 @@ func buildFilesTreemapItems(snap *statsengine.Snapshot, metric bubbleMetric) []s
if len(items) == 0 {
return nil
}
- sort.Slice(items, func(i, j int) bool {
- if items[i].Value != items[j].Value {
- return items[i].Value > items[j].Value
+ slices.SortFunc(items, func(a, b syscallTreemapItem) int {
+ if a.Value != b.Value {
+ return cmp.Compare(b.Value, a.Value)
}
- return items[i].Name < items[j].Name
+ return cmp.Compare(a.Name, b.Name)
})
if len(items) > maxSyscallTreemapItems {
items = items[:maxSyscallTreemapItems]
@@ -217,11 +218,11 @@ func buildProcessesTreemapItems(snap *statsengine.Snapshot, metric bubbleMetric)
if len(items) == 0 {
return nil
}
- sort.Slice(items, func(i, j int) bool {
- if items[i].Value != items[j].Value {
- return items[i].Value > items[j].Value
+ slices.SortFunc(items, func(a, b syscallTreemapItem) int {
+ if a.Value != b.Value {
+ return cmp.Compare(b.Value, a.Value)
}
- return items[i].Name < items[j].Name
+ return cmp.Compare(a.Name, b.Name)
})
if len(items) > maxSyscallTreemapItems {
items = items[:maxSyscallTreemapItems]
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index a7b26f8..0552a4f 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -1,11 +1,11 @@
package flamegraph
import (
+ "cmp"
"encoding/json"
"fmt"
"image/color"
"slices"
- "sort"
"strings"
"time"
@@ -727,8 +727,8 @@ func framesAtDepthFiltered(frames []tuiFrame, depth int, include map[int]bool) [
indices = append(indices, idx)
}
}
- sort.Slice(indices, func(i, j int) bool {
- return frames[indices[i]].Col < frames[indices[j]].Col
+ slices.SortFunc(indices, func(a, b int) int {
+ return cmp.Compare(frames[a].Col, frames[b].Col)
})
return indices
}
@@ -878,19 +878,19 @@ func (m Model) visibleTraversalOrder() []int {
}
indices = append(indices, idx)
}
- sort.Slice(indices, func(i, j int) bool {
- left := m.frames[indices[i]]
- right := m.frames[indices[j]]
+ slices.SortFunc(indices, func(a, b int) int {
+ left := m.frames[a]
+ right := m.frames[b]
if left.Depth != right.Depth {
- return left.Depth < right.Depth
+ return cmp.Compare(left.Depth, right.Depth)
}
if left.Col != right.Col {
- return left.Col < right.Col
+ return cmp.Compare(left.Col, right.Col)
}
if left.Row != right.Row {
- return left.Row < right.Row
+ return cmp.Compare(left.Row, right.Row)
}
- return indices[i] < indices[j]
+ return cmp.Compare(a, b)
})
return indices
}
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
index f9f6a89..12e5f8e 100644
--- a/internal/tui/flamegraph/renderer.go
+++ b/internal/tui/flamegraph/renderer.go
@@ -1,11 +1,12 @@
package flamegraph
import (
+ "cmp"
"fmt"
"hash/fnv"
"image/color"
"math"
- "sort"
+ "slices"
"strings"
"unicode/utf8"
@@ -129,11 +130,11 @@ func allocateChildWidths(children []*snapshotNode, parentTotal uint64, span int)
// If proportional rounding culled every child, surface top contributors so
// the user can still navigate beyond the root frame.
if used == 0 {
- sort.Slice(items, func(i, j int) bool {
- if items[i].total == items[j].total {
- return items[i].idx < items[j].idx
+ slices.SortFunc(items, func(a, b childWidth) int {
+ if a.total != b.total {
+ return cmp.Compare(b.total, a.total)
}
- return items[i].total > items[j].total
+ return cmp.Compare(a.idx, b.idx)
})
visible := min(span, len(items))
for i := 0; i < visible; i++ {
@@ -334,8 +335,8 @@ func buildRenderRows(frames []tuiFrame, width, rowOffset, maxRow, barHeight, ava
rows := make([]string, 0, (maxRow-rowOffset+1)*barHeight)
for row := maxRow; row >= rowOffset; row-- {
framesAtRow := rowsByDepth[row]
- sort.Slice(framesAtRow, func(i, j int) bool {
- return framesAtRow[i].frame.Col < framesAtRow[j].frame.Col
+ slices.SortFunc(framesAtRow, func(a, b indexedFrame) int {
+ return cmp.Compare(a.frame.Col, b.frame.Col)
})
for repeat := 0; repeat < barHeight; repeat++ {
showLabels := repeat == barHeight/2
@@ -680,8 +681,8 @@ func compactMatchRoots(frames []tuiFrame, matchSet map[int]bool) []matchRoot {
total: frames[idx].Total,
})
}
- sort.Slice(roots, func(i, j int) bool {
- return len(roots[i].path) < len(roots[j].path)
+ slices.SortFunc(roots, func(a, b matchRoot) int {
+ return cmp.Compare(len(a.path), len(b.path))
})
merged := make([]matchRoot, 0, len(roots))
for _, candidate := range roots {
diff --git a/internal/tui/pidpicker/proclist.go b/internal/tui/pidpicker/proclist.go
index 20e580d..73ff209 100644
--- a/internal/tui/pidpicker/proclist.go
+++ b/internal/tui/pidpicker/proclist.go
@@ -2,11 +2,12 @@ package pidpicker
import (
"bytes"
+ "cmp"
"fmt"
"io/fs"
"os"
"path/filepath"
- "sort"
+ "slices"
"strconv"
"strings"
"sync"
@@ -50,8 +51,8 @@ func scanProcessesFrom(procRoot string) ([]ProcessInfo, error) {
processes = append(processes, process)
}
- sort.Slice(processes, func(i, j int) bool {
- return processes[i].Pid < processes[j].Pid
+ slices.SortFunc(processes, func(a, b ProcessInfo) int {
+ return cmp.Compare(a.Pid, b.Pid)
})
return processes, nil
}
@@ -145,8 +146,8 @@ func scanThreadsFrom(procRoot string, pid int) ([]ProcessInfo, error) {
threads = append(threads, thread)
}
- sort.Slice(threads, func(i, j int) bool {
- return threads[i].Pid < threads[j].Pid
+ slices.SortFunc(threads, func(a, b ProcessInfo) int {
+ return cmp.Compare(a.Pid, b.Pid)
})
return threads, nil
}
@@ -209,11 +210,11 @@ func scanAllThreadsFrom(procRoot string) ([]ProcessInfo, error) {
}
wg.Wait()
- sort.Slice(threads, func(i, j int) bool {
- if threads[i].Pid == threads[j].Pid {
- return threads[i].ParentPID < threads[j].ParentPID
+ slices.SortFunc(threads, func(a, b ProcessInfo) int {
+ if a.Pid != b.Pid {
+ return cmp.Compare(a.Pid, b.Pid)
}
- return threads[i].Pid < threads[j].Pid
+ return cmp.Compare(a.ParentPID, b.ParentPID)
})
return threads, nil
}
diff --git a/internal/types/generated_types.go b/internal/types/generated_types.go
index 63c4319..ff3ed02 100644
--- a/internal/types/generated_types.go
+++ b/internal/types/generated_types.go
@@ -338,7 +338,7 @@ func (o *OpenEvent) GetTime() uint64 {
}
var poolOfOpenEvents = sync.Pool{
- New: func() interface{} { return &OpenEvent{} },
+ New: func() any { return &OpenEvent{} },
}
func NewOpenEvent(raw []byte) *OpenEvent {
@@ -405,7 +405,7 @@ func (n *NullEvent) GetTime() uint64 {
}
var poolOfNullEvents = sync.Pool{
- New: func() interface{} { return &NullEvent{} },
+ New: func() any { return &NullEvent{} },
}
func NewNullEvent(raw []byte) *NullEvent {
@@ -473,7 +473,7 @@ func (f *FdEvent) GetTime() uint64 {
}
var poolOfFdEvents = sync.Pool{
- New: func() interface{} { return &FdEvent{} },
+ New: func() any { return &FdEvent{} },
}
func NewFdEvent(raw []byte) *FdEvent {
@@ -542,7 +542,7 @@ func (r *RetEvent) GetTime() uint64 {
}
var poolOfRetEvents = sync.Pool{
- New: func() interface{} { return &RetEvent{} },
+ New: func() any { return &RetEvent{} },
}
func NewRetEvent(raw []byte) *RetEvent {
@@ -611,7 +611,7 @@ func (n *NameEvent) GetTime() uint64 {
}
var poolOfNameEvents = sync.Pool{
- New: func() interface{} { return &NameEvent{} },
+ New: func() any { return &NameEvent{} },
}
func NewNameEvent(raw []byte) *NameEvent {
@@ -679,7 +679,7 @@ func (p *PathEvent) GetTime() uint64 {
}
var poolOfPathEvents = sync.Pool{
- New: func() interface{} { return &PathEvent{} },
+ New: func() any { return &PathEvent{} },
}
func NewPathEvent(raw []byte) *PathEvent {
@@ -749,7 +749,7 @@ func (f *FcntlEvent) GetTime() uint64 {
}
var poolOfFcntlEvents = sync.Pool{
- New: func() interface{} { return &FcntlEvent{} },
+ New: func() any { return &FcntlEvent{} },
}
func NewFcntlEvent(raw []byte) *FcntlEvent {
@@ -818,7 +818,7 @@ func (d *Dup3Event) GetTime() uint64 {
}
var poolOfDup3Events = sync.Pool{
- New: func() interface{} { return &Dup3Event{} },
+ New: func() any { return &Dup3Event{} },
}
func NewDup3Event(raw []byte) *Dup3Event {
@@ -886,7 +886,7 @@ func (o *OpenByHandleAtEvent) GetTime() uint64 {
}
var poolOfOpenByHandleAtEvents = sync.Pool{
- New: func() interface{} { return &OpenByHandleAtEvent{} },
+ New: func() any { return &OpenByHandleAtEvent{} },
}
func NewOpenByHandleAtEvent(raw []byte) *OpenByHandleAtEvent {