diff options
| -rw-r--r-- | internal/tui/filterstack.go | 13 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 21 |
2 files changed, 34 insertions, 0 deletions
diff --git a/internal/tui/filterstack.go b/internal/tui/filterstack.go index 016d845..28667d9 100644 --- a/internal/tui/filterstack.go +++ b/internal/tui/filterstack.go @@ -7,6 +7,11 @@ import ( "ior/internal/globalfilter/presenter" ) +// maxFilterHistory is the maximum number of undo levels retained by filterStack. +// Once this cap is reached, the oldest entry is evicted so the slices never +// grow without bound across long-running sessions or automated filter changes. +const maxFilterHistory = 50 + // filterStack manages the trace filter chain: the active filter, the undo // history, and the human-readable label stack displayed in the status bar. // It owns all filter mutation and label-generation logic so the top-level @@ -29,6 +34,8 @@ func (f *filterStack) current() globalfilter.Filter { // push records oldFilter in the history stack, sets global to newFilter, and // appends an action label. Returns true when the filter actually changed. +// When the history depth would exceed maxFilterHistory the oldest entry is +// evicted from both slices to keep memory usage bounded. func (f *filterStack) push(newFilter globalfilter.Filter, action string) bool { next := newFilter.Clone() if f.global.Equal(next) { @@ -36,6 +43,12 @@ func (f *filterStack) push(newFilter globalfilter.Filter, action string) bool { } f.history = append(f.history, f.global.Clone()) f.stack = append(f.stack, globalFilterActionLabel(f.global, next, action)) + // Evict the oldest undo level once the cap is exceeded so the slices + // never grow without bound across long-running sessions. + if len(f.history) > maxFilterHistory { + f.history = f.history[1:] + f.stack = f.stack[1:] + } f.global = next return true } diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index ba0f8ed..a685719 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -1725,6 +1725,27 @@ func TestDashboardFooterShowsGlobalFilterStack(t *testing.T) { } } +func TestFilterStackHistoryCapEvictsOldestEntries(t *testing.T) { + // Push maxFilterHistory+10 distinct filters and verify the slices never + // exceed the cap. The oldest entries must be evicted, and the most-recent + // maxFilterHistory entries must be retained. + fs := newFilterStack(globalfilter.Filter{}) + for i := 0; i < maxFilterHistory+10; i++ { + f := globalfilter.Filter{} + f.FD = globalfilter.NewEqFilter(int64(i + 1)) // unique per iteration + fs.push(f, "") + } + if len(fs.history) > maxFilterHistory { + t.Fatalf("history exceeds cap: got %d, want <= %d", len(fs.history), maxFilterHistory) + } + if len(fs.stack) > maxFilterHistory { + t.Fatalf("stack exceeds cap: got %d, want <= %d", len(fs.stack), maxFilterHistory) + } + if len(fs.history) != len(fs.stack) { + t.Fatalf("history and stack lengths must match: history=%d stack=%d", len(fs.history), len(fs.stack)) + } +} + func TestProcessesTabEnterAppliesSelectedProcessAsGlobalFilter(t *testing.T) { m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard |
