From 83ff18252be5ad4d667084a3a6edbf7cd5271e6b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 8 Mar 2026 20:16:56 +0200 Subject: task 368: filter live pairs before TUI ingestion --- internal/globalfilter/pair.go | 99 ++++++++++++++++++++++++++++++++++++++ internal/globalfilter/pair_test.go | 55 +++++++++++++++++++++ internal/ior.go | 11 +++++ internal/ior_mode_test.go | 39 +++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 internal/globalfilter/pair.go create mode 100644 internal/globalfilter/pair_test.go diff --git a/internal/globalfilter/pair.go b/internal/globalfilter/pair.go new file mode 100644 index 0000000..4215120 --- /dev/null +++ b/internal/globalfilter/pair.go @@ -0,0 +1,99 @@ +package globalfilter + +import ( + "ior/internal/event" + "ior/internal/types" +) + +func MatchPair(filter Filter, pair *event.Pair) bool { + if pair == nil { + return false + } + return filter.Matches(pairCandidate{pair: pair}) +} + +type pairCandidate struct { + pair *event.Pair +} + +func (p pairCandidate) SyscallValue() string { + if p.pair == nil || p.pair.EnterEv == nil { + return "" + } + return p.pair.EnterEv.GetTraceId().Name() +} + +func (p pairCandidate) CommValue() string { + if p.pair == nil { + return "" + } + return p.pair.Comm +} + +func (p pairCandidate) FileValue() string { + if p.pair == nil || p.pair.File == nil { + return "" + } + return p.pair.File.Name() +} + +func (p pairCandidate) PIDValue() uint32 { + if p.pair == nil || p.pair.EnterEv == nil { + return 0 + } + return p.pair.EnterEv.GetPid() +} + +func (p pairCandidate) TIDValue() uint32 { + if p.pair == nil || p.pair.EnterEv == nil { + return 0 + } + return p.pair.EnterEv.GetTid() +} + +func (p pairCandidate) FDValue() int32 { + if p.pair == nil { + return -1 + } + fd, ok := p.pair.FileDescriptor() + if !ok { + return -1 + } + return fd +} + +func (p pairCandidate) LatencyValue() uint64 { + if p.pair == nil { + return 0 + } + return p.pair.Duration +} + +func (p pairCandidate) GapValue() uint64 { + if p.pair == nil { + return 0 + } + return p.pair.DurationToPrev +} + +func (p pairCandidate) BytesValue() uint64 { + if p.pair == nil { + return 0 + } + return p.pair.Bytes +} + +func (p pairCandidate) ReturnValue() int64 { + if p.pair == nil { + return 0 + } + retEvent, ok := p.pair.ExitEv.(*types.RetEvent) + if !ok { + return 0 + } + return retEvent.Ret +} + +func (p pairCandidate) ErrorValue() bool { + return p.ReturnValue() < 0 +} diff --git a/internal/globalfilter/pair_test.go b/internal/globalfilter/pair_test.go new file mode 100644 index 0000000..4810a0b --- /dev/null +++ b/internal/globalfilter/pair_test.go @@ -0,0 +1,55 @@ +package globalfilter + +import ( + "testing" + + "ior/internal/event" + "ior/internal/file" + "ior/internal/types" +) + +func samplePair() *event.Pair { + return &event.Pair{ + EnterEv: &types.RetEvent{TraceId: types.SYS_ENTER_READ, Pid: 1234, Tid: 1235}, + ExitEv: &types.RetEvent{TraceId: types.SYS_EXIT_READ, Pid: 1234, Tid: 1235, Ret: -1}, + Comm: "nginx", + File: file.NewFd(7, "/var/log/access.log", 0), + Duration: 1_500_000, + DurationToPrev: 12_000, + Bytes: 4_096, + } +} + +func TestMatchPairMatchesAllSupportedFields(t *testing.T) { + filter := Filter{ + Syscall: &StringFilter{Pattern: "rea"}, + Comm: &StringFilter{Pattern: "NGI"}, + File: &StringFilter{Pattern: "access"}, + PID: &NumericFilter{Op: OpEq, Value: 1234}, + TID: &NumericFilter{Op: OpEq, Value: 1235}, + FD: &NumericFilter{Op: OpEq, Value: 7}, + LatencyNs: &NumericFilter{Op: OpGt, Value: 1_000_000}, + GapNs: &NumericFilter{Op: OpLte, Value: 12_000}, + Bytes: &NumericFilter{Op: OpLt, Value: 8_192}, + RetVal: &NumericFilter{Op: OpEq, Value: -1}, + ErrorsOnly: true, + } + if !MatchPair(filter, samplePair()) { + t.Fatalf("expected full filter to match pair") + } +} + +func TestMatchPairRejectsMismatchesAndMissingFD(t *testing.T) { + pair := samplePair() + if MatchPair(Filter{Syscall: &StringFilter{Pattern: "write"}}, pair) { + t.Fatalf("expected syscall mismatch to reject pair") + } + if MatchPair(Filter{FD: &NumericFilter{Op: OpEq, Value: 99}}, pair) { + t.Fatalf("expected fd mismatch to reject pair") + } + + pair.File = nil + if MatchPair(Filter{FD: &NumericFilter{Op: OpEq, Value: 7}}, pair) { + t.Fatalf("expected missing fd to reject pair") + } +} diff --git a/internal/ior.go b/internal/ior.go index ea06baa..d113fff 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -201,6 +201,10 @@ func tuiTraceStarterFromRunTrace( go func() { err := startTrace(ctx, cfg, startedCh, func(el *eventLoop) { el.printCb = func(ep *event.Pair) { + if !shouldIngestTracePair(cfg.GlobalFilter, ep) { + ep.Recycle() + return + } engine.Ingest(ep) streamEvents <- eventstream.NewStreamEvent(ep.EnterEv.GetTime(), ep) liveTrie.Ingest(ep) @@ -230,6 +234,13 @@ func tuiTraceStarterFromRunTrace( } } +func shouldIngestTracePair(filter globalfilter.Filter, pair *event.Pair) bool { + if !filter.IsActive() { + return true + } + return globalfilter.MatchPair(filter, pair) +} + func applyTraceFilterConfig(cfg *flags.Config, filter globalfilter.Filter) { if cfg == nil { return diff --git a/internal/ior_mode_test.go b/internal/ior_mode_test.go index fef3125..4140485 100644 --- a/internal/ior_mode_test.go +++ b/internal/ior_mode_test.go @@ -9,9 +9,12 @@ import ( "testing/synctest" "time" + "ior/internal/event" + "ior/internal/file" "ior/internal/flags" "ior/internal/globalfilter" "ior/internal/tui" + "ior/internal/types" ) func TestShouldRunTraceMode(t *testing.T) { @@ -472,6 +475,42 @@ func TestTuiTraceStarterFromRunTraceUsesContextFilters(t *testing.T) { } } +func TestShouldIngestTracePairAppliesFullGlobalFilter(t *testing.T) { + pair := &event.Pair{ + EnterEv: &types.RetEvent{TraceId: types.SYS_ENTER_READ, Pid: 1234, Tid: 1235}, + ExitEv: &types.RetEvent{TraceId: types.SYS_EXIT_READ, Pid: 1234, Tid: 1235, Ret: -1}, + Comm: "nginx", + File: file.NewFd(7, "/var/log/access.log", 0), + Duration: 1_500_000, + DurationToPrev: 12_000, + Bytes: 4_096, + } + + filter := globalfilter.Filter{ + Syscall: &globalfilter.StringFilter{Pattern: "rea"}, + Comm: &globalfilter.StringFilter{Pattern: "ngi"}, + File: &globalfilter.StringFilter{Pattern: "access"}, + PID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 1234}, + TID: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 1235}, + FD: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 7}, + LatencyNs: &globalfilter.NumericFilter{Op: globalfilter.OpGt, Value: 1_000_000}, + GapNs: &globalfilter.NumericFilter{Op: globalfilter.OpLte, Value: 12_000}, + Bytes: &globalfilter.NumericFilter{Op: globalfilter.OpLt, Value: 8_192}, + RetVal: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: -1}, + ErrorsOnly: true, + } + + if !shouldIngestTracePair(filter, pair) { + t.Fatalf("expected full filter to accept matching pair") + } + if shouldIngestTracePair(globalfilter.Filter{Syscall: &globalfilter.StringFilter{Pattern: "write"}}, pair) { + t.Fatalf("expected syscall mismatch to reject pair") + } + if shouldIngestTracePair(globalfilter.Filter{FD: &globalfilter.NumericFilter{Op: globalfilter.OpEq, Value: 99}}, pair) { + t.Fatalf("expected fd mismatch to reject pair") + } +} + func TestProfilingFilesForMode(t *testing.T) { cpu, mem, execTrace, duration := profilingFilesForMode(false) if cpu != "ior.cpuprofile" || mem != "ior.memprofile" { -- cgit v1.2.3