package driver import ( "fmt" "net/url" "reflect" "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"` // 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: "functions", } } // 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", } def := defaultConfig() configFieldMap = map[string]configField{} t := reflect.TypeOf(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. for _, choice := range f.choices { if choice == 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 }