summaryrefslogtreecommitdiff
path: root/internal
AgeCommit message (Collapse)Author
2026-05-13keep View() pure by moving state transitions into Update() handlersPaul Buetow
The dashboard model's View() was mutating sub-model state on every render: it called streamModel.SetFooterVisible() and flameModel.SetViewport() on local copies instead of keeping those fields in sync through Update(). Moved the sync points to the Update() handlers that trigger each change: - handleWindowSize: syncs stream footer visibility alongside SetViewport - handleHelpToggleKey: already updated flame viewport; now also updates stream footer visibility when the help bar is toggled - handleKey: syncs flame viewport when switching to the flame tab, covering the case that window-resize and help-toggle do not Aligned the stream model's initial showFooter value with the dashboard's default showHelp=false in NewModelWithConfig so there is no mismatch on the first render. Also extracted numeric tab shortcuts into the tab registry via ShortcutKey fields so handleShortcutKey no longer needs updating when tabs are added. Updated two tests that bypassed Update() to set dimensions directly; they now use WindowSizeMsg so sub-model viewports are initialised correctly under the new pure-View contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13refactor: extract outputFormatter collaborator from eventLoopPaul Buetow
The printCb and warningCb function fields on eventLoop bundled two distinct concerns (pair emission and warning delivery) directly on the event-processing struct. This commit extracts them into a dedicated outputFormatter type that owns these callbacks plus emit() and notifyWarning() helper methods. outputFormatter is embedded (not pointed-to) in eventLoop so that existing call sites — including tests that write el.printCb = ... and el.warningCb = ... directly — require no changes beyond the three struct-literal sites in eventloop_filter_test.go that used field initialiser syntax. fdTracker and commResolver were already proper collaborator types; only the output concern needed extraction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13introduce Accumulator interface in statsengine to separate ingestion from ↵Paul Buetow
snapshot-building Define statsengine.Accumulator (Ingest + Reset) to represent the event-accumulation responsibility separately from runtime.SnapshotSource (Snapshot), which handles the read side. This reduces the SRP violation in Engine: callers that only push events now hold an Accumulator; callers that only read statistics hold a SnapshotSource. - Add Accumulator interface and compile-time assertion in statsengine/engine.go - Add EventIngester type alias (= statsengine.Accumulator) in runtime/runtime.go with a compile-time assertion, so callers in the runtime layer can reference the ingestion contract without importing statsengine directly - Split tuiRuntime.engine field into accumulator + snapSource so the event-loop callback holds Accumulator and wireRuntimeBindings passes SnapshotSource to SetDashboardSnapshotSource — making each consumer's dependency explicit - Simplify resetDashboardSnapshotSource in tui.go to cast for interface{ Reset() } independently of Snapshot(), removing the combined ad-hoc interface check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: stop BPF ring-buffer polling goroutine on trace end to prevent leakPaul Buetow
setupEventChannel now returns the *bpf.RingBuffer handle alongside the event channel. Both callers (runTraceWithContext, runHeadlessParquet) defer rb.Stop() so the libbpfgo polling goroutine and C ring_buffer struct are released promptly when the trace context is cancelled, rather than waiting for bpfModule.Close() to eventually call rb.Close(). rb.Stop() and rb.Close() are both idempotent, so the double-call from the defer and from bpfModule.Close() is safe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13refactor: move TraceFilter and tracepoint selector logic out of flags.ConfigPaul Buetow
- Add tracepoints.Selector type with ShouldAttach method and ParseSelector constructor, replacing the raw TracepointsToAttach/TracepointsToExclude regex slices on flags.Config. - Add flags.BuildTraceFilter as a standalone function replacing the Config.TraceFilter() method, keeping filter-building logic out of the config struct. - Remove stale ShouldIAttachTracepoint noise-filter entry from Magefile. - Add selector_test.go with full coverage of ParseSelector and ShouldAttach. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13extract generic sortedWithState helper to deduplicate tab sort logicPaul Buetow
Four near-identical sorted-rows functions (sortedFileSnapshots, sortedDirSnapshots, sortedSyscallSnapshots, sortedProcessTableRows) each repeated the same guard-clone-sort-tiebreak-apply pattern. Replace them with a single generic sortedWithState[T,K] in sort.go that accepts per-tab byKey and tiebreak comparators as parameters. Remove now-unused slices imports from syscalls.go and processes.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13refactor: make receiver types consistent per type (pointer receivers)Paul Buetow
fileRankHeap Len/Less/Swap converted to pointer receivers to match Push/Pop; bubbleChart HasNodes and AnimationState Settled converted to pointer receivers to match all other methods on their types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13use errgroup instead of WaitGroup for concurrent snapshot buildersPaul Buetow
Replace sync.WaitGroup with errgroup.Group in buildSubSnapshots so errors from sub-builders (buildSyscallSnapshots, buildFileSnapshots, buildProcessSnapshots, buildHistogramSnapshot) are captured and propagated rather than silently dropped. Change Engine.Snapshot() to return (*Snapshot, error), update runtime.SnapshotSource and dashboard.SnapshotSource interfaces accordingly, and adjust all callers in tui.go, dashboard/model.go, and the test helpers. Each sub-builder now returns (result, error); the error return is currently always nil but establishes the contract for future validation. The per-type Snapshot() convenience methods (histogram, syscall, file, process) panic on error since they are internal helpers where failure would be a programming bug. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13perf: replace string += concatenation with strings.Builder in TUI render hot ↵Paul Buetow
paths Swap out ad-hoc += string concatenation in the flamegraph toolbar/status lines, dashboard filter summary, bubble/treemap status lines, eventstream view, processes tab, and probes list for strings.Builder, eliminating redundant allocations on every render tick. Also update dashboard/model_test.go fake SnapshotSource implementations to match the updated interface signature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13refactor(generate): replace classifySyscall switches with kindRegistry (OCP)Paul Buetow
Introduce kindregistry.go with a kindMeta struct (structName, enterAccepted) and a kindRegistry map keyed by TracepointKind. Replace the switch in isEnterRejected (codegen.go) and the switch in eventStructName (bpfhandler.go) with lookupKind registry lookups. Adding a new TracepointKind now only requires a single registry entry — no switch statements need to be touched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13test: replace time.Sleep with deterministic synchronization in unit testsPaul Buetow
Replace three time.Sleep usages in tests with channel-based and happens-before reasoning: - internal/ior_mode_test.go: waitForStreamRows no longer polls with time.Sleep(1ms); starter() only returns after the trace goroutine closes the started channel, which happens after all printCb pushes, forming a happens-before edge that guarantees the rows are visible. - internal/probemanager/manager_test.go: the intermediate attach-count assertions (enter==1, exit==0) are now checked immediately after <-enterBlocked, which is itself a happens-before edge, rather than after a 50ms sleep. The concurrent goroutine 2 is started afterwards; the final count assertions after both goroutines complete verify the serialization invariant. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13feat(eventloop): add panic recovery to events() goroutine for resiliencePaul Buetow
Wrap processRawEvent calls in a new processRawEventSafe() helper that uses defer/recover to catch any panic from a callback and convert it into a warning notification via warningCb, preventing a single bad event from crashing the whole process. Added TestEventsPanicInCallbackIsRecoveredAndNotified to verify the recovery behaviour end-to-end. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix(pidpicker): eliminate defer-inside-goroutine-in-loop anti-patternsPaul Buetow
Extract the per-pid goroutine body from scanAllThreadsFrom into a named scanThreadsWorker function. This removes both defer statements from the anonymous closure that was spawned inside a for loop: the semaphore release is now an explicit <-sem call immediately after I/O completes, and wg.Done() is called directly after scanThreadsWorker returns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: avoid int64->int truncation in NumericFilter.EqValue on 32-bit archPaul Buetow
EqValue() previously returned int, silently truncating int64 values that exceed math.MaxInt32 on 32-bit architectures. Change the return type to int64 and update callers (ior.go, filterstack.go) with explicit int() casts that are safe because Linux PID/TID values never exceed 4194304. Add TestEqValueReturnsInt64PreservesLargeValues to guard the fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13add compile-time interface assertions for public types (task c4)Paul Buetow
Extend the var _ Interface = (*Concrete)(nil) coverage started in j3 to cover the remaining public types not yet guarded: - *file.FdFile → file.File (file/file.go) - streamrow.Row → globalfilter.Candidate (streamrow/row.go; adds globalfilter import — no cycle since globalfilter does not import streamrow) - *streamrow.RingBuffer → eventstream.Source (tui/eventstream/ringbuffer.go; already a type alias for streamrow.RingBuffer) - *probemanager.Manager → tui/probes.Manager (tui/probes/model.go) - All 9 generated *types.*Event types → event.Event in the new file internal/event/interface_assertions.go (non-generated, lives in the event package which already imports types, so no new import cycle) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13improve unit test coverage to >=60% in probes, common, export, streamrow, ↵Paul Buetow
pidpicker, tui/export Before: probes=30%, tui/common=41%, export=0%, streamrow=25%, pidpicker=59%, tui/export=45% After: probes=89%, tui/common=97%, export=77%, streamrow=100%, pidpicker=73%, tui/export=99% New test files cover RingBuffer push/wrap/reset, Row accessor methods, nil Sequencer safety, SnapshotCSV nil and data paths, helper functions snapValue / snapValueF / trendSummary, all table navigation keys, VisibleTableWindow/ ClampTableCol edge cases, RenderTableHeader/Row, PickerShortHelp, probe modal navigation/search/toggle/view/error paths, truncateText/sanitizeOneLine, export modal View rendering, key navigation, status messages, scanAllThreadsFrom, readThreadInfo guards, formatProcess variants, and clamp helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13split Event interface into EventIdentity and EventLifecyclePaul Buetow
Extract GetTraceId/GetPid/GetTid into EventIdentity (routing and aggregation concerns) and Recycle into EventLifecycle (pool memory management). Event embeds both and retains String, GetTime, Equals. All existing implementations satisfy the composed interface unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13enforce gofmt formatting and add Fmt/FmtCheck mage targetsPaul Buetow
Run gofmt -w on 9 files that had minor alignment/whitespace drift (pair.go, filter.go, ior_mode_registry.go, ior_mode_test.go, runtime.go, engine.go, dashboard/model.go, flamegraph/model.go, flamegraph/renderer.go). Add two new Mage targets to Magefile.go: - `mage fmt` – rewrites all .go files in-place via go/format - `mage fmtCheck` – dry-run check; fails with a list of offending files, suitable as a CI gate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: move context.Context to first parameter in startTraceCmd and ↵Paul Buetow
startTraceCmdWithTimeout Per Go convention, context.Context must always be the first parameter. Updated both function signatures and all call sites in tracelifecycle.go and tui_test.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: prevent goroutine leak in tuiTraceStarterFromRunTrace via done channelPaul Buetow
Replace the buffered errCh (capacity 1) with an unbuffered channel plus a dedicated done channel that is closed via defer when the outer function returns for any reason (ctx cancellation, startedCh signal, or startup error). The trace goroutine selects on both errCh and done when delivering its result, guaranteeing it can always exit and is never left blocked waiting on a channel that nobody will drain. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: prevent path traversal in TUI stream CSV export filenamePaul Buetow
User-supplied filenames are now sanitised through filepath.Base before being joined with exportDir, so inputs like "../../etc/passwd" can no longer write files outside the intended export directory. Pure directory references ("..") are rejected outright. Two new tests cover both the unit-level sanitisation and the end-to-end exportRowsToCSV path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13cap filterStack undo history to 50 levels to prevent unbounded growthPaul Buetow
Adds maxFilterHistory=50 and evicts the oldest entry from both the history and label-stack slices in filterStack.push whenever the cap is exceeded. Covers the fix with a new table-driven unit test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: use shell-aware tokenizer in resolveEditorCommand to handle EDITOR ↵Paul Buetow
paths with spaces Replace strings.Fields with a new shellSplit function that implements POSIX-like quoting rules (single-quotes, double-quotes, backslash escapes), so EDITOR values such as '/My Editor/hx' or "/path/with spaces/hx" --wait are correctly parsed rather than mangled into multiple path components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: reject negative and zero -duration flag values with a clear errorPaul Buetow
A negative or zero -duration was silently accepted, causing the trace context to be cancelled immediately (time.Duration(N) * time.Second with N <= 0 yields a non-positive timeout), so no events were ever captured. parseFromFlagSet now returns an error for Duration <= 0, matching the existing pattern used for -resetTimer validation. Three new tests cover the negative, zero, and valid positive cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: guard Pair.CalculateDurations against uint64 underflow on clock skewPaul Buetow
BPF timestamps can be non-monotonic across CPUs (NTP step, TSC skew). When exit < enter or enter < prevPairTime the uint64 subtraction wraps to a huge value, corrupting latency histograms and flamegraph weights. Clamp both Duration and DurationToPrev to 0 instead of underflowing. Add TestPairCalculateDurationsNegativeDelta to cover this case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: add 1s context timeout to commResolver procfs reads to prevent ↵Paul Buetow
indefinite blocking Frozen cgroup entries in /proc could stall a lookup worker goroutine forever, preventing clean shutdown because shutdown() waits on workersWG.Wait(). Changed resolveFn signature to accept context.Context and wrap each call in context.WithTimeout(1s) in both lookupWorker and seedTrackedPidComm. Added TestCommResolverLookupWorkerRespectsTimeout to verify the pending entry is cleared after a timeout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: log swallowed defer mgr.Close errors for BPF resourcesPaul Buetow
Wrap the bare `defer mgr.Close()` calls in runTraceWithContext (ior.go) and runHeadlessParquet (ior_parquet_sink.go) in a closure that checks the returned error and logs it via logln, so BPF probe-detach failures and map-cleanup errors are no longer silently discarded. bpfModule.Close() has no return value and is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: wrap SetMaxEntries and GetMap errors in resizeBPFMap with map name contextPaul Buetow
Raw errors from GetMap and SetMaxEntries gave no indication of which BPF map failed. Wrap both with fmt.Errorf including the map name (and target size for SetMaxEntries) so callers can immediately identify the offending map. Also simplify resizeBPFMaps to return directly since resizeBPFMap now carries full context, removing the redundant outer wrap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: add 30s startup timeout to startTraceCmd to prevent indefinite hang on ↵Paul Buetow
BPF attach failure If kernel lock contention or another issue causes BPF probe attachment to stall, the TUI previously remained in the 'Attaching tracepoints...' spinner state forever. startTraceCmdWithTimeout now races the starter goroutine against a configurable deadline (defaultStartupTimeout = 30s) and returns a TracingErrorMsg with a clear message when the deadline expires. The stuck goroutine is cleaned up when the caller cancels the trace context on the next user action (e.g. traceLifecycle.stop). Two new tests cover the timeout and context-cancel paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix: log pprof.WriteHeapProfile error instead of silently ignoring itPaul Buetow
Silent disk-full or permission-denied failures when writing the heap profile are now surfaced via logln so the operator can diagnose them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13fix ignored file.Close error in iordata serialization error pathPaul Buetow
On serialization failure the deferred close was called but its error silently discarded, potentially masking filesystem issues such as a full-disk condition detected only at close time. Capture the close error and join it with the existing retErr via errors.Join so callers see the full picture. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13Revert "fix: surface rb.Poll errors instead of silently dropping them"Paul Buetow
This reverts commit 66956756ba018ed32a455aa57f04517af2c7357f.
2026-05-13fix: surface rb.Poll errors instead of silently dropping themPaul Buetow
The libbpfgo RingBuffer.Poll() previously launched a background goroutine that could fail with a fatal C-level errno (e.g. EBADF, EPERM) and silently exit, causing BPF events to stop flowing with no diagnostic. Changes: - Patch ../libbpfgo (replace directive) so RingBuffer.Poll() returns a buffered <-chan error that the background goroutine sends to on failure. - In setupEventChannel, capture the error channel and spawn a monitoring goroutine that logs any poll error to stderr. - Update go.mod with the replace directive and drop the now-unused remote checksum entries from go.sum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13split globalfilter presentation and parsing into sub-packagesPaul Buetow
Move ParseDurationNs to globalfilter/parser and move CompareOpSymbol, AppendStringSummary, AppendNumericSummary, FilterSummary to globalfilter/presenter. The domain Filter type retains only matching, equality, clone, and active-predicate logic. All callers updated; tests for the new sub-packages added. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13replace global flags singleton with Parse() returning Config valuePaul Buetow
Remove the global atomic.Pointer[Config], sync.Once, and parseErr fields from the flags package. Parse() now returns (Config, error) directly, eliminating all global-state accessors (Get, SetPidFilter, SetTidFilter, SetTUIExportEnable, setCurrent, updateCurrent). The internal parse logic is factored into parseFromFlagSet so tests can inject a fresh FlagSet without touching os.Args or package-level state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13replace package-level test doubles in ior.go with constructor injection (DIP)Paul Buetow
Introduce runnerDeps struct to bundle all injectable function dependencies (getEUID, runTrace, runParquet, runTraceWithContext, runTUI*) that were previously package-level vars overridden in tests. The modeRegistry now carries a runnerDeps instance and passes it to each handler's run() method, eliminating global state mutation in tests. - Add runnerDeps struct and defaultRunnerDeps() constructor in ior_mode_registry.go - Convert modeRegistry from a []modeHandler slice type to a struct with handlers + deps fields; add newModeRegistry(deps) constructor - Update modeHandler.run() signature to accept runnerDeps; handlers call deps.getEUID / deps.runTrace etc. instead of globals - Update SetTUIRunners to write into defaultRegistry.deps instead of package-level vars - Add dispatchRunWithDeps helper for test isolation without global mutation - Remove root-privilege check from runTraceWithContext and runHeadlessParquet; each mode handler owns the EUID gate via deps.getEUID - Rewrite ior_mode_test.go: replace save/restore patterns with stubDeps() helper and dispatchRunWithDeps; add three new root-privilege tests replacing the removed TestRunTraceWithContextRequiresRoot Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13unify filter summary rendering by reusing globalfilter helpersPaul Buetow
Export AppendStringSummary and AppendNumericSummary from globalfilter so filterstack.go can delegate to the canonical token-formatting logic instead of reimplementing fmt.Sprintf("%s~%s") and fmt.Sprintf("%sOP%s") in appendStringFilterChange / appendNumericFilterChange. Drop the now-unused fmt, strconv, and time imports from filterstack.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12extract generic trimLRU helper to eliminate DRY violation in eventloop_state.goPaul Buetow
Replace three near-identical trimOldest* functions with a single generic trimLRU[K comparable, V any] helper; the three wrappers now delegate to it, with trimOldestPendingPairs passing a cleanup callback for Recycle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12add DefaultTopN constant to statsengine and replace hard-coded 64 capacity ↵Paul Buetow
values Introduces statsengine.DefaultTopN = 64 as the canonical named constant for the top-N capacity used when constructing an Engine. Removes the local defaultEngineCapacity constant from runtime_builder.go and updates all call sites to reference statsengine.DefaultTopN instead of bare 64. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12add compile-time interface satisfaction assertions for public typesPaul Buetow
Add var _ Interface = (*ConcreteType)(nil) assertions for: - *flamegraph.LiveTrie → runtime.{Snapshotter,Configurator,LiveTrieSource} and tui/flamegraph.{Snapshotter,Configurator,LiveTrieSource} - *probemanager.Manager → runtime.ProbeManager - *statsengine.Engine → runtime.SnapshotSource - *streamrow.RingBuffer → runtime.EventSink - *runtimeBindings (tui) → runtime.TraceRuntimeBindings - *lateBoundDashboardSource → dashboard.SnapshotSource - libbpfTracepointProgram/Module → probemanager.{Program,Attacher} Assertions are grouped close to their interface definitions to avoid introducing new import cycles (runtime already imports all affected packages; tui/flamegraph already imports coreflamegraph). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12rename ShouldIAttachTracepoint receiver from flags to fPaul Buetow
The receiver name flags shadowed the package name flags, making the package inaccessible inside the method body. Rename to f to match all other receivers in flags.go and add a doc comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12add Go doc comments to exported fields on flags.Config and globalfilter.FilterPaul Buetow
Document every exported field in Config (flags package) and Filter, NumericFilter, StringFilter (globalfilter package) with a concise one-line doc comment describing its purpose and behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12split LiveTrieSource into Snapshotter and Configurator (ISP)Paul Buetow
Applies the Interface Segregation Principle to LiveTrieSource in both internal/runtime and internal/tui/flamegraph. The monolithic interface is replaced by two focused sub-interfaces — Snapshotter (read-only: Version, SnapshotJSON, SnapshotTree) and Configurator (mutating: Fields, CountField, Reconfigure, SetCountField, Reset) — which LiveTrieSource then embeds so all existing consumers continue to compile unchanged. The buildSnapshotMsg helper is narrowed to Snapshotter since it only reads snapshot data, preventing accidental mutation from the background goroutine. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12extract dashboard tab framework into Tab registry for OCP compliancePaul Buetow
Introduce tabDescriptor struct and tabDescriptors map in new tabregistry.go. Each tab registers its name, short name, ordered position, allowed viz modes, render function, scroll handler, and optional init tick command. Adding a new tab now requires only a single registry entry — no existing switch/if chains need editing. Key changes: - Tab.String() and tabLabel() use lookupTab() instead of a switch - renderActiveTabContent() dispatches via d.Render (replaces renderActiveTab switch) - handleScrollKey() dispatches via d.HandleScroll (replaces tab switch) - Init() and postKeyTransitionCmd() use d.InitCmd (replaces stream/flame tab checks) - allowedVizModes() delegates to tabAllowedVizModes() from registry - orderedTabs() replaces hardcoded allTabs slice, derived from Position field - bubbleChartFor() helper eliminates 5 repeated switch-on-tab for chart ops - toggleBubbleMetric, tickActiveBubbleChart, moveBubbleSelection, activeBubbleChartHasNodes all use bubbleChartFor() - refreshBubbleData split into two focused functions under 50 lines - Two pre-existing test functions over 50 lines refactored All tests pass; go build ./internal/tui/... clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12refactor dispatchRun/validateRunConfig into ModeRegistry for OCP compliancePaul Buetow
Introduce a ModeRegistry pattern so new execution modes can be added without modifying the dispatch or validation switch/if chains. Each mode is a self-contained modeHandler (match + validate + run); the ordered defaultRegistry replaces the open if-chains in dispatchRun and validateRunConfig, which become thin wrappers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12extract TUI Model god class into focused sub-controllersPaul Buetow
Split the 1389-line tui.go Model into three focused sub-controllers that each own a single concern: - filterstack.go (filterStack): owns the filter chain, undo history, and label stack; provides push/pop/rebindProcessFilters API so the Model never manipulates filter slices directly. - tracelifecycle.go (traceLifecycle): owns trace start/stop and the active context.CancelFunc; provides beginCmd/stop API; also houses the recorder helpers (recorderStart/Stop/Active/Status) and the auto-reset cycle logic (nextAutoResetInterval, autoResetCycle). - screenrouter.go (screenRouter): owns the picker-return bookmark (pickerReturn) and the applyWindowSizeToPicker helper so screen transition code in tui.go delegates to it. The Model.Update switch is split into dispatchTypedMsg (framework messages) and dispatchAppMsg (app messages) to keep each helper under the 50-line limit. View is split into viewPickerScreen and viewDashboardScreen for the same reason. All functions are ≤50 lines. go test ./internal/tui/... passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12introduce RuntimeBuilder to separate construction from orchestration in ior.goPaul Buetow
RuntimeBuilder encapsulates allocation of per-trace-session components (statsengine.Engine, streamrow.RingBuffer, Sequencer, flamegraph.LiveTrie); buildTUIRuntime, buildTestFlamesRuntime, and buildTestLiveFlamesRuntime now delegate construction to RuntimeBuilder.Build() and focus only on wiring. wireRuntimeBindings is extracted to isolate the context-binding wiring step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12refactor flamegraph TUI model into focused sub-controllersPaul Buetow
Break the god-class Model in internal/tui/flamegraph/model.go into four focused sub-controllers that each own a single concern: - ZoomNavigator : zoom path, stack, root node, and reset/undo logic - SelectionManager: selected frame index, clamp, traversal, navigation - FrameAnimator : spring animation, frame layout swap, ancestry index - SearchController: search query, match indices, filter-visible set All four are embedded in Model so existing field access (m.selectedIdx, m.zoomPath, etc.) is promoted unchanged, keeping tests and callers intact. Model methods now delegate to the sub-controllers rather than holding all logic inline. Also split RenderTerminalView (88 lines) into computeRenderParams, buildToolbar, buildFilteredStatus, and buildNormalStatus helpers, and extracted buildSnapshotMsg from RefreshFromLiveTrieCmd. All functions are now ≤ 50 lines. All tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12invert dependency: internal no longer imports internal/tuiPaul Buetow
Introduce internal/runtime as a neutral contract package that both the core engine (internal) and the TUI layer (internal/tui) depend on. - internal/runtime: defines TraceStarter, StreamSource, EventSink, LiveTrieSource, SnapshotSource, ProbeManager, RuntimePublisher, RuntimeState, TraceRuntimeBindings, and all context key/helper functions (RuntimeBindingsFromContext, RuntimePublisherFromContext, ContextWithRuntimeBindings, ContextWithTraceFilters, TraceFiltersFromContext). These were previously defined in internal/tui, forcing the core package to import the TUI layer. - internal/streamrow: add RingBuffer (moved from internal/tui/eventstream) so the core tracing engine can use the ring buffer without importing the TUI layer. - internal/tui/eventstream/ringbuffer.go: change to a thin re-export of streamrow.RingBuffer via a type alias, preserving the existing API for all callers within the TUI layer. - internal/tui/tui.go: replace locally-defined interface declarations (TraceStarter, SnapshotSource, ProbeManager, RuntimePublisher, RuntimeState, TraceRuntimeBindings) with type aliases from internal/runtime. Delegate all context helper functions to runtime. - internal/ior.go, internal/ior_bpfsetup.go: remove imports of internal/tui and internal/tui/eventstream; use internal/runtime and internal/streamrow instead. Add SetTUIRunners injection point so the concrete TUI runner functions are wired in by cmd/ior/main.go. - cmd/ior/main.go: call internal.SetTUIRunners with the concrete TUI runner functions before internal.Run, completing the wiring without creating a cycle. - Test files (internal/ior_mode_test.go, internal/bench_pipeline_test.go): updated to use runtime.* and streamrow.* types in place of tui.* and eventstream.* types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12refactor renderActiveContent to stay under 50-line limitPaul Buetow
Extract renderActiveContentViz (treemap/icicle/bubble dispatch) and renderActiveContentTable (table-with-sort dispatch) from the original 56-line renderActiveContent, reducing it to 15 lines of code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>