From cd554b0af706b5f62b4e1bfde04091052b4aac61 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 18 Mar 2026 20:54:35 +0200 Subject: cleanup --- docs/tui-flamegraph-plan.md | 450 -------------------------------------------- 1 file changed, 450 deletions(-) delete mode 100644 docs/tui-flamegraph-plan.md (limited to 'docs/tui-flamegraph-plan.md') 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 -``` -- cgit v1.2.3