summaryrefslogtreecommitdiff
path: root/internal/flags
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-23 20:04:06 +0300
committerPaul Buetow <paul@buetow.org>2026-05-23 20:04:06 +0300
commit4541ee21203f7c494530f142a6387244ac3a6c62 (patch)
tree3279dcafffcbcb22662f01fea1f9afb0e4039663 /internal/flags
parentb4a172404be8b52ed62171dcbbd3e9f46e36ac67 (diff)
0c promote aggregate-only sampling defaults in raw output modes
Default aggregate-only sampling (rate 0) for futex* and clock_gettime causes BPF to suppress ring-buffer events. In -plain, -flamegraph, and headless -parquet modes there is no aggregate sink, so these probes would emit no rows even when explicitly selected. Promote those defaults to rate 1 during flag resolution; user-explicit overrides are preserved. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diffstat (limited to 'internal/flags')
-rw-r--r--internal/flags/flags.go16
-rw-r--r--internal/flags/sampling.go17
-rw-r--r--internal/flags/sampling_test.go86
3 files changed, 119 insertions, 0 deletions
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index cc39638..c20707b 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -85,6 +85,15 @@ type Config struct {
ShowVersion bool
}
+// IsRawOutputMode reports whether the config selects a headless output path
+// (-plain, -flamegraph, or headless -parquet) that lacks a TUI aggregate
+// sink. In these modes, aggregate-only sampling (rate 0) would silently
+// suppress ring-buffer events, so callers should promote default aggregate-
+// only rates to 1.
+func (f Config) IsRawOutputMode() bool {
+ return f.PlainMode || f.FlamegraphOutput || strings.TrimSpace(f.ParquetPath) != ""
+}
+
// DefaultResetTimer is the default cadence for the dashboard's auto-reset
// timer. It periodically clears aggregate state (live flamegraph trie and
// stats engine) — the same effect as pressing `r` — to prevent unbounded
@@ -266,6 +275,13 @@ func resolveSamplingRates(cfg *Config, familySampling, syscallSampling *string)
}
cfg.SyscallFamilySamplingRates = familyRates
cfg.SyscallSamplingRates = mergeSyscallSamplingRates(syscallRates)
+ // In raw output modes (-plain, -flamegraph, headless -parquet) there is
+ // no aggregate sink, so aggregate-only defaults (rate 0) would silently
+ // suppress ring-buffer events. Promote those defaults to rate 1 unless
+ // the user explicitly requested rate 0 via -syscall-sampling-syscalls.
+ if cfg.IsRawOutputMode() {
+ promoteAggregateOnlyForRawOutput(cfg.SyscallSamplingRates, syscallRates)
+ }
return nil
}
diff --git a/internal/flags/sampling.go b/internal/flags/sampling.go
index 0f0af2a..5656f0a 100644
--- a/internal/flags/sampling.go
+++ b/internal/flags/sampling.go
@@ -49,6 +49,23 @@ func mergeSyscallSamplingRates(overrides map[string]uint32) map[string]uint32 {
return out
}
+// promoteAggregateOnlyForRawOutput replaces default aggregate-only rates (0)
+// with rate 1 (emit every event) when running in a raw output mode that lacks
+// an aggregate sink. Without this promotion, BPF suppresses ring-buffer
+// events for these syscalls and no rows appear in -plain, -flamegraph, or
+// headless -parquet output. User-explicit overrides (present in userOverrides)
+// are preserved unchanged.
+func promoteAggregateOnlyForRawOutput(merged map[string]uint32, userOverrides map[string]uint32) {
+ for _, syscall := range defaultAggregateOnlySyscalls {
+ if _, explicit := userOverrides[syscall]; explicit {
+ continue
+ }
+ if merged[syscall] == 0 {
+ merged[syscall] = 1
+ }
+ }
+}
+
func parseFamilySamplingRates(raw string) (map[types.SyscallFamily]uint32, error) {
entries, err := parseSamplingEntries(raw)
if err != nil {
diff --git a/internal/flags/sampling_test.go b/internal/flags/sampling_test.go
index c43c2fc..6ae95c4 100644
--- a/internal/flags/sampling_test.go
+++ b/internal/flags/sampling_test.go
@@ -102,3 +102,89 @@ func TestParseSamplingRatesOverrideDefaultFutexRate(t *testing.T) {
t.Fatalf("futex rate = %d, want 7", got)
}
}
+
+func TestPlainModePromotesAggregateOnlyDefaults(t *testing.T) {
+ cfg, err := parseForTest(t, "-plain")
+ if err != nil {
+ t.Fatalf("parse returned error: %v", err)
+ }
+ for _, syscall := range []string{"futex", "futex_wait", "futex_wake", "futex_requeue", "futex_waitv", "clock_gettime"} {
+ rate, ok := cfg.SyscallSamplingRates[syscall]
+ if !ok {
+ t.Fatalf("expected sampling entry for %s in plain mode", syscall)
+ }
+ if rate != 1 {
+ t.Fatalf("%s rate in plain mode = %d, want 1 (promoted from aggregate-only)", syscall, rate)
+ }
+ }
+}
+
+func TestFlamegraphModePromotesAggregateOnlyDefaults(t *testing.T) {
+ cfg, err := parseForTest(t, "-flamegraph")
+ if err != nil {
+ t.Fatalf("parse returned error: %v", err)
+ }
+ if got := cfg.SyscallSamplingRates["clock_gettime"]; got != 1 {
+ t.Fatalf("clock_gettime rate in flamegraph mode = %d, want 1", got)
+ }
+}
+
+func TestParquetModePromotesAggregateOnlyDefaults(t *testing.T) {
+ cfg, err := parseForTest(t, "-parquet", "trace.parquet")
+ if err != nil {
+ t.Fatalf("parse returned error: %v", err)
+ }
+ if got := cfg.SyscallSamplingRates["futex"]; got != 1 {
+ t.Fatalf("futex rate in parquet mode = %d, want 1", got)
+ }
+}
+
+func TestPlainModePreservesExplicitAggregateOnly(t *testing.T) {
+ cfg, err := parseForTest(t, "-plain", "-syscall-sampling-syscalls", "futex=0")
+ if err != nil {
+ t.Fatalf("parse returned error: %v", err)
+ }
+ // User explicitly requested aggregate-only for futex; it should stay 0.
+ if got := cfg.SyscallSamplingRates["futex"]; got != 0 {
+ t.Fatalf("futex rate = %d, want 0 (explicit override preserved)", got)
+ }
+ // clock_gettime was not overridden, so it should be promoted to 1.
+ if got := cfg.SyscallSamplingRates["clock_gettime"]; got != 1 {
+ t.Fatalf("clock_gettime rate = %d, want 1 (default promoted)", got)
+ }
+}
+
+func TestTUIModeKeepsAggregateOnlyDefaults(t *testing.T) {
+ cfg, err := parseForTest(t)
+ if err != nil {
+ t.Fatalf("parse returned error: %v", err)
+ }
+ // In TUI mode (no -plain, -flamegraph, or -parquet), defaults should
+ // remain aggregate-only (rate 0) because the aggregate sink is present.
+ for _, syscall := range []string{"futex", "clock_gettime"} {
+ if got := cfg.SyscallSamplingRates[syscall]; got != 0 {
+ t.Fatalf("%s rate in TUI mode = %d, want 0 (aggregate-only default)", syscall, got)
+ }
+ }
+}
+
+func TestIsRawOutputMode(t *testing.T) {
+ cases := []struct {
+ name string
+ cfg Config
+ want bool
+ }{
+ {"default TUI", Config{}, false},
+ {"plain", Config{PlainMode: true}, true},
+ {"flamegraph", Config{FlamegraphOutput: true}, true},
+ {"parquet", Config{ParquetPath: "trace.parquet"}, true},
+ {"parquet whitespace", Config{ParquetPath: " "}, false},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := tc.cfg.IsRawOutputMode(); got != tc.want {
+ t.Fatalf("IsRawOutputMode() = %v, want %v", got, tc.want)
+ }
+ })
+ }
+}