From f12756eecf2ea791ad894cc63380ed78f22f8797 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 23 May 2026 20:16:15 +0300 Subject: 9c move Non-IO grouping policy from core stats/types into dashboard Snapshot.NonIOFamilies, Snapshot.NonIOFamiliesCount, and types.IsNonIOSyscallFamily encoded a TUI tab concept in core packages. Move this filtering into internal/tui/dashboard/nonio.go as unexported helpers so the dashboard owns its own grouping policy and Snapshot.Families remains the neutral core API. Co-Authored-By: Claude Opus 4.7 --- AGENTS.md | 2 +- docs/syscall-tracing-plan.md | 2 +- internal/statsengine/engine_test.go | 11 ++++++----- internal/statsengine/snapshot.go | 22 --------------------- internal/statsengine/snapshot_test.go | 29 --------------------------- internal/tui/dashboard/model.go | 2 +- internal/tui/dashboard/nonio.go | 37 ++++++++++++++++++++++++++++++++++- internal/tui/dashboard/nonio_test.go | 37 +++++++++++++++++++++++++++++++++++ internal/types/family.go | 10 ---------- 9 files changed, 82 insertions(+), 70 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 08d37a5..0481ee8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,7 @@ Generator source code: - **File output in TUI** is explicit export only (`e`), writing `ior-stream-.csv` in the current directory from the current filtered stream snapshot. - **Export toggle flag**: `-tuiExport=true|false` (default `true`) enables or disables TUI stream CSV export at runtime. - **Tab navigation** supports `tab/shift+tab`, numeric keys `1..8`, and directional keys `left/right` and `h/l`. -- **Non-IO visibility**: dashboard includes a dedicated `Non-IO` tab backed by per-family aggregate rows (`Snapshot.Families` / `Snapshot.NonIOFamilies`). +- **Non-IO visibility**: dashboard includes a dedicated `Non-IO` tab backed by per-family aggregate rows (`Snapshot.Families`); Non-IO filtering is applied in `internal/tui/dashboard`. - **When export is disabled**, export key hints are hidden from dashboard help and `e` does not open export modal. - **Fast-refresh cadence**: `-tui-fast-refresh` (default `250ms`) controls the high-frequency tick interval for the flamegraph and stream tabs; set to `0` to disable high-frequency refresh and fall back to the standard dashboard cadence. - **Sampling / aggregate-only mode**: diff --git a/docs/syscall-tracing-plan.md b/docs/syscall-tracing-plan.md index 0a6e370..25e1af3 100644 --- a/docs/syscall-tracing-plan.md +++ b/docs/syscall-tracing-plan.md @@ -102,7 +102,7 @@ Memory extent is tracked separately via address-space metrics. ## Runtime Notes - Dashboard ships with a dedicated `Non-IO` tab (shortcut `8`) backed by - per-family aggregates (`Snapshot.Families` / `Snapshot.NonIOFamilies`). + per-family aggregates (`Snapshot.Families`); Non-IO filtering is applied in `internal/tui/dashboard`. - Aggregate-only sampling mode is implemented (`rate=0`) via: - `-syscall-sampling-families` - `-syscall-sampling-syscalls` diff --git a/internal/statsengine/engine_test.go b/internal/statsengine/engine_test.go index 0500d20..6eb19e8 100644 --- a/internal/statsengine/engine_test.go +++ b/internal/statsengine/engine_test.go @@ -115,12 +115,13 @@ func TestEngineAggregatesSyscallFamilies(t *testing.T) { t.Fatalf("FS family = %+v, want count=1 bytes=4", families["FS"]) } - nonIO := familyRowsByName(snap.NonIOFamilies()) - if _, ok := nonIO["FS"]; ok { - t.Fatalf("NonIOFamilies should not include FS: %+v", nonIO["FS"]) + // Verify that non-FS families exist and FS is present in the full + // family list — Non-IO filtering has moved to the dashboard package. + if _, ok := families["Polling"]; !ok { + t.Fatalf("Families missing Polling row: %+v", families) } - if nonIO["Polling"].Count != 2 || nonIO["Process"].Count != 1 { - t.Fatalf("NonIOFamilies missing expected rows: %+v", nonIO) + if _, ok := families["Process"]; !ok { + t.Fatalf("Families missing Process row: %+v", families) } } diff --git a/internal/statsengine/snapshot.go b/internal/statsengine/snapshot.go index bec92fb..fa9c948 100644 --- a/internal/statsengine/snapshot.go +++ b/internal/statsengine/snapshot.go @@ -237,28 +237,6 @@ func (s Snapshot) FamiliesCount() int { return len(s.families) } -// NonIOFamilies returns family rows outside the file-system/fd-focused family. -func (s Snapshot) NonIOFamilies() []FamilySnapshot { - rows := make([]FamilySnapshot, 0, len(s.families)) - for _, row := range s.families { - if types.IsNonIOSyscallFamily(row.Family) { - rows = append(rows, row) - } - } - return rows -} - -// NonIOFamiliesCount returns number of non-FS syscall-family rows. -func (s Snapshot) NonIOFamiliesCount() int { - count := 0 - for _, row := range s.families { - if types.IsNonIOSyscallFamily(row.Family) { - count++ - } - } - return count -} - // TopNSyscalls returns at most n per-syscall rows in ranking order. // Callers must treat returned data as read-only. func (s Snapshot) TopNSyscalls(n int) []SyscallSnapshot { diff --git a/internal/statsengine/snapshot_test.go b/internal/statsengine/snapshot_test.go index 9b54409..d5cb7aa 100644 --- a/internal/statsengine/snapshot_test.go +++ b/internal/statsengine/snapshot_test.go @@ -124,35 +124,6 @@ func TestNilAccessorsRemainNil(t *testing.T) { } } -func TestSnapshotNonIOFamilies(t *testing.T) { - s := NewSnapshotWithFamilies( - nil, - nil, - nil, - nil, - []FamilySnapshot{ - {Family: types.FamilyFS, Name: "FS"}, - {Family: types.FamilyPolling, Name: "Polling"}, - {Family: types.FamilyProcess, Name: "Process"}, - }, - nil, - nil, - HistogramSnapshot{}, - HistogramSnapshot{}, - ) - - rows := s.NonIOFamilies() - if len(rows) != 2 { - t.Fatalf("NonIOFamilies len = %d, want 2", len(rows)) - } - if rows[0].Family == types.FamilyFS || rows[1].Family == types.FamilyFS { - t.Fatalf("NonIOFamilies included FS: %+v", rows) - } - if got := s.NonIOFamiliesCount(); got != 2 { - t.Fatalf("NonIOFamiliesCount = %d, want 2", got) - } -} - func TestTopNAccessors(t *testing.T) { s := NewSnapshot( nil, diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go index 1f479c7..fa36453 100644 --- a/internal/tui/dashboard/model.go +++ b/internal/tui/dashboard/model.go @@ -884,7 +884,7 @@ func (m Model) maxSyscallsRows() int { } func (m Model) maxNonIORows() int { - return m.snapshotOrZero().NonIOFamiliesCount() + return nonIOFamiliesCount(m.snapshotOrZero().Families()) } func (m Model) maxFilesRows() int { diff --git a/internal/tui/dashboard/nonio.go b/internal/tui/dashboard/nonio.go index aef63f4..42069a2 100644 --- a/internal/tui/dashboard/nonio.go +++ b/internal/tui/dashboard/nonio.go @@ -6,6 +6,7 @@ import ( "ior/internal/statsengine" common "ior/internal/tui/common" + "ior/internal/types" ) func renderNonIO(snap *statsengine.Snapshot, width, height int) string { @@ -17,7 +18,7 @@ func renderNonIOWithOffset(snap *statsengine.Snapshot, width, height, offset, se return "Non-IO: waiting for stats..." } - rowsData := snap.NonIOFamilies() + rowsData := nonIOFamilies(snap.Families()) columns, rows := nonIOTableData(rowsData, width) if len(rows) == 0 { return "Non-IO: no data" @@ -97,3 +98,37 @@ func nonIORowsCompact(families []statsengine.FamilySnapshot) [][]string { } return rows } + +// isFileSyscallFamily reports whether family belongs to file-system/fd views. +// This is a dashboard presentation concept: the Non-IO tab excludes FS families. +func isFileSyscallFamily(family types.SyscallFamily) bool { + return family == types.FamilyFS +} + +// isNonIOSyscallFamily reports whether family should appear in the Non-IO tab. +// This is a dashboard grouping policy kept out of the core stats/types packages. +func isNonIOSyscallFamily(family types.SyscallFamily) bool { + return family != "" && !isFileSyscallFamily(family) +} + +// nonIOFamilies filters family snapshot rows to those outside the FS family. +func nonIOFamilies(all []statsengine.FamilySnapshot) []statsengine.FamilySnapshot { + rows := make([]statsengine.FamilySnapshot, 0, len(all)) + for _, row := range all { + if isNonIOSyscallFamily(row.Family) { + rows = append(rows, row) + } + } + return rows +} + +// nonIOFamiliesCount returns the number of non-FS family rows. +func nonIOFamiliesCount(all []statsengine.FamilySnapshot) int { + count := 0 + for _, row := range all { + if isNonIOSyscallFamily(row.Family) { + count++ + } + } + return count +} diff --git a/internal/tui/dashboard/nonio_test.go b/internal/tui/dashboard/nonio_test.go index 5fabc76..0b5fc25 100644 --- a/internal/tui/dashboard/nonio_test.go +++ b/internal/tui/dashboard/nonio_test.go @@ -35,3 +35,40 @@ func TestRenderNonIOIncludesExpectedFamilyRows(t *testing.T) { t.Fatalf("non-io table should exclude FS rows:\n%s", out) } } + +func TestNonIOFamiliesFiltering(t *testing.T) { + all := []statsengine.FamilySnapshot{ + {Family: types.FamilyFS, Name: "FS"}, + {Family: types.FamilyPolling, Name: "Polling"}, + {Family: types.FamilyProcess, Name: "Process"}, + } + + rows := nonIOFamilies(all) + if len(rows) != 2 { + t.Fatalf("nonIOFamilies len = %d, want 2", len(rows)) + } + if rows[0].Family == types.FamilyFS || rows[1].Family == types.FamilyFS { + t.Fatalf("nonIOFamilies included FS: %+v", rows) + } + if got := nonIOFamiliesCount(all); got != 2 { + t.Fatalf("nonIOFamiliesCount = %d, want 2", got) + } +} + +func TestIsNonIOSyscallFamily(t *testing.T) { + tests := []struct { + family types.SyscallFamily + want bool + }{ + {types.FamilyFS, false}, + {types.FamilyPolling, true}, + {types.FamilyProcess, true}, + {types.FamilyNetwork, true}, + {"", false}, + } + for _, tt := range tests { + if got := isNonIOSyscallFamily(tt.family); got != tt.want { + t.Errorf("isNonIOSyscallFamily(%q) = %v, want %v", tt.family, got, tt.want) + } + } +} diff --git a/internal/types/family.go b/internal/types/family.go index 048f143..de5e021 100644 --- a/internal/types/family.go +++ b/internal/types/family.go @@ -20,16 +20,6 @@ func AllSyscallFamilies() []SyscallFamily { } } -// IsFileSyscallFamily reports whether family belongs to file-system/syscall-fd views. -func IsFileSyscallFamily(family SyscallFamily) bool { - return family == FamilyFS -} - -// IsNonIOSyscallFamily reports whether family should appear in the Non-IO tab. -func IsNonIOSyscallFamily(family SyscallFamily) bool { - return family != "" && !IsFileSyscallFamily(family) -} - // SyscallFamilyRank returns the stable display rank for a family. func SyscallFamilyRank(family SyscallFamily) int { for idx, candidate := range AllSyscallFamilies() { -- cgit v1.2.3