summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/ior.go3
-rw-r--r--internal/tui/eventstream/ringbuffer.go10
-rw-r--r--internal/tui/tui.go26
-rw-r--r--internal/tui/tui_test.go87
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)