summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-02 07:39:13 +0200
committerPaul Buetow <paul@buetow.org>2026-03-02 07:39:13 +0200
commit73652016579518ddc3524d42b345e7c566a7f5fc (patch)
treef8d23ff5088802452e1f304d3be053124611f2d3 /internal
parent52cc7dc6ccdf2d97ea49d361299701cf6b76e898 (diff)
Replace TUI service-locator globals with runtime bindings
Diffstat (limited to 'internal')
-rw-r--r--internal/ior.go12
-rw-r--r--internal/tui/tui.go128
-rw-r--r--internal/tui/tui_test.go24
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