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