From 4acb116d78588489e79b7e17a79d4609a32fbba7 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 8 Mar 2026 20:14:32 +0200 Subject: task 367: carry full trace filters through TUI context --- internal/flags/flags.go | 3 ++ internal/ior.go | 37 ++++++++++++++++++++-- internal/ior_mode_test.go | 22 ++++++++++++- internal/tui/tui.go | 80 ++++++++++++++++++++++++++++++++--------------- internal/tui/tui_test.go | 24 ++++++++++++++ 5 files changed, 137 insertions(+), 29 deletions(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index af8f84c..61b5470 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -12,6 +12,7 @@ import ( "time" "ior/internal/collapse" + "ior/internal/globalfilter" ) var ( @@ -47,6 +48,7 @@ type Config struct { TUIExportEnable bool CollapsedFields []string CountField string + GlobalFilter globalfilter.Filter } // NewFlags returns a configuration instance initialized with project defaults. @@ -83,6 +85,7 @@ func (f Config) clone() Config { out.TracepointsToAttach = slices.Clone(f.TracepointsToAttach) out.TracepointsToExclude = slices.Clone(f.TracepointsToExclude) out.CollapsedFields = slices.Clone(f.CollapsedFields) + out.GlobalFilter = f.GlobalFilter.Clone() return out } diff --git a/internal/ior.go b/internal/ior.go index 7836ef2..ea06baa 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -18,6 +18,7 @@ import ( "ior/internal/event" "ior/internal/flags" "ior/internal/flamegraph" + "ior/internal/globalfilter" "ior/internal/probemanager" "ior/internal/statsengine" "ior/internal/tracepoints" @@ -174,9 +175,9 @@ func tuiTraceStarterFromRunTrace( }) cfg := baseCfg - if pidFilter, tidFilter, ok := tui.TraceFiltersFromContext(ctx); ok { - cfg.PidFilter = pidFilter - cfg.TidFilter = tidFilter + if filter, ok := tui.TraceFiltersFromContext(ctx); ok { + cfg.GlobalFilter = filter.Clone() + applyTraceFilterConfig(&cfg, filter) } engine := statsengine.NewEngine(64) streamBuf := eventstream.NewRingBuffer() @@ -229,6 +230,36 @@ func tuiTraceStarterFromRunTrace( } } +func applyTraceFilterConfig(cfg *flags.Config, filter globalfilter.Filter) { + if cfg == nil { + return + } + cfg.CommFilter = "" + cfg.PathFilter = "" + cfg.PidFilter = -1 + cfg.TidFilter = -1 + + if filter.Comm != nil { + cfg.CommFilter = filter.Comm.Pattern + } + if filter.File != nil { + cfg.PathFilter = filter.File.Pattern + } + if pid, ok := eqFilterValue(filter.PID); ok { + cfg.PidFilter = pid + } + if tid, ok := eqFilterValue(filter.TID); ok { + cfg.TidFilter = tid + } +} + +func eqFilterValue(filter *globalfilter.NumericFilter) (int, bool) { + if filter == nil || filter.Op != globalfilter.OpEq || filter.Value <= 0 { + return 0, false + } + return int(filter.Value), true +} + func runTrace(cfg flags.Config) error { return runTraceWithContext(context.Background(), cfg, nil, nil) } diff --git a/internal/ior_mode_test.go b/internal/ior_mode_test.go index 6b51985..fef3125 100644 --- a/internal/ior_mode_test.go +++ b/internal/ior_mode_test.go @@ -10,6 +10,7 @@ import ( "time" "ior/internal/flags" + "ior/internal/globalfilter" "ior/internal/tui" ) @@ -440,7 +441,14 @@ func TestTuiTraceStarterFromRunTraceUsesContextFilters(t *testing.T) { }, ) - ctx := tui.ContextWithTraceFilters(context.Background(), 2222, 3333) + ctx := tui.ContextWithTraceFilters(context.Background(), globalfilter.Filter{ + PID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 2222}, + TID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 3333}, + Comm: &globalfilter.StringFilter{Pattern: "nginx"}, + File: &globalfilter.StringFilter{Pattern: "/var/log"}, + Syscall: &globalfilter.StringFilter{Pattern: "read"}, + FD: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 7}, + }) if err := starter(ctx); err != nil { t.Fatalf("starter returned error: %v", err) } @@ -450,6 +458,18 @@ func TestTuiTraceStarterFromRunTraceUsesContextFilters(t *testing.T) { if gotCfg.TidFilter != 3333 { t.Fatalf("expected tid filter from context, got %d", gotCfg.TidFilter) } + if gotCfg.CommFilter != "nginx" { + t.Fatalf("expected comm filter from context, got %q", gotCfg.CommFilter) + } + if gotCfg.PathFilter != "/var/log" { + t.Fatalf("expected path filter from context, got %q", gotCfg.PathFilter) + } + if gotCfg.GlobalFilter.Syscall == nil || gotCfg.GlobalFilter.Syscall.Pattern != "read" { + t.Fatalf("expected syscall preserved in global filter payload, got %+v", gotCfg.GlobalFilter.Syscall) + } + if gotCfg.GlobalFilter.FD == nil || gotCfg.GlobalFilter.FD.Value != 7 { + t.Fatalf("expected fd preserved in global filter payload, got %+v", gotCfg.GlobalFilter.FD) + } } func TestProfilingFilesForMode(t *testing.T) { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index c375057..510a275 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -11,6 +11,7 @@ import ( coreexport "ior/internal/export" "ior/internal/flags" + "ior/internal/globalfilter" "ior/internal/probemanager" "ior/internal/statsengine" common "ior/internal/tui/common" @@ -76,8 +77,7 @@ type runtimeBindings struct { } type traceFilters struct { - pidFilter int - tidFilter int + filter globalfilter.Filter } func newRuntimeBindings() *runtimeBindings { @@ -157,19 +157,19 @@ func RuntimeBindingsFromContext(ctx context.Context) (TraceRuntimeBindings, bool return bindings, true } -// ContextWithTraceFilters stores the active PID/TID filters for the trace starter. -func ContextWithTraceFilters(ctx context.Context, pidFilter, tidFilter int) context.Context { - filters := traceFilters{pidFilter: pidFilter, tidFilter: tidFilter} +// ContextWithTraceFilters stores the active trace filters for the trace starter. +func ContextWithTraceFilters(ctx context.Context, filter globalfilter.Filter) context.Context { + filters := traceFilters{filter: filter.Clone()} return context.WithValue(ctx, traceFiltersContextKey{}, filters) } -// TraceFiltersFromContext returns the active PID/TID filters when provided by the TUI model. -func TraceFiltersFromContext(ctx context.Context) (pidFilter, tidFilter int, ok bool) { +// TraceFiltersFromContext returns the active trace filters when provided by the TUI model. +func TraceFiltersFromContext(ctx context.Context) (globalfilter.Filter, bool) { filters, ok := ctx.Value(traceFiltersContextKey{}).(traceFilters) if !ok { - return 0, 0, false + return globalfilter.Filter{}, false } - return filters.pidFilter, filters.tidFilter, true + return filters.filter.Clone(), true } // Run starts the TUI program in alternate screen mode. @@ -184,7 +184,7 @@ func RunWithTraceStarter(starter TraceStarter) error { // RunWithTraceStarterConfig starts the TUI with explicit runtime flags. func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { - model := newModelWithRuntimeConfig(cfg.PidFilter, cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, starter) + model := newModelWithRuntimeConfig(cfg.PidFilter, filterFromConfig(cfg), cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, starter) program := tea.NewProgram(model) _, err := program.Run() return err @@ -198,7 +198,7 @@ func RunTestFlamesWithTraceStarter(starter TraceStarter) error { // RunTestFlamesWithTraceStarterConfig starts test-flames mode with explicit runtime flags. func RunTestFlamesWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error { - model := newModelWithRuntimeConfig(1, 1, -1, cfg.TUIExportEnable, starter) + model := newModelWithRuntimeConfig(1, filterFromConfig(cfg), 1, -1, cfg.TUIExportEnable, starter) program := tea.NewProgram(model) _, err := program.Run() return err @@ -230,6 +230,7 @@ type Model struct { pidFilter int tidFilter int + globalFilter globalfilter.Filter pickerReturn *pickerReturnState exportEnabled bool isDark bool @@ -260,10 +261,10 @@ func NewModel(initialPID int, startTrace TraceStarter) Model { // NewModelWithConfig creates the top-level TUI model with explicit runtime flags. func NewModelWithConfig(cfg flags.Config, initialPID int, startTrace TraceStarter) Model { - return newModelWithRuntimeConfig(initialPID, cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, startTrace) + return newModelWithRuntimeConfig(initialPID, filterFromConfig(cfg), cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, startTrace) } -func newModelWithRuntimeConfig(initialPID, startupPidFilter, startupTidFilter int, exportEnabled bool, startTrace TraceStarter) Model { +func newModelWithRuntimeConfig(initialPID int, startupFilter globalfilter.Filter, startupPidFilter, startupTidFilter int, exportEnabled bool, startTrace TraceStarter) Model { common.ApplyPalette(true) syncStylesFromCommon() @@ -300,12 +301,12 @@ func newModelWithRuntimeConfig(initialPID, startupPidFilter, startupTidFilter in keys: keys, spin: spin, startTrace: startTrace, - pidFilter: pidFilter, - tidFilter: tidFilter, + globalFilter: startupFilter.Clone(), exportEnabled: exportEnabled, isDark: true, focused: true, } + model.setProcessFilters(pidFilter, tidFilter) if initialPID > 0 { model.screen = ScreenDashboard @@ -652,10 +653,8 @@ 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.pidFilter = pid - m.tidFilter = -1 + m.setProcessFilters(pid, -1) m.pickerReturn = nil - m.dashboard.SetPidFilter(pid) m.screen = ScreenDashboard m.attaching = true m.lastErr = nil @@ -669,10 +668,8 @@ func (m Model) handleTidSelected(msg TidSelectedMsg) (tea.Model, tea.Cmd) { pid = msg.Pid } m.stopTrace() - m.pidFilter = pid - m.tidFilter = tid + m.setProcessFilters(pid, tid) m.pickerReturn = nil - m.dashboard.SetPidFilter(pid) m.screen = ScreenDashboard m.attaching = true m.lastErr = nil @@ -741,9 +738,7 @@ func (m Model) cancelPickerToDashboard() (tea.Model, tea.Cmd) { returnState := *m.pickerReturn m.pickerReturn = nil m.stopTrace() - m.pidFilter = returnState.pidFilter - m.tidFilter = returnState.tidFilter - m.dashboard.SetPidFilter(m.pidFilter) + m.setProcessFilters(returnState.pidFilter, returnState.tidFilter) m.screen = ScreenDashboard m.attaching = true m.lastErr = nil @@ -754,7 +749,7 @@ func (m *Model) beginTraceCmd() tea.Cmd { ctx, cancel := context.WithCancel(context.Background()) m.traceStop = cancel ctx = context.WithValue(ctx, runtimeBindingsContextKey{}, m.runtime) - ctx = ContextWithTraceFilters(ctx, m.pidFilter, m.tidFilter) + ctx = ContextWithTraceFilters(ctx, m.globalFilter) return startTraceCmd(m.startTrace, ctx) } @@ -774,6 +769,41 @@ func defaultTraceStarter(context.Context) error { return nil } +func filterFromConfig(cfg flags.Config) globalfilter.Filter { + filter := cfg.GlobalFilter.Clone() + if filter.IsActive() { + return filter + } + if cfg.CommFilter != "" { + filter.Comm = &globalfilter.StringFilter{Pattern: cfg.CommFilter} + } + if cfg.PathFilter != "" { + filter.File = &globalfilter.StringFilter{Pattern: cfg.PathFilter} + } + if cfg.PidFilter > 0 { + filter.PID = eqNumericFilter(cfg.PidFilter) + } + if cfg.TidFilter > 0 { + filter.TID = eqNumericFilter(cfg.TidFilter) + } + return filter +} + +func eqNumericFilter(value int) *globalfilter.NumericFilter { + if value <= 0 { + return nil + } + return &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: int64(value)} +} + +func (m *Model) setProcessFilters(pid, tid int) { + m.pidFilter = pid + m.tidFilter = tid + m.globalFilter.PID = eqNumericFilter(pid) + m.globalFilter.TID = eqNumericFilter(tid) + m.dashboard.SetPidFilter(pid) +} + func (m *Model) stopTrace() { if m.traceStop != nil { m.traceStop() diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 85ebb71..608ba41 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -11,6 +11,7 @@ import ( "time" coreflamegraph "ior/internal/flamegraph" + "ior/internal/globalfilter" "ior/internal/probemanager" "ior/internal/statsengine" dashboardui "ior/internal/tui/dashboard" @@ -34,6 +35,29 @@ func (f fakeProbeManager) States() []probemanager.ProbeState { return f.states } func (f fakeProbeManager) Toggle(string) error { return nil } func (f fakeProbeManager) ActiveCount() (int, int) { return len(f.states), len(f.states) } +func TestTraceFiltersContextRoundTripClonesPayload(t *testing.T) { + original := globalfilter.Filter{ + Comm: &globalfilter.StringFilter{Pattern: "nginx"}, + File: &globalfilter.StringFilter{Pattern: "/var/log"}, + PID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 42}, + } + + ctx := ContextWithTraceFilters(context.Background(), original) + original.Comm.Pattern = "mutated" + original.PID.Value = 7 + + got, ok := TraceFiltersFromContext(ctx) + if !ok { + t.Fatalf("expected trace filters in context") + } + if got.Comm == nil || got.Comm.Pattern != "nginx" { + t.Fatalf("expected comm pattern cloned into context, got %+v", got.Comm) + } + if got.PID == nil || got.PID.Value != 42 { + t.Fatalf("expected pid filter cloned into context, got %+v", got.PID) + } +} + func TestPidSelectedTransitionsToDashboardAndSetsPIDFilter(t *testing.T) { flags.SetPidFilter(-1) flags.SetTidFilter(99) -- cgit v1.2.3