summaryrefslogtreecommitdiff
path: root/internal/flags
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
committerPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
commit1561987330cb898f5ff64383a9c78e7e6559f118 (patch)
tree69a823e8f98dce572566c97e6879c11c9d591bda /internal/flags
parent96225fb6159212a8851043a08d781aba721b4e78 (diff)
parent110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff)
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/flags')
-rw-r--r--internal/flags/doc.go2
-rw-r--r--internal/flags/flags.go210
-rw-r--r--internal/flags/flags_test.go84
3 files changed, 156 insertions, 140 deletions
diff --git a/internal/flags/doc.go b/internal/flags/doc.go
new file mode 100644
index 0000000..103b6d4
--- /dev/null
+++ b/internal/flags/doc.go
@@ -0,0 +1,2 @@
+// Package flags parses CLI options and exposes runtime configuration snapshots.
+package flags
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index b2d9dce..af8f84c 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -10,44 +10,23 @@ import (
"sync"
"sync/atomic"
"time"
+
+ "ior/internal/collapse"
)
var (
- singleton = Flags{
- TUIExportEnable: true,
- }
- once sync.Once
- parseErr error
- pidFilter atomic.Int64
- tidFilter atomic.Int64
- tuiExportEnable atomic.Bool
+ current atomic.Pointer[Config]
+ once sync.Once
+ parseErr error
)
func init() {
- pidFilter.Store(-1)
- tidFilter.Store(-1)
- tuiExportEnable.Store(true)
+ defaults := NewFlags()
+ current.Store(&defaults)
}
-var (
- validCollapsedFields = []string{
- "path",
- "comm",
- "tracepoint",
- "pid",
- "tid",
- "flags",
- }
-
- validCollapsedCounts = []string{
- "count",
- "duration",
- "durationToPrev",
- "bytes",
- }
-)
-
-type Flags struct {
+// Config captures runtime configuration parsed from CLI flags.
+type Config struct {
PidFilter int
TidFilter int
EventMapSize int
@@ -60,46 +39,104 @@ type Flags struct {
TracepointsToAttach []*regexp.Regexp
TracepointsToExclude []*regexp.Regexp
- // Flamegraph flags
- PlainMode bool
- FlamegraphEnable bool
- LiveFlamegraph bool
- LiveInterval time.Duration
- OpenCommand string
- FlamegraphName string
- FlamegraphJSON bool
- TUIExportEnable bool
-
- // To convert ior data into native SVG format
- IorDataFile string
- IorWatchInterval time.Duration
- CollapsedFields []string
- CountField string
-}
-
-func Get() Flags {
- out := singleton
- out.PidFilter = int(pidFilter.Load())
- out.TidFilter = int(tidFilter.Load())
- out.TUIExportEnable = tuiExportEnable.Load()
+ // Output/runtime flags
+ PlainMode bool
+ TestFlames bool
+ TestLiveFlames bool
+ LiveInterval time.Duration
+ TUIExportEnable bool
+ CollapsedFields []string
+ CountField string
+}
+
+// NewFlags returns a configuration instance initialized with project defaults.
+func NewFlags() Config {
+ return Config{
+ PidFilter: -1,
+ TidFilter: -1,
+ EventMapSize: 4096 * 16,
+ Duration: 900,
+ LiveInterval: 200 * time.Millisecond,
+ TUIExportEnable: true,
+ CollapsedFields: []string{"comm", "tracepoint", "path"},
+ CountField: "count",
+ }
+}
+
+// GetPidFilter returns the active process filter.
+func (f Config) GetPidFilter() int {
+ return f.PidFilter
+}
+
+// GetTidFilter returns the active thread filter.
+func (f Config) GetTidFilter() int {
+ return f.TidFilter
+}
+
+// GetTUIExportEnable reports whether TUI CSV export is enabled.
+func (f Config) GetTUIExportEnable() bool {
+ return f.TUIExportEnable
+}
+
+func (f Config) clone() Config {
+ out := f
+ out.TracepointsToAttach = slices.Clone(f.TracepointsToAttach)
+ out.TracepointsToExclude = slices.Clone(f.TracepointsToExclude)
+ out.CollapsedFields = slices.Clone(f.CollapsedFields)
return out
}
+// Get returns a copy of the currently active runtime configuration.
+func Get() Config {
+ cfg := current.Load()
+ if cfg == nil {
+ return NewFlags()
+ }
+ return cfg.clone()
+}
+
+func setCurrent(cfg Config) {
+ snapshot := cfg.clone()
+ current.Store(&snapshot)
+}
+
+func updateCurrent(update func(*Config)) {
+ for {
+ old := current.Load()
+ next := NewFlags()
+ if old != nil {
+ next = old.clone()
+ }
+ update(&next)
+ snapshot := next.clone()
+ if current.CompareAndSwap(old, &snapshot) {
+ return
+ }
+ }
+}
+
// SetPidFilter updates the active PID filter used for subsequent tracing runs.
func SetPidFilter(pid int) {
- pidFilter.Store(int64(pid))
+ updateCurrent(func(cfg *Config) {
+ cfg.PidFilter = pid
+ })
}
// SetTidFilter updates the active TID filter used for subsequent tracing runs.
func SetTidFilter(tid int) {
- tidFilter.Store(int64(tid))
+ updateCurrent(func(cfg *Config) {
+ cfg.TidFilter = tid
+ })
}
// SetTUIExportEnable toggles TUI snapshot export file writing.
func SetTUIExportEnable(enabled bool) {
- tuiExportEnable.Store(enabled)
+ updateCurrent(func(cfg *Config) {
+ cfg.TUIExportEnable = enabled
+ })
}
+// Parse parses CLI flags once and updates the current runtime configuration.
func Parse() error {
once.Do(func() {
parseErr = parse()
@@ -108,48 +145,42 @@ func Parse() error {
}
func parse() error {
- flag.IntVar(&singleton.PidFilter, "pid", -1, "Filter for processes ID")
- flag.IntVar(&singleton.TidFilter, "tid", -1, "Filter for thread ID")
- flag.IntVar(&singleton.EventMapSize, "mapSize", 4096*16, "BPF FD event ring buffer map size")
- flag.IntVar(&singleton.Duration, "duration", 900, "Probe duration in seconds")
+ cfg := NewFlags()
+ validFields := collapse.ValidFields()
+ validCounts := collapse.ValidCountFields()
+
+ flag.IntVar(&cfg.PidFilter, "pid", cfg.PidFilter, "Filter for processes ID")
+ flag.IntVar(&cfg.TidFilter, "tid", cfg.TidFilter, "Filter for thread ID")
+ flag.IntVar(&cfg.EventMapSize, "mapSize", cfg.EventMapSize, "BPF FD event ring buffer map size")
+ flag.IntVar(&cfg.Duration, "duration", cfg.Duration, "Probe duration in seconds")
- flag.StringVar(&singleton.CommFilter, "comm", "", "Command to filter for")
- flag.StringVar(&singleton.PathFilter, "path", "", "Path to filter for")
+ flag.StringVar(&cfg.CommFilter, "comm", "", "Command to filter for")
+ flag.StringVar(&cfg.PathFilter, "path", "", "Path to filter for")
- flag.BoolVar(&singleton.PprofEnable, "pprof", false, "Enable profiling")
+ flag.BoolVar(&cfg.PprofEnable, "pprof", false, "Enable profiling")
tracepointsToAttach := flag.String("tps", "", "Comma separated list regexes for tracepoints to load")
tracepointsToExclude := flag.String("tpsExclude", "", "Comma separated list regexes for tracepoints to exclude")
- flag.BoolVar(&singleton.PlainMode, "plain", false, "Enable plain CSV output mode (disable TUI)")
- flag.BoolVar(&singleton.FlamegraphEnable, "flamegraph", false, "Enable flamegraph builder")
- flag.BoolVar(&singleton.LiveFlamegraph, "live", false, "Enable live flamegraph mode")
- flag.DurationVar(&singleton.LiveInterval, "live-interval", 200*time.Millisecond, "Live flamegraph refresh interval")
- flag.StringVar(&singleton.OpenCommand, "open", "", "Command to open live flamegraph URL (used with -live); use {url} placeholder or URL is appended")
- flag.StringVar(&singleton.FlamegraphName, "name", "default", "Name of the flamegraph, used to generate the SVG file")
- flag.BoolVar(&singleton.FlamegraphJSON, "flamegraphJson", false, "Also export flamegraph tree as JSON in -ior mode (experimental WASM-ready output)")
- flag.BoolVar(&singleton.TUIExportEnable, "tuiExport", true, "Enable writing TUI snapshot export files")
-
- flag.StringVar(&singleton.IorDataFile, "ior", "", "IOR data file to convert into native SVG flamegraph")
- flag.DurationVar(&singleton.IorWatchInterval, "iorWatchInterval", 0,
- "In -ior mode, poll input file for changes and regenerate outputs; also enables auto-reloading viewer")
+ flag.BoolVar(&cfg.PlainMode, "plain", false, "Enable plain CSV output mode (disable TUI)")
+ flag.BoolVar(&cfg.TestFlames, "testflames", false, "Run TUI with static synthetic flamegraph data for keyboard-navigation testing")
+ flag.BoolVar(&cfg.TestLiveFlames, "testliveflames", false, "Run TUI with continuously-updating synthetic flamegraph data for live keyboard-navigation testing")
+ flag.DurationVar(&cfg.LiveInterval, "live-interval", cfg.LiveInterval, "Synthetic live flamegraph refresh interval for --testliveflames")
+ flag.BoolVar(&cfg.TUIExportEnable, "tuiExport", cfg.TUIExportEnable, "Enable writing TUI snapshot export files")
fields := flag.String("fields", "",
- fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validCollapsedFields))
- flag.StringVar(&singleton.CountField, "count", "count",
- fmt.Sprintf("Count field to collapse, valid are: %v", validCollapsedCounts))
+ fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validFields))
+ flag.StringVar(&cfg.CountField, "count", cfg.CountField,
+ fmt.Sprintf("Count field to collapse, valid are: %v", validCounts))
if err := flag.CommandLine.Parse(os.Args[1:]); err != nil {
return err
}
- pidFilter.Store(int64(singleton.PidFilter))
- tidFilter.Store(int64(singleton.TidFilter))
- tuiExportEnable.Store(singleton.TUIExportEnable)
var err error
- singleton.TracepointsToAttach, err = extractTracepointFlags(*tracepointsToAttach)
+ cfg.TracepointsToAttach, err = extractTracepointFlags(*tracepointsToAttach)
if err != nil {
return err
}
- singleton.TracepointsToExclude, err = extractTracepointFlags(*tracepointsToExclude)
+ cfg.TracepointsToExclude, err = extractTracepointFlags(*tracepointsToExclude)
if err != nil {
return err
}
@@ -160,21 +191,22 @@ func parse() error {
// If future kernels regress, add targeted exclusions here.
if *fields == "" {
- singleton.CollapsedFields = []string{"comm", "path", "tracepoint"}
+ cfg.CollapsedFields = []string{"comm", "tracepoint", "path"}
} else {
- singleton.CollapsedFields = strings.Split(*fields, ",")
+ cfg.CollapsedFields = strings.Split(*fields, ",")
}
- for _, field := range singleton.CollapsedFields {
- if !slices.Contains(validCollapsedFields, field) {
+ for _, field := range cfg.CollapsedFields {
+ if !collapse.IsValidField(field) {
return fmt.Errorf("invalid field for collapse: %s", field)
}
}
- if !slices.Contains(validCollapsedCounts, singleton.CountField) {
- return fmt.Errorf("invalid count field: %s", singleton.CountField)
+ if !collapse.IsValidCountField(cfg.CountField) {
+ return fmt.Errorf("invalid count field: %s", cfg.CountField)
}
+ setCurrent(cfg)
return nil
}
@@ -192,7 +224,7 @@ func extractTracepointFlags(tracepoints string) (regexes []*regexp.Regexp, err e
return regexes, nil
}
-func (flags Flags) ShouldIAttachTracepoint(tracepointName string) bool {
+func (flags Config) ShouldIAttachTracepoint(tracepointName string) bool {
for _, re := range flags.TracepointsToExclude {
if re.MatchString(tracepointName) {
return false
diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go
index 54c65b8..2469068 100644
--- a/internal/flags/flags_test.go
+++ b/internal/flags/flags_test.go
@@ -9,114 +9,96 @@ import (
"time"
)
-func parseForTest(t *testing.T, args ...string) (Flags, error) {
+func parseForTest(t *testing.T, args ...string) (Config, error) {
t.Helper()
oldCommandLine := flag.CommandLine
oldArgs := os.Args
- oldSingleton := singleton
+ oldCurrent := Get()
oldParseErr := parseErr
- oldPID := pidFilter.Load()
- oldTID := tidFilter.Load()
- oldTUIExport := tuiExportEnable.Load()
fs := flag.NewFlagSet("ior-test", flag.ContinueOnError)
fs.SetOutput(io.Discard)
flag.CommandLine = fs
os.Args = append([]string{"ior"}, args...)
- singleton = Flags{TUIExportEnable: true}
+ setCurrent(NewFlags())
parseErr = nil
- pidFilter.Store(-1)
- tidFilter.Store(-1)
- tuiExportEnable.Store(true)
err := parse()
- cfg := singleton
+ cfg := Get()
t.Cleanup(func() {
flag.CommandLine = oldCommandLine
os.Args = oldArgs
- singleton = oldSingleton
+ setCurrent(oldCurrent)
parseErr = oldParseErr
- pidFilter.Store(oldPID)
- tidFilter.Store(oldTID)
- tuiExportEnable.Store(oldTUIExport)
})
return cfg, err
}
-func TestParseLiveFlagsAndInterval(t *testing.T) {
- cfg, err := parseForTest(t, "-live", "-live-interval", "200ms", "-pid", "1234")
+func TestParseLiveIntervalAndPID(t *testing.T) {
+ cfg, err := parseForTest(t, "-live-interval", "200ms", "-pid", "1234")
if err != nil {
t.Fatalf("parse returned error: %v", err)
}
- if !cfg.LiveFlamegraph {
- t.Fatalf("expected -live to enable live mode")
- }
if cfg.LiveInterval != 200*time.Millisecond {
t.Fatalf("live interval = %v, want %v", cfg.LiveInterval, 200*time.Millisecond)
}
if cfg.PidFilter != 1234 {
t.Fatalf("pid filter = %d, want 1234", cfg.PidFilter)
}
- if got := int(pidFilter.Load()); got != 1234 {
- t.Fatalf("global pid filter = %d, want 1234", got)
- }
- if cfg.OpenCommand != "" {
- t.Fatalf("expected empty open command by default")
+ if got := Get().GetPidFilter(); got != 1234 {
+ t.Fatalf("Get().GetPidFilter() = %d, want 1234", got)
}
}
-func TestParseLiveDefaults(t *testing.T) {
- cfg, err := parseForTest(t)
- if err != nil {
- t.Fatalf("parse returned error: %v", err)
+func TestNewFlagsDefaultsAndGetters(t *testing.T) {
+ cfg := NewFlags()
+ if cfg.GetPidFilter() != -1 {
+ t.Fatalf("GetPidFilter() = %d, want -1", cfg.GetPidFilter())
}
-
- if cfg.LiveFlamegraph {
- t.Fatalf("expected live mode disabled by default")
+ if cfg.GetTidFilter() != -1 {
+ t.Fatalf("GetTidFilter() = %d, want -1", cfg.GetTidFilter())
}
- if cfg.LiveInterval != 200*time.Millisecond {
- t.Fatalf("default live interval = %v, want %v", cfg.LiveInterval, 200*time.Millisecond)
+ if !cfg.GetTUIExportEnable() {
+ t.Fatalf("GetTUIExportEnable() = false, want true")
}
- if cfg.OpenCommand != "" {
- t.Fatalf("expected empty open command by default")
+ if cfg.CountField != "count" {
+ t.Fatalf("CountField = %q, want count", cfg.CountField)
}
}
-func TestParseOpenFlags(t *testing.T) {
- cfg, err := parseForTest(t, "-live", "-open", "chromium --new-window")
+func TestParseLiveDefaults(t *testing.T) {
+ cfg, err := parseForTest(t)
if err != nil {
t.Fatalf("parse returned error: %v", err)
}
- if !cfg.LiveFlamegraph {
- t.Fatalf("expected live mode enabled")
- }
- if cfg.OpenCommand != "chromium --new-window" {
- t.Fatalf("open command = %q, want %q", cfg.OpenCommand, "chromium --new-window")
+
+ if cfg.LiveInterval != 200*time.Millisecond {
+ t.Fatalf("default live interval = %v, want %v", cfg.LiveInterval, 200*time.Millisecond)
}
}
-func TestParseFlamegraphJSONFlag(t *testing.T) {
- cfg, err := parseForTest(t, "-flamegraphJson")
+func TestParseTestFlamesFlag(t *testing.T) {
+ cfg, err := parseForTest(t, "--testflames")
if err != nil {
t.Fatalf("parse returned error: %v", err)
}
- if !cfg.FlamegraphJSON {
- t.Fatalf("expected -flamegraphJson to enable JSON export")
+ if !cfg.TestFlames {
+ t.Fatalf("expected --testflames to enable static flamegraph test mode")
}
}
-func TestParseIorWatchIntervalFlag(t *testing.T) {
- cfg, err := parseForTest(t, "-iorWatchInterval", "2s")
+func TestParseTestLiveFlamesFlag(t *testing.T) {
+ cfg, err := parseForTest(t, "--testliveflames")
if err != nil {
t.Fatalf("parse returned error: %v", err)
}
- if cfg.IorWatchInterval != 2*time.Second {
- t.Fatalf("ior watch interval = %v, want %v", cfg.IorWatchInterval, 2*time.Second)
+ if !cfg.TestLiveFlames {
+ t.Fatalf("expected --testliveflames to enable synthetic live flamegraph test mode")
}
}
@@ -126,7 +108,7 @@ func TestParseDefaultCollapsedFieldsOrder(t *testing.T) {
t.Fatalf("parse returned error: %v", err)
}
- want := []string{"comm", "path", "tracepoint"}
+ want := []string{"comm", "tracepoint", "path"}
if len(cfg.CollapsedFields) != len(want) {
t.Fatalf("default collapsed fields len = %d, want %d", len(cfg.CollapsedFields), len(want))
}