blob: 599599f3b05ad2d5e1c414b4ef4da11ca66e7713 [file] [log] [blame]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00001// Package experiment provides support for experimental features.
2package experiment
3
4import (
5 "fmt"
6 "io"
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -07007 "log/slog"
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -07008 "os"
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +00009 "strings"
10 "sync"
11)
12
13// Experiment represents an experimental feature.
14// Experiments are global.
15type Experiment struct {
16 Name string // The name of the experiment used in -x flag
17 Description string // A short description of what the experiment does
18 Enabled bool // Whether the experiment is enabled
19}
20
21var (
22 mu sync.Mutex
23 experiments = []Experiment{
24 {
25 Name: "list",
26 Description: "List all available experiments and exit",
27 },
28 {
29 Name: "all",
30 Description: "Enable all experiments",
31 },
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000032 }
33 byName = map[string]*Experiment{}
34)
35
36func Enabled(name string) bool {
37 mu.Lock()
38 defer mu.Unlock()
Josh Bleecher Snyder6f446fa2025-05-05 14:51:49 -070039 e, ok := byName[name]
40 if !ok {
41 slog.Error("unknown experiment", "name", name)
42 return false
43 }
44 return e.Enabled
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000045}
46
47func init() {
Josh Bleecher Snydere01ea0e2025-05-06 02:03:26 +000048 for i := range experiments {
49 e := &experiments[i]
50 byName[e.Name] = e
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000051 }
52}
53
54func (e Experiment) String() string {
55 return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
56}
57
58// Fprint writes a list of all available experiments to w.
59func Fprint(w io.Writer) {
60 mu.Lock()
61 defer mu.Unlock()
62
63 fmt.Fprintln(w, "Available experiments:")
64 for _, e := range experiments {
65 fmt.Fprintln(w, e)
66 }
67}
68
69// Flag is a custom flag type that allows for comma-separated
70// values and can be used multiple times.
71type Flag struct {
72 Value string
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070073 set bool // whether the flag has been set explicitly
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000074}
75
76// String returns the string representation of the flag value.
77func (f *Flag) String() string {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070078 return f.Get().(string)
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000079}
80
81// Set adds a value to the flag.
82func (f *Flag) Set(value string) error {
Josh Bleecher Snyder085f74c2025-05-06 01:58:25 +000083 if f.Value == "" {
84 f.Value = value
85 } else {
86 f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
87 }
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070088 f.set = true
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000089 return nil
90}
91
92// Get returns the flag values.
93func (f *Flag) Get() any {
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -070094 if f.set {
95 return f.Value
96 }
97 return os.Getenv("SKETCH_EXPERIMENT")
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +000098}
99
100// Process handles all flag values, enabling the appropriate experiments.
101func (f *Flag) Process() error {
102 mu.Lock()
103 defer mu.Unlock()
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700104 v := f.String()
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000105
Josh Bleecher Snyder436655e2025-05-14 10:20:25 -0700106 for name := range strings.SplitSeq(v, ",") {
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000107 name = strings.TrimSpace(name)
108 if name == "" {
109 continue
110 }
111 e, ok := byName[name]
112 if !ok {
113 return fmt.Errorf("unknown experiment: %q", name)
114 }
115 e.Enabled = true
116 }
117 if byName["all"].Enabled {
Josh Bleecher Snyderd42577f2025-05-05 17:59:00 -0700118 for i := range experiments {
119 e := &experiments[i]
Josh Bleecher Snyderb4782142025-05-05 19:16:54 +0000120 if e.Name == "list" {
121 continue
122 }
123 e.Enabled = true
124 }
125 }
126 return nil
127}