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