summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-08 20:33:41 +0200
committerPaul Buetow <paul@buetow.org>2026-03-08 20:33:41 +0200
commit4ca02bb88cffb28bb000326688c6e8e7c1cbe8a9 (patch)
tree4c74aaaaa2c96c3a694a09c511836e485e3eeffc /internal/tui
parent8236891a2c3a774a3eee2401980c10080aa85da6 (diff)
task 372: restart tracing when filters change
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/dashboard/model.go21
-rw-r--r--internal/tui/eventstream/model.go9
-rw-r--r--internal/tui/tui.go23
-rw-r--r--internal/tui/tui_test.go68
4 files changed, 118 insertions, 3 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index a7d415d..5c3f690 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -4,6 +4,7 @@ import (
"strings"
"time"
+ "ior/internal/globalfilter"
"ior/internal/statsengine"
common "ior/internal/tui/common"
"ior/internal/tui/eventstream"
@@ -512,6 +513,11 @@ func (m Model) LatestSnapshot() *statsengine.Snapshot {
return m.latest
}
+// ActiveTab returns the currently selected dashboard tab.
+func (m Model) ActiveTab() Tab {
+ return m.activeTab
+}
+
// BlocksGlobalShortcuts reports whether the active tab should suppress a
// top-level shortcut for the given key press.
func (m Model) BlocksGlobalShortcuts(msg tea.KeyPressMsg) bool {
@@ -529,6 +535,12 @@ func (m *Model) SetStreamSource(source eventstream.Source) {
m.streamModel.SetSource(source)
}
+// SetGlobalFilter forwards the shared TUI filter into the stream tab so
+// buffered rows can be re-filtered immediately.
+func (m *Model) SetGlobalFilter(filter globalfilter.Filter) {
+ m.streamModel.SetFilter(eventstream.Filter(filter))
+}
+
// SetLiveTrie updates the live trie source used by the flamegraph tab.
func (m *Model) SetLiveTrie(liveTrie flamegraphtui.LiveTrieSource) {
m.liveTrie = liveTrie
@@ -539,6 +551,15 @@ func (m *Model) SetLiveTrie(liveTrie flamegraphtui.LiveTrieSource) {
m.flamegraphModel.RefreshFromLiveTrie()
}
+// PrepareForTraceRestart clears aggregate state while keeping the current tab
+// and retained stream rows intact for the next trace session.
+func (m *Model) PrepareForTraceRestart() {
+ m.latest = nil
+ m.liveTrie = nil
+ m.flamegraphModel.SetLiveTrie(nil)
+ m.refreshBubbleData()
+}
+
// SetDarkMode updates dashboard child models for the active theme.
func (m *Model) SetDarkMode(isDark bool) {
m.isDark = isDark
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go
index ee65793..3dc774b 100644
--- a/internal/tui/eventstream/model.go
+++ b/internal/tui/eventstream/model.go
@@ -133,6 +133,15 @@ func (m *Model) SetSource(source Source) {
m.Refresh()
}
+// SetFilter updates the active stream filter and immediately re-filters the
+// current in-memory snapshot without mutating the underlying ring buffer.
+func (m *Model) SetFilter(filter Filter) {
+ targetSeq := m.currentSelectedSeq()
+ m.filter = filter.Clone()
+ m.applyFilter()
+ m.restoreSelectionBySeq(targetSeq)
+}
+
// SetDarkMode updates stream modal text input styles for the active theme.
func (m *Model) SetDarkMode(isDark bool) {
m.isDark = isDark
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 991a9d6..db24538 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -400,10 +400,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.attaching = false
m.dashboard.SetStreamSource(m.runtime.eventStreamSource())
m.dashboard.SetLiveTrie(m.runtime.liveTrie())
+ m.dashboard.SetGlobalFilter(m.globalFilter)
width, height := common.EffectiveViewport(m.width, m.height)
next, sizeCmd := m.dashboard.Update(tea.WindowSizeMsg{Width: width, Height: height})
m.dashboard = next.(dashboardui.Model)
- return m, tea.Batch(sizeCmd, m.dashboard.Init())
+ return m, tea.Batch(sizeCmd, m.dashboard.Init(), m.dashboard.SnapshotCmd())
case TracingErrorMsg:
m.attaching = false
m.lastErr = msg.Err
@@ -540,7 +541,8 @@ func (m Model) updateFilterModal(msg tea.Msg) (tea.Model, tea.Cmd) {
wasVisible := m.filterModal.Visible()
m.filterModal = m.filterModal.Update(msg)
if wasVisible && !m.filterModal.Visible() {
- m.setGlobalFilter(m.filterModal.Filter())
+ next, cmd := m.applyGlobalFilter(m.filterModal.Filter())
+ return next, tea.Batch(dashboardCmd, cmd)
}
return m, dashboardCmd
}
@@ -831,6 +833,7 @@ func (m *Model) setProcessFilters(pid, tid int) {
m.globalFilter.PID = eqNumericFilter(pid)
m.globalFilter.TID = eqNumericFilter(tid)
m.dashboard.SetPidFilter(pid)
+ m.dashboard.SetGlobalFilter(m.globalFilter)
}
func (m *Model) setGlobalFilter(filter globalfilter.Filter) {
@@ -840,6 +843,22 @@ func (m *Model) setGlobalFilter(filter globalfilter.Filter) {
m.pidFilter = pid
m.tidFilter = tid
m.dashboard.SetPidFilter(pid)
+ m.dashboard.SetGlobalFilter(m.globalFilter)
+}
+
+func (m Model) applyGlobalFilter(filter globalfilter.Filter) (tea.Model, tea.Cmd) {
+ nextFilter := filter.Clone()
+ changed := !m.globalFilter.Equal(nextFilter)
+ m.setGlobalFilter(nextFilter)
+ if !changed || m.screen != ScreenDashboard {
+ return m, nil
+ }
+
+ m.stopTrace()
+ m.dashboard.PrepareForTraceRestart()
+ m.attaching = true
+ m.lastErr = nil
+ return m, tea.Batch(m.spin.Tick, m.beginTraceCmd())
}
func eqNumericFilterValue(filter *globalfilter.NumericFilter) (int, bool) {
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index faacdc3..a5cd16a 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -452,7 +452,7 @@ func TestTracingStartedRebindsEventStreamSource(t *testing.T) {
rb := eventstream.NewRingBuffer()
rb.Push(eventstream.StreamEvent{Seq: 1, Syscall: "read", Comm: "proc", PID: 1, TID: 1})
- m := NewModel(-1, func(context.Context) error { return nil })
+ m := NewModelWithConfig(flags.Config{PidFilter: -1, TidFilter: -1, TUIExportEnable: true}, -1, func(context.Context) error { return nil })
m.runtime.SetEventStreamSource(rb)
m.screen = ScreenDashboard
m.attaching = true
@@ -1036,6 +1036,10 @@ func TestQuitClosesGlobalFilterModalWithoutQuitting(t *testing.T) {
func TestGlobalFilterModalUpdatesStoredFilterState(t *testing.T) {
m := NewModel(-1, func(context.Context) error { return nil })
m.screen = ScreenDashboard
+ m.attaching = false
+
+ stopped := false
+ m.traceStop = func() { stopped = true }
next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: string([]rune{'f'})})
m = next.(Model)
@@ -1056,6 +1060,68 @@ func TestGlobalFilterModalUpdatesStoredFilterState(t *testing.T) {
if m.globalFilter.Syscall == nil || m.globalFilter.Syscall.Pattern != "read" {
t.Fatalf("expected stored global filter updated from modal, got %+v", m.globalFilter.Syscall)
}
+ if !stopped {
+ t.Fatalf("expected filter apply to stop the active trace")
+ }
+ if !m.attaching {
+ t.Fatalf("expected filter apply to restart tracing")
+ }
+}
+
+func TestGlobalFilterCloseWithoutChangesDoesNotRestartTrace(t *testing.T) {
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+
+ stopped := false
+ m.traceStop = func() { stopped = true }
+
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: string([]rune{'f'})})
+ m = next.(Model)
+ if !m.filterModal.Visible() {
+ t.Fatalf("expected filter modal to open")
+ }
+
+ next, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
+ m = next.(Model)
+
+ if cmd != nil {
+ t.Fatalf("expected no restart command when filter is unchanged")
+ }
+ if stopped {
+ t.Fatalf("expected unchanged filter close not to stop tracing")
+ }
+ if m.attaching {
+ t.Fatalf("expected unchanged filter close not to restart tracing")
+ }
+}
+
+func TestGlobalFilterApplyPreservesActiveDashboardTab(t *testing.T) {
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'4'}[0], Text: string([]rune{'4'})})
+ m = next.(Model)
+ if m.dashboard.ActiveTab() != dashboardui.TabFiles {
+ t.Fatalf("expected files tab active before filter apply")
+ }
+
+ next, _ = m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: string([]rune{'f'})})
+ m = next.(Model)
+ next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ m = next.(Model)
+ next, _ = m.Update(tea.KeyPressMsg{Code: []rune("log")[0], Text: string([]rune("log"))})
+ m = next.(Model)
+ next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
+ m = next.(Model)
+
+ if m.dashboard.ActiveTab() != dashboardui.TabFiles {
+ t.Fatalf("expected active tab preserved across filter restart")
+ }
+ if !m.attaching {
+ t.Fatalf("expected apply to enter attaching state")
+ }
}
func TestQuestionMarkDoesNotBlockUnderlyingActions(t *testing.T) {