summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-08 20:14:32 +0200
committerPaul Buetow <paul@buetow.org>2026-03-08 20:14:32 +0200
commit4acb116d78588489e79b7e17a79d4609a32fbba7 (patch)
treea43e755c890083fec1fb86169844593cf9a70e13
parent21aa0cd0f96087fa040750643109c496e7a1b3ee (diff)
task 367: carry full trace filters through TUI context
-rw-r--r--internal/flags/flags.go3
-rw-r--r--internal/ior.go37
-rw-r--r--internal/ior_mode_test.go22
-rw-r--r--internal/tui/tui.go80
-rw-r--r--internal/tui/tui_test.go24
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)