diff options
| -rw-r--r-- | internal/flags/flags.go | 154 | ||||
| -rw-r--r-- | internal/flags/flags_test.go | 37 |
2 files changed, 128 insertions, 63 deletions
diff --git a/internal/flags/flags.go b/internal/flags/flags.go index b2d9dce..bd21768 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -13,20 +13,14 @@ import ( ) var ( - singleton = Flags{ - TUIExportEnable: true, - } - once sync.Once - parseErr error - pidFilter atomic.Int64 - tidFilter atomic.Int64 - tuiExportEnable atomic.Bool + current atomic.Pointer[Flags] + once sync.Once + parseErr error ) func init() { - pidFilter.Store(-1) - tidFilter.Store(-1) - tuiExportEnable.Store(true) + defaults := NewFlags() + current.Store(&defaults) } var ( @@ -77,27 +71,91 @@ type Flags struct { CountField string } -func Get() Flags { - out := singleton - out.PidFilter = int(pidFilter.Load()) - out.TidFilter = int(tidFilter.Load()) - out.TUIExportEnable = tuiExportEnable.Load() +// NewFlags returns a configuration instance initialized with project defaults. +func NewFlags() Flags { + return Flags{ + PidFilter: -1, + TidFilter: -1, + EventMapSize: 4096 * 16, + Duration: 900, + LiveInterval: 200 * time.Millisecond, + FlamegraphName: "default", + TUIExportEnable: true, + CollapsedFields: []string{"comm", "path", "tracepoint"}, + CountField: "count", + } +} + +// GetPidFilter returns the active process filter. +func (f Flags) GetPidFilter() int { + return f.PidFilter +} + +// GetTidFilter returns the active thread filter. +func (f Flags) GetTidFilter() int { + return f.TidFilter +} + +// GetTUIExportEnable reports whether TUI CSV export is enabled. +func (f Flags) GetTUIExportEnable() bool { + return f.TUIExportEnable +} + +func (f Flags) clone() Flags { + out := f + out.TracepointsToAttach = slices.Clone(f.TracepointsToAttach) + out.TracepointsToExclude = slices.Clone(f.TracepointsToExclude) + out.CollapsedFields = slices.Clone(f.CollapsedFields) return out } +func Get() Flags { + cfg := current.Load() + if cfg == nil { + return NewFlags() + } + return cfg.clone() +} + +func setCurrent(cfg Flags) { + snapshot := cfg.clone() + current.Store(&snapshot) +} + +func updateCurrent(update func(*Flags)) { + 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 *Flags) { + 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 *Flags) { + cfg.TidFilter = tid + }) } // SetTUIExportEnable toggles TUI snapshot export file writing. func SetTUIExportEnable(enabled bool) { - tuiExportEnable.Store(enabled) + updateCurrent(func(cfg *Flags) { + cfg.TUIExportEnable = enabled + }) } func Parse() error { @@ -108,48 +166,47 @@ 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() + + 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, + flag.BoolVar(&cfg.PlainMode, "plain", false, "Enable plain CSV output mode (disable TUI)") + flag.BoolVar(&cfg.FlamegraphEnable, "flamegraph", false, "Enable flamegraph builder") + flag.BoolVar(&cfg.LiveFlamegraph, "live", false, "Enable live flamegraph mode") + flag.DurationVar(&cfg.LiveInterval, "live-interval", cfg.LiveInterval, "Live flamegraph refresh interval") + flag.StringVar(&cfg.OpenCommand, "open", "", "Command to open live flamegraph URL (used with -live); use {url} placeholder or URL is appended") + flag.StringVar(&cfg.FlamegraphName, "name", cfg.FlamegraphName, "Name of the flamegraph, used to generate the SVG file") + flag.BoolVar(&cfg.FlamegraphJSON, "flamegraphJson", false, "Also export flamegraph tree as JSON in -ior mode (experimental WASM-ready output)") + flag.BoolVar(&cfg.TUIExportEnable, "tuiExport", cfg.TUIExportEnable, "Enable writing TUI snapshot export files") + + flag.StringVar(&cfg.IorDataFile, "ior", "", "IOR data file to convert into native SVG flamegraph") + flag.DurationVar(&cfg.IorWatchInterval, "iorWatchInterval", 0, "In -ior mode, poll input file for changes and regenerate outputs; also enables auto-reloading viewer") fields := flag.String("fields", "", fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validCollapsedFields)) - flag.StringVar(&singleton.CountField, "count", "count", + flag.StringVar(&cfg.CountField, "count", cfg.CountField, fmt.Sprintf("Count field to collapse, valid are: %v", validCollapsedCounts)) 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 +217,22 @@ func parse() error { // If future kernels regress, add targeted exclusions here. if *fields == "" { - singleton.CollapsedFields = []string{"comm", "path", "tracepoint"} + cfg.CollapsedFields = []string{"comm", "path", "tracepoint"} } else { - singleton.CollapsedFields = strings.Split(*fields, ",") + cfg.CollapsedFields = strings.Split(*fields, ",") } - for _, field := range singleton.CollapsedFields { + for _, field := range cfg.CollapsedFields { if !slices.Contains(validCollapsedFields, 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 !slices.Contains(validCollapsedCounts, cfg.CountField) { + return fmt.Errorf("invalid count field: %s", cfg.CountField) } + setCurrent(cfg) return nil } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 54c65b8..3534916 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -14,34 +14,25 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) { 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 @@ -62,14 +53,30 @@ func TestParseLiveFlagsAndInterval(t *testing.T) { 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 got := Get().GetPidFilter(); got != 1234 { + t.Fatalf("Get().GetPidFilter() = %d, want 1234", got) } if cfg.OpenCommand != "" { t.Fatalf("expected empty open command by default") } } +func TestNewFlagsDefaultsAndGetters(t *testing.T) { + cfg := NewFlags() + if cfg.GetPidFilter() != -1 { + t.Fatalf("GetPidFilter() = %d, want -1", cfg.GetPidFilter()) + } + if cfg.GetTidFilter() != -1 { + t.Fatalf("GetTidFilter() = %d, want -1", cfg.GetTidFilter()) + } + if !cfg.GetTUIExportEnable() { + t.Fatalf("GetTUIExportEnable() = false, want true") + } + if cfg.CountField != "count" { + t.Fatalf("CountField = %q, want count", cfg.CountField) + } +} + func TestParseLiveDefaults(t *testing.T) { cfg, err := parseForTest(t) if err != nil { |
