summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-24 08:45:38 +0200
committerPaul Buetow <paul@buetow.org>2026-02-24 08:45:38 +0200
commite5116514c33b2fce6ce2fc7904d6720c5236221f (patch)
treee5bc8c9ba37836305ec1d095573535fb8344cbe7
parentb01e24374398eb3d343e9472f3262668039db56c (diff)
tui: wire eventloop stats engine into dashboard snapshots
-rw-r--r--internal/ior.go23
-rw-r--r--internal/ior_mode_test.go21
-rw-r--r--internal/tui/tui.go62
-rw-r--r--internal/tui/tui_test.go25
4 files changed, 115 insertions, 16 deletions
diff --git a/internal/ior.go b/internal/ior.go
index 011d2fb..1d67892 100644
--- a/internal/ior.go
+++ b/internal/ior.go
@@ -11,8 +11,10 @@ import (
"syscall"
"time"
+ "ior/internal/event"
"ior/internal/flags"
"ior/internal/flamegraph"
+ "ior/internal/statsengine"
"ior/internal/tracepoints"
"ior/internal/tui"
@@ -122,13 +124,23 @@ func shouldRunTraceMode(cfg flags.Flags) bool {
return cfg.PlainMode || cfg.FlamegraphEnable || cfg.PprofEnable
}
-func tuiTraceStarterFromRunTrace(startTrace func(context.Context, chan<- struct{}) error) tui.TraceStarter {
+func tuiTraceStarterFromRunTrace(
+ startTrace func(context.Context, chan<- struct{}, func(*eventLoop)) error,
+) tui.TraceStarter {
return func(ctx context.Context) error {
+ engine := statsengine.NewEngine(64)
+ tui.SetDashboardSnapshotSource(engine)
+
startedCh := make(chan struct{})
errCh := make(chan error, 1)
go func() {
- errCh <- startTrace(ctx, startedCh)
+ errCh <- startTrace(ctx, startedCh, func(el *eventLoop) {
+ el.printCb = func(ep *event.Pair) {
+ engine.Ingest(ep)
+ ep.Recycle()
+ }
+ })
close(errCh)
}()
@@ -144,10 +156,10 @@ func tuiTraceStarterFromRunTrace(startTrace func(context.Context, chan<- struct{
}
func runTrace() error {
- return runTraceWithContext(context.Background(), nil)
+ return runTraceWithContext(context.Background(), nil, nil)
}
-func runTraceWithContext(parentCtx context.Context, started chan<- struct{}) error {
+func runTraceWithContext(parentCtx context.Context, started chan<- struct{}, configure func(*eventLoop)) error {
bpfModule, err := bpf.NewModuleFromFile("ior.bpf.o")
if err != nil {
return err
@@ -195,6 +207,9 @@ func runTraceWithContext(parentCtx context.Context, started chan<- struct{}) err
signalTraceStarted(started)
el := newEventLoop()
+ if configure != nil {
+ configure(el)
+ }
duration := time.Duration(flags.Get().Duration) * time.Second
fmt.Println("Probing for", duration)
ctx, cancel := context.WithTimeout(parentCtx, duration)
diff --git a/internal/ior_mode_test.go b/internal/ior_mode_test.go
index ddc915b..84ff651 100644
--- a/internal/ior_mode_test.go
+++ b/internal/ior_mode_test.go
@@ -76,7 +76,10 @@ func TestDispatchRunUsesTUIStarterWhenNotPlain(t *testing.T) {
}()
traceDone := make(chan struct{}, 1)
- runTraceWithContextFn = func(_ context.Context, started chan<- struct{}) error {
+ runTraceWithContextFn = func(_ context.Context, started chan<- struct{}, configure func(*eventLoop)) error {
+ if configure != nil {
+ configure(&eventLoop{})
+ }
close(started)
traceDone <- struct{}{}
return nil
@@ -110,9 +113,9 @@ func TestDispatchRunUsesTUIStarterWhenNotPlain(t *testing.T) {
}
func TestTuiTraceStarterFromRunTracePropagatesError(t *testing.T) {
- starter := tuiTraceStarterFromRunTrace(func(context.Context, chan<- struct{}) error {
- return errors.New("startup failed")
- })
+ starter := tuiTraceStarterFromRunTrace(
+ func(context.Context, chan<- struct{}, func(*eventLoop)) error { return errors.New("startup failed") },
+ )
err := starter(context.Background())
if err == nil || err.Error() != "startup failed" {
@@ -121,10 +124,12 @@ func TestTuiTraceStarterFromRunTracePropagatesError(t *testing.T) {
}
func TestTuiTraceStarterFromRunTraceRespectsCancel(t *testing.T) {
- starter := tuiTraceStarterFromRunTrace(func(ctx context.Context, _ chan<- struct{}) error {
- <-ctx.Done()
- return ctx.Err()
- })
+ starter := tuiTraceStarterFromRunTrace(
+ func(ctx context.Context, _ chan<- struct{}, _ func(*eventLoop)) error {
+ <-ctx.Done()
+ return ctx.Err()
+ },
+ )
ctx, cancel := context.WithCancel(context.Background())
cancel()
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 49d2365..b54875a 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -5,7 +5,10 @@ import (
"errors"
"fmt"
"ior/internal/flags"
+ "ior/internal/statsengine"
"ior/internal/tui/pidpicker"
+ "sync"
+ "time"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
@@ -26,6 +29,30 @@ const (
// Long-lived tracing work should continue in background goroutines.
type TraceStarter func(context.Context) error
+type snapshotSource interface {
+ Snapshot() *statsengine.Snapshot
+}
+
+type dashboardTickMsg struct{}
+
+var dashboardSourceState struct {
+ mu sync.RWMutex
+ source snapshotSource
+}
+
+// SetDashboardSnapshotSource sets the snapshot source used by dashboard mode.
+func SetDashboardSnapshotSource(source snapshotSource) {
+ dashboardSourceState.mu.Lock()
+ dashboardSourceState.source = source
+ dashboardSourceState.mu.Unlock()
+}
+
+func getDashboardSnapshotSource() snapshotSource {
+ dashboardSourceState.mu.RLock()
+ defer dashboardSourceState.mu.RUnlock()
+ return dashboardSourceState.source
+}
+
// Run starts the TUI program in alternate screen mode.
func Run() error {
return RunWithTraceStarter(defaultTraceStarter)
@@ -70,7 +97,7 @@ func NewModel(initialPID int, startTrace TraceStarter) Model {
model := Model{
screen: ScreenPIDPicker,
pidPicker: pidpicker.New(),
- dashboard: newDashboardModel(),
+ dashboard: newDashboardModel(getDashboardSnapshotSource()),
keys: Keys,
spin: spin,
startTrace: startTrace,
@@ -111,11 +138,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.handlePidSelected(msg)
case TracingStartedMsg:
m.attaching = false
- return m, nil
+ return m, dashboardTickCmd()
case TracingErrorMsg:
m.attaching = false
m.lastErr = msg.Err
return m, nil
+ case dashboardTickMsg:
+ m.dashboard.refresh()
+ return m, dashboardTickCmd()
}
if m.attaching {
@@ -216,10 +246,15 @@ func (m Model) View() string {
type dashboardModel struct {
selectedPID int
+ source snapshotSource
+ latest *statsengine.Snapshot
}
-func newDashboardModel() dashboardModel {
- return dashboardModel{selectedPID: -1}
+func newDashboardModel(source snapshotSource) dashboardModel {
+ return dashboardModel{
+ selectedPID: -1,
+ source: source,
+ }
}
func (d dashboardModel) Init() tea.Cmd {
@@ -231,8 +266,27 @@ func (d dashboardModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
}
func (d dashboardModel) View() string {
+ if d.latest != nil {
+ return PanelStyle.Render(
+ fmt.Sprintf("Dashboard (%d syscalls, %.1f/s)", d.latest.TotalSyscalls, d.latest.SyscallRatePerSec),
+ )
+ }
if d.selectedPID > 0 {
return PanelStyle.Render(fmt.Sprintf("Dashboard (PID %d)", d.selectedPID))
}
return PanelStyle.Render("Dashboard (All PIDs)")
}
+
+func (d *dashboardModel) refresh() {
+ if source := getDashboardSnapshotSource(); source != nil {
+ d.source = source
+ }
+ if d.source == nil {
+ return
+ }
+ d.latest = d.source.Snapshot()
+}
+
+func dashboardTickCmd() tea.Cmd {
+ return tea.Tick(time.Second, func(time.Time) tea.Msg { return dashboardTickMsg{} })
+}
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index 3801813..40ea67b 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -3,6 +3,7 @@ package tui
import (
"context"
"errors"
+ "ior/internal/statsengine"
"strings"
"testing"
"time"
@@ -164,3 +165,27 @@ func TestQuitInvokesTraceStop(t *testing.T) {
t.Fatalf("expected stopTrace to be invoked on quit")
}
}
+
+type fakeDashboardSource struct {
+ snap *statsengine.Snapshot
+}
+
+func (f fakeDashboardSource) Snapshot() *statsengine.Snapshot {
+ return f.snap
+}
+
+func TestDashboardRefreshPicksLateBoundSource(t *testing.T) {
+ orig := getDashboardSnapshotSource()
+ defer SetDashboardSnapshotSource(orig)
+
+ SetDashboardSnapshotSource(nil)
+ d := newDashboardModel(nil)
+
+ want := &statsengine.Snapshot{TotalSyscalls: 77}
+ SetDashboardSnapshotSource(fakeDashboardSource{snap: want})
+
+ d.refresh()
+ if d.latest != want {
+ t.Fatalf("expected dashboard refresh to bind and use latest global source")
+ }
+}