| package driver | |
| import ( | |
| "fmt" | |
| "net/url" | |
| "reflect" | |
| "slices" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| ) | |
| // config holds settings for a single named config. | |
| // The JSON tag name for a field is used both for JSON encoding and as | |
| // a named variable. | |
| type config struct { | |
| // Filename for file-based output formats, stdout by default. | |
| Output string `json:"-"` | |
| // Display options. | |
| CallTree bool `json:"call_tree,omitempty"` | |
| RelativePercentages bool `json:"relative_percentages,omitempty"` | |
| Unit string `json:"unit,omitempty"` | |
| CompactLabels bool `json:"compact_labels,omitempty"` | |
| SourcePath string `json:"-"` | |
| TrimPath string `json:"-"` | |
| IntelSyntax bool `json:"intel_syntax,omitempty"` | |
| Mean bool `json:"mean,omitempty"` | |
| SampleIndex string `json:"-"` | |
| DivideBy float64 `json:"-"` | |
| Normalize bool `json:"normalize,omitempty"` | |
| Sort string `json:"sort,omitempty"` | |
| // Label pseudo stack frame generation options | |
| TagRoot string `json:"tagroot,omitempty"` | |
| TagLeaf string `json:"tagleaf,omitempty"` | |
| // Filtering options | |
| DropNegative bool `json:"drop_negative,omitempty"` | |
| NodeCount int `json:"nodecount,omitempty"` | |
| NodeFraction float64 `json:"nodefraction,omitempty"` | |
| EdgeFraction float64 `json:"edgefraction,omitempty"` | |
| Trim bool `json:"trim,omitempty"` | |
| Focus string `json:"focus,omitempty"` | |
| Ignore string `json:"ignore,omitempty"` | |
| PruneFrom string `json:"prune_from,omitempty"` | |
| Hide string `json:"hide,omitempty"` | |
| Show string `json:"show,omitempty"` | |
| ShowFrom string `json:"show_from,omitempty"` | |
| TagFocus string `json:"tagfocus,omitempty"` | |
| TagIgnore string `json:"tagignore,omitempty"` | |
| TagShow string `json:"tagshow,omitempty"` | |
| TagHide string `json:"taghide,omitempty"` | |
| NoInlines bool `json:"noinlines,omitempty"` | |
| ShowColumns bool `json:"showcolumns,omitempty"` | |
| // Output granularity | |
| Granularity string `json:"granularity,omitempty"` | |
| } | |
| // defaultConfig returns the default configuration values; it is unaffected by | |
| // flags and interactive assignments. | |
| func defaultConfig() config { | |
| return config{ | |
| Unit: "minimum", | |
| NodeCount: -1, | |
| NodeFraction: 0.005, | |
| EdgeFraction: 0.001, | |
| Trim: true, | |
| DivideBy: 1.0, | |
| Sort: "flat", | |
| Granularity: "", // Default depends on the display format | |
| } | |
| } | |
| // currentConfig holds the current configuration values; it is affected by | |
| // flags and interactive assignments. | |
| var currentCfg = defaultConfig() | |
| var currentMu sync.Mutex | |
| func currentConfig() config { | |
| currentMu.Lock() | |
| defer currentMu.Unlock() | |
| return currentCfg | |
| } | |
| func setCurrentConfig(cfg config) { | |
| currentMu.Lock() | |
| defer currentMu.Unlock() | |
| currentCfg = cfg | |
| } | |
| // configField contains metadata for a single configuration field. | |
| type configField struct { | |
| name string // JSON field name/key in variables | |
| urlparam string // URL parameter name | |
| saved bool // Is field saved in settings? | |
| field reflect.StructField // Field in config | |
| choices []string // Name Of variables in group | |
| defaultValue string // Default value for this field. | |
| } | |
| var ( | |
| configFields []configField // Precomputed metadata per config field | |
| // configFieldMap holds an entry for every config field as well as an | |
| // entry for every valid choice for a multi-choice field. | |
| configFieldMap map[string]configField | |
| ) | |
| func init() { | |
| // Config names for fields that are not saved in settings and therefore | |
| // do not have a JSON name. | |
| notSaved := map[string]string{ | |
| // Not saved in settings, but present in URLs. | |
| "SampleIndex": "sample_index", | |
| // Following fields are also not placed in URLs. | |
| "Output": "output", | |
| "SourcePath": "source_path", | |
| "TrimPath": "trim_path", | |
| "DivideBy": "divide_by", | |
| } | |
| // choices holds the list of allowed values for config fields that can | |
| // take on one of a bounded set of values. | |
| choices := map[string][]string{ | |
| "sort": {"cum", "flat"}, | |
| "granularity": {"functions", "filefunctions", "files", "lines", "addresses"}, | |
| } | |
| // urlparam holds the mapping from a config field name to the URL | |
| // parameter used to hold that config field. If no entry is present for | |
| // a name, the corresponding field is not saved in URLs. | |
| urlparam := map[string]string{ | |
| "drop_negative": "dropneg", | |
| "call_tree": "calltree", | |
| "relative_percentages": "rel", | |
| "unit": "unit", | |
| "compact_labels": "compact", | |
| "intel_syntax": "intel", | |
| "nodecount": "n", | |
| "nodefraction": "nf", | |
| "edgefraction": "ef", | |
| "trim": "trim", | |
| "focus": "f", | |
| "ignore": "i", | |
| "prune_from": "prunefrom", | |
| "hide": "h", | |
| "show": "s", | |
| "show_from": "sf", | |
| "tagfocus": "tf", | |
| "tagignore": "ti", | |
| "tagshow": "ts", | |
| "taghide": "th", | |
| "mean": "mean", | |
| "sample_index": "si", | |
| "normalize": "norm", | |
| "sort": "sort", | |
| "granularity": "g", | |
| "noinlines": "noinlines", | |
| "showcolumns": "showcolumns", | |
| } | |
| def := defaultConfig() | |
| configFieldMap = map[string]configField{} | |
| t := reflect.TypeFor[config]() | |
| for i, n := 0, t.NumField(); i < n; i++ { | |
| field := t.Field(i) | |
| js := strings.Split(field.Tag.Get("json"), ",") | |
| if len(js) == 0 { | |
| continue | |
| } | |
| // Get the configuration name for this field. | |
| name := js[0] | |
| if name == "-" { | |
| name = notSaved[field.Name] | |
| if name == "" { | |
| // Not a configurable field. | |
| continue | |
| } | |
| } | |
| f := configField{ | |
| name: name, | |
| urlparam: urlparam[name], | |
| saved: (name == js[0]), | |
| field: field, | |
| choices: choices[name], | |
| } | |
| f.defaultValue = def.get(f) | |
| configFields = append(configFields, f) | |
| configFieldMap[f.name] = f | |
| for _, choice := range f.choices { | |
| configFieldMap[choice] = f | |
| } | |
| } | |
| } | |
| // fieldPtr returns a pointer to the field identified by f in *cfg. | |
| func (cfg *config) fieldPtr(f configField) interface{} { | |
| // reflect.ValueOf: converts to reflect.Value | |
| // Elem: dereferences cfg to make *cfg | |
| // FieldByIndex: fetches the field | |
| // Addr: takes address of field | |
| // Interface: converts back from reflect.Value to a regular value | |
| return reflect.ValueOf(cfg).Elem().FieldByIndex(f.field.Index).Addr().Interface() | |
| } | |
| // get returns the value of field f in cfg. | |
| func (cfg *config) get(f configField) string { | |
| switch ptr := cfg.fieldPtr(f).(type) { | |
| case *string: | |
| return *ptr | |
| case *int: | |
| return fmt.Sprint(*ptr) | |
| case *float64: | |
| return fmt.Sprint(*ptr) | |
| case *bool: | |
| return fmt.Sprint(*ptr) | |
| } | |
| panic(fmt.Sprintf("unsupported config field type %v", f.field.Type)) | |
| } | |
| // set sets the value of field f in cfg to value. | |
| func (cfg *config) set(f configField, value string) error { | |
| switch ptr := cfg.fieldPtr(f).(type) { | |
| case *string: | |
| if len(f.choices) > 0 { | |
| // Verify that value is one of the allowed choices. | |
| if slices.Contains(f.choices, value) { | |
| *ptr = value | |
| return nil | |
| } | |
| return fmt.Errorf("invalid %q value %q", f.name, value) | |
| } | |
| *ptr = value | |
| case *int: | |
| v, err := strconv.Atoi(value) | |
| if err != nil { | |
| return err | |
| } | |
| *ptr = v | |
| case *float64: | |
| v, err := strconv.ParseFloat(value, 64) | |
| if err != nil { | |
| return err | |
| } | |
| *ptr = v | |
| case *bool: | |
| v, err := stringToBool(value) | |
| if err != nil { | |
| return err | |
| } | |
| *ptr = v | |
| default: | |
| panic(fmt.Sprintf("unsupported config field type %v", f.field.Type)) | |
| } | |
| return nil | |
| } | |
| // isConfigurable returns true if name is either the name of a config field, or | |
| // a valid value for a multi-choice config field. | |
| func isConfigurable(name string) bool { | |
| _, ok := configFieldMap[name] | |
| return ok | |
| } | |
| // isBoolConfig returns true if name is either name of a boolean config field, | |
| // or a valid value for a multi-choice config field. | |
| func isBoolConfig(name string) bool { | |
| f, ok := configFieldMap[name] | |
| if !ok { | |
| return false | |
| } | |
| if name != f.name { | |
| return true // name must be one possible value for the field | |
| } | |
| var cfg config | |
| _, ok = cfg.fieldPtr(f).(*bool) | |
| return ok | |
| } | |
| // completeConfig returns the list of configurable names starting with prefix. | |
| func completeConfig(prefix string) []string { | |
| var result []string | |
| for v := range configFieldMap { | |
| if strings.HasPrefix(v, prefix) { | |
| result = append(result, v) | |
| } | |
| } | |
| return result | |
| } | |
| // configure stores the name=value mapping into the current config, correctly | |
| // handling the case when name identifies a particular choice in a field. | |
| func configure(name, value string) error { | |
| currentMu.Lock() | |
| defer currentMu.Unlock() | |
| f, ok := configFieldMap[name] | |
| if !ok { | |
| return fmt.Errorf("unknown config field %q", name) | |
| } | |
| if f.name == name { | |
| return currentCfg.set(f, value) | |
| } | |
| // name must be one of the choices. If value is true, set field-value | |
| // to name. | |
| if v, err := strconv.ParseBool(value); v && err == nil { | |
| return currentCfg.set(f, name) | |
| } | |
| return fmt.Errorf("unknown config field %q", name) | |
| } | |
| // resetTransient sets all transient fields in *cfg to their currently | |
| // configured values. | |
| func (cfg *config) resetTransient() { | |
| current := currentConfig() | |
| cfg.Output = current.Output | |
| cfg.SourcePath = current.SourcePath | |
| cfg.TrimPath = current.TrimPath | |
| cfg.DivideBy = current.DivideBy | |
| cfg.SampleIndex = current.SampleIndex | |
| } | |
| // applyURL updates *cfg based on params. | |
| func (cfg *config) applyURL(params url.Values) error { | |
| for _, f := range configFields { | |
| var value string | |
| if f.urlparam != "" { | |
| value = params.Get(f.urlparam) | |
| } | |
| if value == "" { | |
| continue | |
| } | |
| if err := cfg.set(f, value); err != nil { | |
| return fmt.Errorf("error setting config field %s: %v", f.name, err) | |
| } | |
| } | |
| return nil | |
| } | |
| // makeURL returns a URL based on initialURL that contains the config contents | |
| // as parameters. The second result is true iff a parameter value was changed. | |
| func (cfg *config) makeURL(initialURL url.URL) (url.URL, bool) { | |
| q := initialURL.Query() | |
| changed := false | |
| for _, f := range configFields { | |
| if f.urlparam == "" || !f.saved { | |
| continue | |
| } | |
| v := cfg.get(f) | |
| if v == f.defaultValue { | |
| v = "" // URL for of default value is the empty string. | |
| } else if f.field.Type.Kind() == reflect.Bool { | |
| // Shorten bool values to "f" or "t" | |
| v = v[:1] | |
| } | |
| if q.Get(f.urlparam) == v { | |
| continue | |
| } | |
| changed = true | |
| if v == "" { | |
| q.Del(f.urlparam) | |
| } else { | |
| q.Set(f.urlparam, v) | |
| } | |
| } | |
| if changed { | |
| initialURL.RawQuery = q.Encode() | |
| } | |
| return initialURL, changed | |
| } |