diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-18 20:54:35 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-18 20:54:35 +0200 |
| commit | cd554b0af706b5f62b4e1bfde04091052b4aac61 (patch) | |
| tree | e6d02f1c2a1da27da17386e8832c2d4a3e699cdf | |
| parent | b421b2232351049277ee4ad5b31367bb2b6779bb (diff) | |
cleanup
36 files changed, 301 insertions, 1463 deletions
@@ -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-fl |
