diff options
| -rw-r--r-- | internal/ior.go | 12 | ||||
| -rw-r--r-- | internal/tui/tui.go | 128 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 24 |
3 files changed, 89 insertions, 75 deletions
diff --git a/internal/ior.go b/internal/ior.go index c43ad45..39d09b5 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -170,8 +170,10 @@ func tuiTraceStarterFromRunTrace( engine := statsengine.NewEngine(64) streamBuf := eventstream.NewRingBuffer() - tui.SetDashboardSnapshotSource(engine) - tui.SetEventStreamSource(streamBuf) + if bindings, ok := tui.RuntimeBindingsFromContext(ctx); ok { + bindings.SetDashboardSnapshotSource(engine) + bindings.SetEventStreamSource(streamBuf) + } streamEvents := make(chan eventstream.StreamEvent, 4096) go func() { @@ -271,8 +273,10 @@ func runTraceWithContext(parentCtx context.Context, started chan<- struct{}, con if err := mgr.AttachAll(cfg.ShouldIAttachTracepoint, tracepoints.List); err != nil { return err } - tui.SetProbeManager(mgr) - defer tui.SetProbeManager(nil) + if bindings, ok := tui.RuntimeBindingsFromContext(parentCtx); ok { + bindings.SetProbeManager(mgr) + defer bindings.SetProbeManager(nil) + } // 4096 channel size, minimises event drops ch := make(chan []byte, 4096) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 10e7988..bdd3ab5 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -40,7 +40,8 @@ const ( // Long-lived tracing work should continue in background goroutines. type TraceStarter func(context.Context) error -type snapshotSource interface { +// SnapshotSource provides dashboard snapshots for TUI rendering. +type SnapshotSource interface { Snapshot() *statsengine.Snapshot } @@ -51,62 +52,66 @@ type ProbeManager interface { ActiveCount() (int, int) } -var dashboardSourceState struct { - mu sync.RWMutex - source snapshotSource +// TraceRuntimeBindings allows a trace starter to publish runtime dependencies +// (snapshot source, stream source, probe manager) into the active TUI model. +type TraceRuntimeBindings interface { + SetDashboardSnapshotSource(source SnapshotSource) + SetEventStreamSource(source *eventstream.RingBuffer) + SetProbeManager(manager ProbeManager) } -var eventStreamSourceState struct { - mu sync.RWMutex - source *eventstream.RingBuffer +type runtimeBindingsContextKey struct{} + +type runtimeBindings struct { + mu sync.RWMutex + + snapshotSource SnapshotSource + streamSource *eventstream.RingBuffer + probeManager ProbeManager } -var probeManagerState struct { - mu sync.RWMutex - manager ProbeManager +func newRuntimeBindings() *runtimeBindings { + return &runtimeBindings{} } -// SetDashboardSnapshotSource sets the snapshot source used by dashboard mode. -func SetDashboardSnapshotSource(source snapshotSource) { - dashboardSourceState.mu.Lock() - dashboardSourceState.source = source - dashboardSourceState.mu.Unlock() +func (r *runtimeBindings) SetDashboardSnapshotSource(source SnapshotSource) { + r.mu.Lock() + r.snapshotSource = source + r.mu.Unlock() } -// SetEventStreamSource sets the event stream source used by dashboard mode. -func SetEventStreamSource(source *eventstream.RingBuffer) { - eventStreamSourceState.mu.Lock() - eventStreamSourceState.source = source - eventStreamSourceState.mu.Unlock() +func (r *runtimeBindings) SetEventStreamSource(source *eventstream.RingBuffer) { + r.mu.Lock() + r.streamSource = source + r.mu.Unlock() } -// SetProbeManager sets the probe manager used by TUI probe controls. -func SetProbeManager(manager ProbeManager) { - probeManagerState.mu.Lock() - probeManagerState.manager = manager - probeManagerState.mu.Unlock() +func (r *runtimeBindings) SetProbeManager(manager ProbeManager) { + r.mu.Lock() + r.probeManager = manager + r.mu.Unlock() } -func getDashboardSnapshotSource() snapshotSource { - dashboardSourceState.mu.RLock() - defer dashboardSourceState.mu.RUnlock() - return dashboardSourceState.source +func (r *runtimeBindings) dashboardSnapshotSource() SnapshotSource { + r.mu.RLock() + defer r.mu.RUnlock() + return r.snapshotSource } -func getEventStreamSource() *eventstream.RingBuffer { - eventStreamSourceState.mu.RLock() - defer eventStreamSourceState.mu.RUnlock() - return eventStreamSourceState.source +func (r *runtimeBindings) eventStreamSource() *eventstream.RingBuffer { + r.mu.RLock() + defer r.mu.RUnlock() + return r.streamSource } -func getProbeManager() ProbeManager { - probeManagerState.mu.RLock() - defer probeManagerState.mu.RUnlock() - return probeManagerState.manager +func (r *runtimeBindings) currentProbeManager() ProbeManager { + r.mu.RLock() + defer r.mu.RUnlock() + return r.probeManager } -func resetDashboardSnapshotSource() *statsengine.Snapshot { - src := getDashboardSnapshotSource() +func (r *runtimeBindings) resetDashboardSnapshotSource() *statsengine.Snapshot { + src := r.dashboardSnapshotSource() if src == nil { return nil } @@ -120,6 +125,16 @@ func resetDashboardSnapshotSource() *statsengine.Snapshot { return nil } +// RuntimeBindingsFromContext returns model-scoped trace bindings when the +// context was created by the TUI. +func RuntimeBindingsFromContext(ctx context.Context) (TraceRuntimeBindings, bool) { + bindings, ok := ctx.Value(runtimeBindingsContextKey{}).(*runtimeBindings) + if !ok || bindings == nil { + return nil, false + } + return bindings, true +} + // Run starts the TUI program in alternate screen mode. func Run() error { return RunWithTraceStarter(defaultTraceStarter) @@ -141,6 +156,7 @@ type Model struct { dashboard dashboardui.Model exporter tuiexport.Model probeModal probes.Model + runtime *runtimeBindings keys KeyMap @@ -176,7 +192,8 @@ func newModelWithRuntimeConfig(initialPID, startupPidFilter int, exportEnabled b keys.Export = key.NewBinding() } - dashboard := dashboardui.NewModelWithConfig(lateBoundDashboardSource{}, getEventStreamSource(), 1000, keys) + runtime := newRuntimeBindings() + dashboard := dashboardui.NewModelWithConfig(lateBoundDashboardSource{runtime: runtime}, runtime.eventStreamSource(), 1000, keys) pidFilter := selectedPIDFilter(startupPidFilter) if initialPID > 0 { pidFilter = selectedPIDFilter(initialPID) @@ -188,7 +205,8 @@ func newModelWithRuntimeConfig(initialPID, startupPidFilter int, exportEnabled b pidPicker: pidpicker.New(), dashboard: dashboard, exporter: tuiexport.NewModel(), - probeModal: probes.NewModel(getProbeManager()), + probeModal: probes.NewModel(runtime.currentProbeManager()), + runtime: runtime, keys: keys, spin: spin, startTrace: startTrace, @@ -239,7 +257,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Probes) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { - m.probeModal = probes.NewModel(getProbeManager()).Open() + m.probeModal = probes.NewModel(m.runtime.currentProbeManager()).Open() return m, nil } if m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.SelectPID) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { @@ -261,7 +279,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case probes.ProbeToggledMsg: var cmd tea.Cmd m.probeModal, cmd = m.probeModal.Update(msg) - if snap := resetDashboardSnapshotSource(); snap != nil { + if snap := m.runtime.resetDashboardSnapshotSource(); snap != nil { next, dashboardCmd := m.dashboard.Update(messages.StatsTickMsg{Snap: snap}) m.dashboard = next.(dashboardui.Model) return m, tea.Batch(dashboardCmd, cmd) @@ -273,7 +291,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleTidSelected(msg) case TracingStartedMsg: m.attaching = false - m.dashboard.SetStreamSource(getEventStreamSource()) + m.dashboard.SetStreamSource(m.runtime.eventStreamSource()) return m, m.dashboard.Init() case TracingErrorMsg: m.attaching = false @@ -365,7 +383,7 @@ func (m Model) reselectPID() (tea.Model, tea.Cmd) { m.attaching = false m.lastErr = nil m.exporter = tuiexport.NewModel() - m.probeModal = probes.NewModel(getProbeManager()) + m.probeModal = probes.NewModel(m.runtime.currentProbeManager()) m.pidPicker = pidpicker.New() var sizeCmd tea.Cmd @@ -386,7 +404,7 @@ func (m Model) reselectTID() (tea.Model, tea.Cmd) { m.attaching = false m.lastErr = nil m.exporter = tuiexport.NewModel() - m.probeModal = probes.NewModel(getProbeManager()) + m.probeModal = probes.NewModel(m.runtime.currentProbeManager()) m.pidPicker = pidpicker.NewTIDWithKeys(pid, pidpicker.DefaultKeyMap()) var sizeCmd tea.Cmd @@ -409,6 +427,7 @@ func selectedPIDFilter(pid int) int { func (m *Model) beginTraceCmd() tea.Cmd { ctx, cancel := context.WithCancel(context.Background()) m.traceStop = cancel + ctx = context.WithValue(ctx, runtimeBindingsContextKey{}, m.runtime) return startTraceCmd(m.startTrace, ctx) } @@ -491,22 +510,21 @@ func runExportCmd(exportEnabled bool, option tuiexport.Option, snap *statsengine } } -type lateBoundDashboardSource struct{} +type lateBoundDashboardSource struct { + runtime *runtimeBindings +} -func (lateBoundDashboardSource) Snapshot() *statsengine.Snapshot { - source := getDashboardSnapshotSource() +func (s lateBoundDashboardSource) Snapshot() *statsengine.Snapshot { + if s.runtime == nil { + return nil + } + source := s.runtime.dashboardSnapshotSource() if source == nil { return nil } return source.Snapshot() } -type lateBoundEventStreamSource struct{} - -func (lateBoundEventStreamSource) Source() *eventstream.RingBuffer { - return getEventStreamSource() -} - func exportSnapshotCSV(snap *statsengine.Snapshot) (string, error) { filename := fmt.Sprintf("ior-snapshot-%s.csv", time.Now().Format("20060102-150405")) f, err := os.Create(filename) diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index ed361a6..890dfc4 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -206,27 +206,23 @@ func (f *fakeResettableDashboardSource) Reset() { } func TestDashboardRefreshPicksLateBoundSource(t *testing.T) { - orig := getDashboardSnapshotSource() - defer SetDashboardSnapshotSource(orig) - - SetDashboardSnapshotSource(nil) - source := lateBoundDashboardSource{} + runtime := newRuntimeBindings() + source := lateBoundDashboardSource{runtime: runtime} want := &statsengine.Snapshot{TotalSyscalls: 77} - SetDashboardSnapshotSource(fakeDashboardSource{snap: want}) + runtime.SetDashboardSnapshotSource(fakeDashboardSource{snap: want}) got := source.Snapshot() if got != want { - t.Fatalf("expected late-bound source to use latest global source") + t.Fatalf("expected late-bound source to use latest runtime source") } } func TestProbeToggledMsgResetsDashboardStatsSource(t *testing.T) { src := &fakeResettableDashboardSource{snap: &statsengine.Snapshot{TotalSyscalls: 99}} - SetDashboardSnapshotSource(src) - t.Cleanup(func() { SetDashboardSnapshotSource(nil) }) m := NewModel(-1, func(context.Context) error { return nil }) + m.runtime.SetDashboardSnapshotSource(src) m.screen = ScreenDashboard m.attaching = false m.probeModal = probes.NewModel(fakeProbeManager{states: []probemanager.ProbeState{{Syscall: "read", Active: true}}}).Open() @@ -244,14 +240,11 @@ func TestProbeToggledMsgResetsDashboardStatsSource(t *testing.T) { } func TestTracingStartedRebindsEventStreamSource(t *testing.T) { - orig := getEventStreamSource() - defer SetEventStreamSource(orig) - rb := eventstream.NewRingBuffer() rb.Push(eventstream.StreamEvent{Seq: 1, Syscall: "read", Comm: "proc", PID: 1, TID: 1}) - SetEventStreamSource(rb) m := NewModel(-1, func(context.Context) error { return nil }) + m.runtime.SetEventStreamSource(rb) m.screen = ScreenDashboard m.attaching = true @@ -596,10 +589,9 @@ func TestDashboardTabKeysChangeActiveView(t *testing.T) { } func TestProbeModalViewDoesNotStackDashboardContent(t *testing.T) { - SetProbeManager(fakeProbeManager{states: []probemanager.ProbeState{{Syscall: "read", Active: true}}}) - t.Cleanup(func() { SetProbeManager(nil) }) - m := NewModel(-1, func(context.Context) error { return nil }) + m.runtime.SetProbeManager(fakeProbeManager{states: []probemanager.ProbeState{{Syscall: "read", Active: true}}}) + m.probeModal = probes.NewModel(m.runtime.currentProbeManager()) m.screen = ScreenDashboard m.attaching = false m.width = 120 |
