diff options
| -rw-r--r-- | internal/ior.go | 3 | ||||
| -rw-r--r-- | internal/tui/eventstream/ringbuffer.go | 10 | ||||
| -rw-r--r-- | internal/tui/tui.go | 26 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 87 |
4 files changed, 125 insertions, 1 deletions
diff --git a/internal/ior.go b/internal/ior.go index d113fff..9f58821 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -183,6 +183,9 @@ func tuiTraceStarterFromRunTrace( streamBuf := eventstream.NewRingBuffer() liveTrie := flamegraph.NewLiveTrie(cfg.CollapsedFields, cfg.CountField) if bindings, ok := tui.RuntimeBindingsFromContext(ctx); ok { + if persistent := bindings.StreamBuffer(); persistent != nil { + streamBuf = persistent + } bindings.SetDashboardSnapshotSource(engine) bindings.SetEventStreamSource(streamBuf) bindings.SetLiveTrie(liveTrie) diff --git a/internal/tui/eventstream/ringbuffer.go b/internal/tui/eventstream/ringbuffer.go index a2ec1dc..87dacae 100644 --- a/internal/tui/eventstream/ringbuffer.go +++ b/internal/tui/eventstream/ringbuffer.go @@ -57,3 +57,13 @@ func (r *RingBuffer) TotalPushed() uint64 { defer r.mu.RUnlock() return r.totalPushed } + +func (r *RingBuffer) Reset() { + r.mu.Lock() + defer r.mu.Unlock() + + clear(r.buf) + r.start = 0 + r.size = 0 + r.totalPushed = 0 +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index db24538..a1dd8fb 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -63,6 +63,7 @@ type TraceRuntimeBindings interface { SetEventStreamSource(source eventstream.Source) SetLiveTrie(liveTrie flamegraphtui.LiveTrieSource) SetProbeManager(manager ProbeManager) + StreamBuffer() *eventstream.RingBuffer } type runtimeBindingsContextKey struct{} @@ -73,6 +74,7 @@ type runtimeBindings struct { snapshotSource SnapshotSource streamSource eventstream.Source + streamBuffer *eventstream.RingBuffer liveTrieSource flamegraphtui.LiveTrieSource probeManager ProbeManager } @@ -82,7 +84,11 @@ type traceFilters struct { } func newRuntimeBindings() *runtimeBindings { - return &runtimeBindings{} + streamBuffer := eventstream.NewRingBuffer() + return &runtimeBindings{ + streamSource: streamBuffer, + streamBuffer: streamBuffer, + } } func (r *runtimeBindings) SetDashboardSnapshotSource(source SnapshotSource) { @@ -97,6 +103,12 @@ func (r *runtimeBindings) SetEventStreamSource(source eventstream.Source) { r.mu.Unlock() } +func (r *runtimeBindings) StreamBuffer() *eventstream.RingBuffer { + r.mu.RLock() + defer r.mu.RUnlock() + return r.streamBuffer +} + func (r *runtimeBindings) SetLiveTrie(liveTrie flamegraphtui.LiveTrieSource) { r.mu.Lock() r.liveTrieSource = liveTrie @@ -133,6 +145,16 @@ func (r *runtimeBindings) currentProbeManager() ProbeManager { return r.probeManager } +func (r *runtimeBindings) resetStreamBuffer() { + r.mu.Lock() + defer r.mu.Unlock() + if r.streamBuffer == nil { + r.streamBuffer = eventstream.NewRingBuffer() + } + r.streamBuffer.Reset() + r.streamSource = r.streamBuffer +} + func (r *runtimeBindings) resetDashboardSnapshotSource() *statsengine.Snapshot { src := r.dashboardSnapshotSource() if src == nil { @@ -682,6 +704,7 @@ func (m Model) updateActiveModel(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) handlePidSelected(msg PidSelectedMsg) (tea.Model, tea.Cmd) { pid := selectedPIDFilter(msg.Pid) m.stopTrace() + m.runtime.resetStreamBuffer() m.setProcessFilters(pid, -1) m.pickerReturn = nil m.screen = ScreenDashboard @@ -697,6 +720,7 @@ func (m Model) handleTidSelected(msg TidSelectedMsg) (tea.Model, tea.Cmd) { pid = msg.Pid } m.stopTrace() + m.runtime.resetStreamBuffer() m.setProcessFilters(pid, tid) m.pickerReturn = nil m.screen = ScreenDashboard diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index a5cd16a..df1d751 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -427,6 +427,30 @@ func TestRuntimeBindingsStoreAndExposeLiveTrie(t *testing.T) { } } +func TestRuntimeBindingsProvidePersistentStreamBuffer(t *testing.T) { + runtime := newRuntimeBindings() + buffer := runtime.StreamBuffer() + if buffer == nil { + t.Fatalf("expected persistent stream buffer") + } + if got := runtime.eventStreamSource(); got != buffer { + t.Fatalf("expected runtime stream source to default to persistent buffer") + } + + buffer.Push(eventstream.StreamEvent{Seq: 1, Syscall: "read"}) + if buffer.Len() != 1 { + t.Fatalf("expected pushed event in persistent buffer") + } + + runtime.resetStreamBuffer() + if buffer.Len() != 0 { + t.Fatalf("expected resetStreamBuffer to clear existing buffer contents") + } + if got := runtime.eventStreamSource(); got != buffer { + t.Fatalf("expected resetStreamBuffer to preserve the same buffer source") + } +} + func TestProbeToggledMsgResetsDashboardStatsSource(t *testing.T) { src := &fakeResettableDashboardSource{snap: &statsengine.Snapshot{TotalSyscalls: 99}} @@ -472,6 +496,57 @@ func TestTracingStartedRebindsEventStreamSource(t *testing.T) { } } +func TestGlobalFilterApplyPreservesBufferedStreamRowsAcrossRestart(t *testing.T) { + m := NewModelWithConfig(flags.Config{PidFilter: -1, TidFilter: -1, TUIExportEnable: true}, -1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.attaching = false + m.width = 120 + m.height = 30 + + buffer := m.runtime.StreamBuffer() + buffer.Push(eventstream.StreamEvent{Seq: 1, Syscall: "read", Comm: "proc", PID: 1, TID: 1, FileName: "/tmp/read"}) + buffer.Push(eventstream.StreamEvent{Seq: 2, Syscall: "write", Comm: "proc", PID: 1, TID: 2, FileName: "/tmp/write"}) + m.dashboard.SetStreamSource(buffer) + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'7'}[0], Text: string([]rune{'7'})}) + m = next.(Model) + next, _ = m.Update(messages.StatsTickMsg{}) + m = next.(Model) + initial := m.View().Content + if !strings.Contains(initial, "read") || !strings.Contains(initial, "write") { + t.Fatalf("expected initial stream view to show buffered rows, got %q", initial) + } + + 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("read")[0], Text: string([]rune("read"))}) + m = next.(Model) + next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + m = next.(Model) + + if buffer.Len() != 2 { + t.Fatalf("expected filter apply not to clear persistent stream buffer") + } + if !m.attaching { + t.Fatalf("expected filter apply to restart tracing") + } + + next, _ = m.Update(TracingStartedMsg{}) + m = next.(Model) + next, _ = m.Update(messages.StatsTickMsg{}) + m = next.(Model) + + view := m.View().Content + if !strings.Contains(view, "read") { + t.Fatalf("expected matching historical row to remain visible, got %q", view) + } + if strings.Contains(view, "write") { + t.Fatalf("expected non-matching historical row to be hidden after refilter, got %q", view) + } +} + func TestTracingStartedUsesCurrentViewportForFlameNavigationWithoutResize(t *testing.T) { trie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") coreflamegraph.SeedTestFlameData(trie) @@ -742,6 +817,18 @@ func TestSelectPIDKeyReturnsToFreshPickerAndStopsTrace(t *testing.T) { } } +func TestPidSelectedClearsPersistentStreamBuffer(t *testing.T) { + m := NewModelWithConfig(flags.Config{PidFilter: -1, TidFilter: -1, TUIExportEnable: true}, -1, func(context.Context) error { return nil }) + m.runtime.StreamBuffer().Push(eventstream.StreamEvent{Seq: 1, Syscall: "read"}) + + next, _ := m.Update(PidSelectedMsg{Pid: 42}) + m = next.(Model) + + if got := m.runtime.StreamBuffer().Len(); got != 0 { + t.Fatalf("expected pid reselection to clear persistent stream buffer, got len=%d", got) + } +} + func TestSelectTIDKeyReturnsToPickerWhenPIDFilterIsAll(t *testing.T) { flags.SetPidFilter(-1) flags.SetTidFilter(-1) |
