1package driver
2
3import (
4	"fmt"
5	"net/url"
6	"reflect"
7	"strconv"
8	"strings"
9	"sync"
10)
11
12// config holds settings for a single named config.
13// The JSON tag name for a field is used both for JSON encoding and as
14// a named variable.
15type config struct {
16	// Filename for file-based output formats, stdout by default.
17	Output string `json:"-"`
18
19	// Display options.
20	CallTree            bool    `json:"call_tree,omitempty"`
21	RelativePercentages bool    `json:"relative_percentages,omitempty"`
22	Unit                string  `json:"unit,omitempty"`
23	CompactLabels       bool    `json:"compact_labels,omitempty"`
24	SourcePath          string  `json:"-"`
25	TrimPath            string  `json:"-"`
26	IntelSyntax         bool    `json:"intel_syntax,omitempty"`
27	Mean                bool    `json:"mean,omitempty"`
28	SampleIndex         string  `json:"-"`
29	DivideBy            float64 `json:"-"`
30	Normalize           bool    `json:"normalize,omitempty"`
31	Sort                string  `json:"sort,omitempty"`
32
33	// Label pseudo stack frame generation options
34	TagRoot string `json:"tagroot,omitempty"`
35	TagLeaf string `json:"tagleaf,omitempty"`
36
37	// Filtering options
38	DropNegative bool    `json:"drop_negative,omitempty"`
39	NodeCount    int     `json:"nodecount,omitempty"`
40	NodeFraction float64 `json:"nodefraction,omitempty"`
41	EdgeFraction float64 `json:"edgefraction,omitempty"`
42	Trim         bool    `json:"trim,omitempty"`
43	Focus        string  `json:"focus,omitempty"`
44	Ignore       string  `json:"ignore,omitempty"`
45	PruneFrom    string  `json:"prune_from,omitempty"`
46	Hide         string  `json:"hide,omitempty"`
47	Show         string  `json:"show,omitempty"`
48	ShowFrom     string  `json:"show_from,omitempty"`
49	TagFocus     string  `json:"tagfocus,omitempty"`
50	TagIgnore    string  `json:"tagignore,omitempty"`
51	TagShow      string  `json:"tagshow,omitempty"`
52	TagHide      string  `json:"taghide,omitempty"`
53	NoInlines    bool    `json:"noinlines,omitempty"`
54
55	// Output granularity
56	Granularity string `json:"granularity,omitempty"`
57}
58
59// defaultConfig returns the default configuration values; it is unaffected by
60// flags and interactive assignments.
61func defaultConfig() config {
62	return config{
63		Unit:         "minimum",
64		NodeCount:    -1,
65		NodeFraction: 0.005,
66		EdgeFraction: 0.001,
67		Trim:         true,
68		DivideBy:     1.0,
69		Sort:         "flat",
70		Granularity:  "functions",
71	}
72}
73
74// currentConfig holds the current configuration values; it is affected by
75// flags and interactive assignments.
76var currentCfg = defaultConfig()
77var currentMu sync.Mutex
78
79func currentConfig() config {
80	currentMu.Lock()
81	defer currentMu.Unlock()
82	return currentCfg
83}
84
85func setCurrentConfig(cfg config) {
86	currentMu.Lock()
87	defer currentMu.Unlock()
88	currentCfg = cfg
89}
90
91// configField contains metadata for a single configuration field.
92type configField struct {
93	name         string              // JSON field name/key in variables
94	urlparam     string              // URL parameter name
95	saved        bool                // Is field saved in settings?
96	field        reflect.StructField // Field in config
97	choices      []string            // Name Of variables in group
98	defaultValue string              // Default value for this field.
99}
100
101var (
102	configFields []configField // Precomputed metadata per config field
103
104	// configFieldMap holds an entry for every config field as well as an
105	// entry for every valid choice for a multi-choice field.
106	configFieldMap map[string]configField
107)
108
109func init() {
110	// Config names for fields that are not saved in settings and therefore
111	// do not have a JSON name.
112	notSaved := map[string]string{
113		// Not saved in settings, but present in URLs.
114		"SampleIndex": "sample_index",
115
116		// Following fields are also not placed in URLs.
117		"Output":     "output",
118		"SourcePath": "source_path",
119		"TrimPath":   "trim_path",
120		"DivideBy":   "divide_by",
121	}
122
123	// choices holds the list of allowed values for config fields that can
124	// take on one of a bounded set of values.
125	choices := map[string][]string{
126		"sort":        {"cum", "flat"},
127		"granularity": {"functions", "filefunctions", "files", "lines", "addresses"},
128	}
129
130	// urlparam holds the mapping from a config field name to the URL
131	// parameter used to hold that config field. If no entry is present for
132	// a name, the corresponding field is not saved in URLs.
133	urlparam := map[string]string{
134		"drop_negative":        "dropneg",
135		"call_tree":            "calltree",
136		"relative_percentages": "rel",
137		"unit":                 "unit",
138		"compact_labels":       "compact",
139		"intel_syntax":         "intel",
140		"nodecount":            "n",
141		"nodefraction":         "nf",
142		"edgefraction":         "ef",
143		"trim":                 "trim",
144		"focus":                "f",
145		"ignore":               "i",
146		"prune_from":           "prunefrom",
147		"hide":                 "h",
148		"show":                 "s",
149		"show_from":            "sf",
150		"tagfocus":             "tf",
151		"tagignore":            "ti",
152		"tagshow":              "ts",
153		"taghide":              "th",
154		"mean":                 "mean",
155		"sample_index":         "si",
156		"normalize":            "norm",
157		"sort":                 "sort",
158		"granularity":          "g",
159		"noinlines":            "noinlines",
160	}
161
162	def := defaultConfig()
163	configFieldMap = map[string]configField{}
164	t := reflect.TypeOf(config{})
165	for i, n := 0, t.NumField(); i < n; i++ {
166		field := t.Field(i)
167		js := strings.Split(field.Tag.Get("json"), ",")
168		if len(js) == 0 {
169			continue
170		}
171		// Get the configuration name for this field.
172		name := js[0]
173		if name == "-" {
174			name = notSaved[field.Name]
175			if name == "" {
176				// Not a configurable field.
177				continue
178			}
179		}
180		f := configField{
181			name:     name,
182			urlparam: urlparam[name],
183			saved:    (name == js[0]),
184			field:    field,
185			choices:  choices[name],
186		}
187		f.defaultValue = def.get(f)
188		configFields = append(configFields, f)
189		configFieldMap[f.name] = f
190		for _, choice := range f.choices {
191			configFieldMap[choice] = f
192		}
193	}
194}
195
196// fieldPtr returns a pointer to the field identified by f in *cfg.
197func (cfg *config) fieldPtr(f configField) interface{} {
198	// reflect.ValueOf: converts to reflect.Value
199	// Elem: dereferences cfg to make *cfg
200	// FieldByIndex: fetches the field
201	// Addr: takes address of field
202	// Interface: converts back from reflect.Value to a regular value
203	return reflect.ValueOf(cfg).Elem().FieldByIndex(f.field.Index).Addr().Interface()
204}
205
206// get returns the value of field f in cfg.
207func (cfg *config) get(f configField) string {
208	switch ptr := cfg.fieldPtr(f).(type) {
209	case *string:
210		return *ptr
211	case *int:
212		return fmt.Sprint(*ptr)
213	case *float64:
214		return fmt.Sprint(*ptr)
215	case *bool:
216		return fmt.Sprint(*ptr)
217	}
218	panic(fmt.Sprintf("unsupported config field type %v", f.field.Type))
219}
220
221// set sets the value of field f in cfg to value.
222func (cfg *config) set(f configField, value string) error {
223	switch ptr := cfg.fieldPtr(f).(type) {
224	case *string:
225		if len(f.choices) > 0 {
226			// Verify that value is one of the allowed choices.
227			for _, choice := range f.choices {
228				if choice == value {
229					*ptr = value
230					return nil
231				}
232			}
233			return fmt.Errorf("invalid %q value %q", f.name, value)
234		}
235		*ptr = value
236	case *int:
237		v, err := strconv.Atoi(value)
238		if err != nil {
239			return err
240		}
241		*ptr = v
242	case *float64:
243		v, err := strconv.ParseFloat(value, 64)
244		if err != nil {
245			return err
246		}
247		*ptr = v
248	case *bool:
249		v, err := stringToBool(value)
250		if err != nil {
251			return err
252		}
253		*ptr = v
254	default:
255		panic(fmt.Sprintf("unsupported config field type %v", f.field.Type))
256	}
257	return nil
258}
259
260// isConfigurable returns true if name is either the name of a config field, or
261// a valid value for a multi-choice config field.
262func isConfigurable(name string) bool {
263	_, ok := configFieldMap[name]
264	return ok
265}
266
267// isBoolConfig returns true if name is either name of a boolean config field,
268// or a valid value for a multi-choice config field.
269func isBoolConfig(name string) bool {
270	f, ok := configFieldMap[name]
271	if !ok {
272		return false
273	}
274	if name != f.name {
275		return true // name must be one possible value for the field
276	}
277	var cfg config
278	_, ok = cfg.fieldPtr(f).(*bool)
279	return ok
280}
281
282// completeConfig returns the list of configurable names starting with prefix.
283func completeConfig(prefix string) []string {
284	var result []string
285	for v := range configFieldMap {
286		if strings.HasPrefix(v, prefix) {
287			result = append(result, v)
288		}
289	}
290	return result
291}
292
293// configure stores the name=value mapping into the current config, correctly
294// handling the case when name identifies a particular choice in a field.
295func configure(name, value string) error {
296	currentMu.Lock()
297	defer currentMu.Unlock()
298	f, ok := configFieldMap[name]
299	if !ok {
300		return fmt.Errorf("unknown config field %q", name)
301	}
302	if f.name == name {
303		return currentCfg.set(f, value)
304	}
305	// name must be one of the choices. If value is true, set field-value
306	// to name.
307	if v, err := strconv.ParseBool(value); v && err == nil {
308		return currentCfg.set(f, name)
309	}
310	return fmt.Errorf("unknown config field %q", name)
311}
312
313// resetTransient sets all transient fields in *cfg to their currently
314// configured values.
315func (cfg *config) resetTransient() {
316	current := currentConfig()
317	cfg.Output = current.Output
318	cfg.SourcePath = current.SourcePath
319	cfg.TrimPath = current.TrimPath
320	cfg.DivideBy = current.DivideBy
321	cfg.SampleIndex = current.SampleIndex
322}
323
324// applyURL updates *cfg based on params.
325func (cfg *config) applyURL(params url.Values) error {
326	for _, f := range configFields {
327		var value string
328		if f.urlparam != "" {
329			value = params.Get(f.urlparam)
330		}
331		if value == "" {
332			continue
333		}
334		if err := cfg.set(f, value); err != nil {
335			return fmt.Errorf("error setting config field %s: %v", f.name, err)
336		}
337	}
338	return nil
339}
340
341// makeURL returns a URL based on initialURL that contains the config contents
342// as parameters.  The second result is true iff a parameter value was changed.
343func (cfg *config) makeURL(initialURL url.URL) (url.URL, bool) {
344	q := initialURL.Query()
345	changed := false
346	for _, f := range configFields {
347		if f.urlparam == "" || !f.saved {
348			continue
349		}
350		v := cfg.get(f)
351		if v == f.defaultValue {
352			v = "" // URL for of default value is the empty string.
353		} else if f.field.Type.Kind() == reflect.Bool {
354			// Shorten bool values to "f" or "t"
355			v = v[:1]
356		}
357		if q.Get(f.urlparam) == v {
358			continue
359		}
360		changed = true
361		if v == "" {
362			q.Del(f.urlparam)
363		} else {
364			q.Set(f.urlparam, v)
365		}
366	}
367	if changed {
368		initialURL.RawQuery = q.Encode()
369	}
370	return initialURL, changed
371}
372