Sfoglia il codice sorgente

Add support for saving the current configuration settings (#535)

Add support for saving the current configuration settings as a named
configuration and reloading such a configuration later.

All of the configurations are stored in a settings file, by default:
~/.config/pprof.json

A struct named config replaces the old map[string]string which used
to store the set of configurable options. Instead, each option is
now a field in the config struct.

The UI now has a new 'Config' menu with

Save as: prompts for a name and saves current config.
Default: applies default config.
$X: for every config named X: applies settings from X.

The currently selected config is highlighted.

Every named config has a delete button to control deletion.

Some semi-related changes:
1. Both filefunctions and functions in the granularity group had an
   initial value of true, which is incorrect since these are mutually
   incompatible choices. Set filefunctions' initial value to false.
2. Renamed the group for sorting from "cumulative" to "sort".
3. Store testing.T in TestUI instead of leaving the field nil.
Sanjay Ghemawat 5 anni fa
parent
commit
427632fa3b
No account linked to committer's email address

+ 34
- 0
doc/README.md Vedi File

387
 
387
 
388
 * **-symbolize=demangle=templates:** Demangle, and trim function parameters, but
388
 * **-symbolize=demangle=templates:** Demangle, and trim function parameters, but
389
   not template parameters.
389
   not template parameters.
390
+
391
+# Web Interface
392
+
393
+When the user requests a web interface (by supplying an `-http=[host]:[port]`
394
+argument on the command-line), pprof starts a web server and opens a browser
395
+window pointing at that server. The web interface provided by the server allows
396
+the user to interactively view profile data in multiple formats.
397
+
398
+The top of the display is a header that contains some buttons and menus.
399
+
400
+## Config
401
+
402
+The `Config` menu allows the user to save the current refinement
403
+settings (e.g., the focus and hide list) as a named configuration. A
404
+saved configuration can later be re-applied to reinstitue the saved
405
+refinements. The `Config` menu contains:
406
+
407
+**Save as ...**: shows a dialog where the user can type in a
408
+configuration name. The current refinement settings are saved under
409
+the specified name.
410
+
411
+**Default**: switches back to the default view by removing all refinements.
412
+
413
+The `Config` menu also contains an entry per named
414
+configuration. Selecting such an entry applies that configuration. The
415
+currently selected entry is marked with a ✓. Clicking on the 🗙 on the
416
+right-hand side of such an entry deletes the configuration (after
417
+prompting the user to confirm).
418
+
419
+## TODO: cover the following issues:
420
+
421
+*   Overall layout
422
+*   Menu entries
423
+*   Explanation of all the views

+ 69
- 60
internal/driver/cli.go Vedi File

69
 	flagHTTP := flag.String("http", "", "Present interactive web UI at the specified http host:port")
69
 	flagHTTP := flag.String("http", "", "Present interactive web UI at the specified http host:port")
70
 	flagNoBrowser := flag.Bool("no_browser", false, "Skip opening a browswer for the interactive web UI")
70
 	flagNoBrowser := flag.Bool("no_browser", false, "Skip opening a browswer for the interactive web UI")
71
 
71
 
72
-	// Flags used during command processing
73
-	installedFlags := installFlags(flag)
72
+	// Flags that set configuration properties.
73
+	cfg := currentConfig()
74
+	configFlagSetter := installConfigFlags(flag, &cfg)
74
 
75
 
75
 	flagCommands := make(map[string]*bool)
76
 	flagCommands := make(map[string]*bool)
76
 	flagParamCommands := make(map[string]*string)
77
 	flagParamCommands := make(map[string]*string)
107
 		}
108
 		}
108
 	}
109
 	}
109
 
110
 
110
-	// Report conflicting options
111
-	if err := updateFlags(installedFlags); err != nil {
111
+	// Apply any specified flags to cfg.
112
+	if err := configFlagSetter(); err != nil {
112
 		return nil, nil, err
113
 		return nil, nil, err
113
 	}
114
 	}
114
 
115
 
124
 		return nil, nil, errors.New("-no_browser only makes sense with -http")
125
 		return nil, nil, errors.New("-no_browser only makes sense with -http")
125
 	}
126
 	}
126
 
127
 
127
-	si := pprofVariables["sample_index"].value
128
+	si := cfg.SampleIndex
128
 	si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
129
 	si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
129
 	si = sampleIndex(flagMeanDelay, si, "delay", "-mean_delay", o.UI)
130
 	si = sampleIndex(flagMeanDelay, si, "delay", "-mean_delay", o.UI)
130
 	si = sampleIndex(flagContentions, si, "contentions", "-contentions", o.UI)
131
 	si = sampleIndex(flagContentions, si, "contentions", "-contentions", o.UI)
132
 	si = sampleIndex(flagInUseObjects, si, "inuse_objects", "-inuse_objects", o.UI)
133
 	si = sampleIndex(flagInUseObjects, si, "inuse_objects", "-inuse_objects", o.UI)
133
 	si = sampleIndex(flagAllocSpace, si, "alloc_space", "-alloc_space", o.UI)
134
 	si = sampleIndex(flagAllocSpace, si, "alloc_space", "-alloc_space", o.UI)
134
 	si = sampleIndex(flagAllocObjects, si, "alloc_objects", "-alloc_objects", o.UI)
135
 	si = sampleIndex(flagAllocObjects, si, "alloc_objects", "-alloc_objects", o.UI)
135
-	pprofVariables.set("sample_index", si)
136
+	cfg.SampleIndex = si
136
 
137
 
137
 	if *flagMeanDelay {
138
 	if *flagMeanDelay {
138
-		pprofVariables.set("mean", "true")
139
+		cfg.Mean = true
139
 	}
140
 	}
140
 
141
 
141
 	source := &source{
142
 	source := &source{
154
 		return nil, nil, err
155
 		return nil, nil, err
155
 	}
156
 	}
156
 
157
 
157
-	normalize := pprofVariables["normalize"].boolValue()
158
+	normalize := cfg.Normalize
158
 	if normalize && len(source.Base) == 0 {
159
 	if normalize && len(source.Base) == 0 {
159
 		return nil, nil, errors.New("must have base profile to normalize by")
160
 		return nil, nil, errors.New("must have base profile to normalize by")
160
 	}
161
 	}
163
 	if bu, ok := o.Obj.(*binutils.Binutils); ok {
164
 	if bu, ok := o.Obj.(*binutils.Binutils); ok {
164
 		bu.SetTools(*flagTools)
165
 		bu.SetTools(*flagTools)
165
 	}
166
 	}
167
+
168
+	setCurrentConfig(cfg)
166
 	return source, cmd, nil
169
 	return source, cmd, nil
167
 }
170
 }
168
 
171
 
194
 	return l
197
 	return l
195
 }
198
 }
196
 
199
 
197
-// installFlags creates command line flags for pprof variables.
198
-func installFlags(flag plugin.FlagSet) flagsInstalled {
199
-	f := flagsInstalled{
200
-		ints:    make(map[string]*int),
201
-		bools:   make(map[string]*bool),
202
-		floats:  make(map[string]*float64),
203
-		strings: make(map[string]*string),
204
-	}
205
-	for n, v := range pprofVariables {
206
-		switch v.kind {
207
-		case boolKind:
208
-			if v.group != "" {
209
-				// Set all radio variables to false to identify conflicts.
210
-				f.bools[n] = flag.Bool(n, false, v.help)
200
+// installConfigFlags creates command line flags for configuration
201
+// fields and returns a function which can be called after flags have
202
+// been parsed to copy any flags specified on the command line to
203
+// *cfg.
204
+func installConfigFlags(flag plugin.FlagSet, cfg *config) func() error {
205
+	// List of functions for setting the different parts of a config.
206
+	var setters []func()
207
+	var err error // Holds any errors encountered while running setters.
208
+
209
+	for _, field := range configFields {
210
+		n := field.name
211
+		help := configHelp[n]
212
+		var setter func()
213
+		switch ptr := cfg.fieldPtr(field).(type) {
214
+		case *bool:
215
+			f := flag.Bool(n, *ptr, help)
216
+			setter = func() { *ptr = *f }
217
+		case *int:
218
+			f := flag.Int(n, *ptr, help)
219
+			setter = func() { *ptr = *f }
220
+		case *float64:
221
+			f := flag.Float64(n, *ptr, help)
222
+			setter = func() { *ptr = *f }
223
+		case *string:
224
+			if len(field.choices) == 0 {
225
+				f := flag.String(n, *ptr, help)
226
+				setter = func() { *ptr = *f }
211
 			} else {
227
 			} else {
212
-				f.bools[n] = flag.Bool(n, v.boolValue(), v.help)
228
+				// Make a separate flag per possible choice.
229
+				// Set all flags to initially false so we can
230
+				// identify conflicts.
231
+				bools := make(map[string]*bool)
232
+				for _, choice := range field.choices {
233
+					bools[choice] = flag.Bool(choice, false, configHelp[choice])
234
+				}
235
+				setter = func() {
236
+					var set []string
237
+					for k, v := range bools {
238
+						if *v {
239
+							set = append(set, k)
240
+						}
241
+					}
242
+					switch len(set) {
243
+					case 0:
244
+						// Leave as default value.
245
+					case 1:
246
+						*ptr = set[0]
247
+					default:
248
+						err = fmt.Errorf("conflicting options set: %v", set)
249
+					}
250
+				}
213
 			}
251
 			}
214
-		case intKind:
215
-			f.ints[n] = flag.Int(n, v.intValue(), v.help)
216
-		case floatKind:
217
-			f.floats[n] = flag.Float64(n, v.floatValue(), v.help)
218
-		case stringKind:
219
-			f.strings[n] = flag.String(n, v.value, v.help)
220
 		}
252
 		}
253
+		setters = append(setters, setter)
221
 	}
254
 	}
222
-	return f
223
-}
224
 
255
 
225
-// updateFlags updates the pprof variables according to the flags
226
-// parsed in the command line.
227
-func updateFlags(f flagsInstalled) error {
228
-	vars := pprofVariables
229
-	groups := map[string]string{}
230
-	for n, v := range f.bools {
231
-		vars.set(n, fmt.Sprint(*v))
232
-		if *v {
233
-			g := vars[n].group
234
-			if g != "" && groups[g] != "" {
235
-				return fmt.Errorf("conflicting options %q and %q set", n, groups[g])
256
+	return func() error {
257
+		// Apply the setter for every flag.
258
+		for _, setter := range setters {
259
+			setter()
260
+			if err != nil {
261
+				return err
236
 			}
262
 			}
237
-			groups[g] = n
238
 		}
263
 		}
264
+		return nil
239
 	}
265
 	}
240
-	for n, v := range f.ints {
241
-		vars.set(n, fmt.Sprint(*v))
242
-	}
243
-	for n, v := range f.floats {
244
-		vars.set(n, fmt.Sprint(*v))
245
-	}
246
-	for n, v := range f.strings {
247
-		vars.set(n, *v)
248
-	}
249
-	return nil
250
-}
251
-
252
-type flagsInstalled struct {
253
-	ints    map[string]*int
254
-	bools   map[string]*bool
255
-	floats  map[string]*float64
256
-	strings map[string]*string
257
 }
266
 }
258
 
267
 
259
 // isBuildID determines if the profile may contain a build ID, by
268
 // isBuildID determines if the profile may contain a build ID, by

+ 81
- 199
internal/driver/commands.go Vedi File

22
 	"os/exec"
22
 	"os/exec"
23
 	"runtime"
23
 	"runtime"
24
 	"sort"
24
 	"sort"
25
-	"strconv"
26
 	"strings"
25
 	"strings"
27
 	"time"
26
 	"time"
28
 
27
 
70
 // SetVariableDefault sets the default value for a pprof
69
 // SetVariableDefault sets the default value for a pprof
71
 // variable. This enables extensions to set their own defaults.
70
 // variable. This enables extensions to set their own defaults.
72
 func SetVariableDefault(variable, value string) {
71
 func SetVariableDefault(variable, value string) {
73
-	if v := pprofVariables[variable]; v != nil {
74
-		v.value = value
75
-	}
72
+	configure(variable, value)
76
 }
73
 }
77
 
74
 
78
 // PostProcessor is a function that applies post-processing to the report output
75
 // PostProcessor is a function that applies post-processing to the report output
124
 	"weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)},
121
 	"weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)},
125
 }
122
 }
126
 
123
 
127
-// pprofVariables are the configuration parameters that affect the
128
-// reported generated by pprof.
129
-var pprofVariables = variables{
124
+// configHelp contains help text per configuration parameter.
125
+var configHelp = map[string]string{
130
 	// Filename for file-based output formats, stdout by default.
126
 	// Filename for file-based output formats, stdout by default.
131
-	"output": &variable{stringKind, "", "", helpText("Output filename for file-based outputs")},
127
+	"output": helpText("Output filename for file-based outputs"),
132
 
128
 
133
 	// Comparisons.
129
 	// Comparisons.
134
-	"drop_negative": &variable{boolKind, "f", "", helpText(
130
+	"drop_negative": helpText(
135
 		"Ignore negative differences",
131
 		"Ignore negative differences",
136
-		"Do not show any locations with values <0.")},
132
+		"Do not show any locations with values <0."),
137
 
133
 
138
 	// Graph handling options.
134
 	// Graph handling options.
139
-	"call_tree": &variable{boolKind, "f", "", helpText(
135
+	"call_tree": helpText(
140
 		"Create a context-sensitive call tree",
136
 		"Create a context-sensitive call tree",
141
-		"Treat locations reached through different paths as separate.")},
137
+		"Treat locations reached through different paths as separate."),
142
 
138
 
143
 	// Display options.
139
 	// Display options.
144
-	"relative_percentages": &variable{boolKind, "f", "", helpText(
140
+	"relative_percentages": helpText(
145
 		"Show percentages relative to focused subgraph",
141
 		"Show percentages relative to focused subgraph",
146
 		"If unset, percentages are relative to full graph before focusing",
142
 		"If unset, percentages are relative to full graph before focusing",
147
-		"to facilitate comparison with original graph.")},
148
-	"unit": &variable{stringKind, "minimum", "", helpText(
143
+		"to facilitate comparison with original graph."),
144
+	"unit": helpText(
149
 		"Measurement units to display",
145
 		"Measurement units to display",
150
 		"Scale the sample values to this unit.",
146
 		"Scale the sample values to this unit.",
151
 		"For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
147
 		"For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
152
 		"For memory profiles, use megabytes, kilobytes, bytes, etc.",
148
 		"For memory profiles, use megabytes, kilobytes, bytes, etc.",
153
-		"Using auto will scale each value independently to the most natural unit.")},
154
-	"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
155
-	"source_path":    &variable{stringKind, "", "", "Search path for source files"},
156
-	"trim_path":      &variable{stringKind, "", "", "Path to trim from source paths before search"},
157
-	"intel_syntax": &variable{boolKind, "f", "", helpText(
149
+		"Using auto will scale each value independently to the most natural unit."),
150
+	"compact_labels": "Show minimal headers",
151
+	"source_path":    "Search path for source files",
152
+	"trim_path":      "Path to trim from source paths before search",
153
+	"intel_syntax": helpText(
158
 		"Show assembly in Intel syntax",
154
 		"Show assembly in Intel syntax",
159
-		"Only applicable to commands `disasm` and `weblist`")},
155
+		"Only applicable to commands `disasm` and `weblist`"),
160
 
156
 
161
 	// Filtering options
157
 	// Filtering options
162
-	"nodecount": &variable{intKind, "-1", "", helpText(
158
+	"nodecount": helpText(
163
 		"Max number of nodes to show",
159
 		"Max number of nodes to show",
164
 		"Uses heuristics to limit the number of locations to be displayed.",
160
 		"Uses heuristics to limit the number of locations to be displayed.",
165
-		"On graphs, dotted edges represent paths through nodes that have been removed.")},
166
-	"nodefraction": &variable{floatKind, "0.005", "", "Hide nodes below <f>*total"},
167
-	"edgefraction": &variable{floatKind, "0.001", "", "Hide edges below <f>*total"},
168
-	"trim": &variable{boolKind, "t", "", helpText(
161
+		"On graphs, dotted edges represent paths through nodes that have been removed."),
162
+	"nodefraction": "Hide nodes below <f>*total",
163
+	"edgefraction": "Hide edges below <f>*total",
164
+	"trim": helpText(
169
 		"Honor nodefraction/edgefraction/nodecount defaults",
165
 		"Honor nodefraction/edgefraction/nodecount defaults",
170
-		"Set to false to get the full profile, without any trimming.")},
171
-	"focus": &variable{stringKind, "", "", helpText(
166
+		"Set to false to get the full profile, without any trimming."),
167
+	"focus": helpText(
172
 		"Restricts to samples going through a node matching regexp",
168
 		"Restricts to samples going through a node matching regexp",
173
 		"Discard samples that do not include a node matching this regexp.",
169
 		"Discard samples that do not include a node matching this regexp.",
174
-		"Matching includes the function name, filename or object name.")},
175
-	"ignore": &variable{stringKind, "", "", helpText(
170
+		"Matching includes the function name, filename or object name."),
171
+	"ignore": helpText(
176
 		"Skips paths going through any nodes matching regexp",
172
 		"Skips paths going through any nodes matching regexp",
177
 		"If set, discard samples that include a node matching this regexp.",
173
 		"If set, discard samples that include a node matching this regexp.",
178
-		"Matching includes the function name, filename or object name.")},
179
-	"prune_from": &variable{stringKind, "", "", helpText(
174
+		"Matching includes the function name, filename or object name."),
175
+	"prune_from": helpText(
180
 		"Drops any functions below the matched frame.",
176
 		"Drops any functions below the matched frame.",
181
 		"If set, any frames matching the specified regexp and any frames",
177
 		"If set, any frames matching the specified regexp and any frames",
182
-		"below it will be dropped from each sample.")},
183
-	"hide": &variable{stringKind, "", "", helpText(
178
+		"below it will be dropped from each sample."),
179
+	"hide": helpText(
184
 		"Skips nodes matching regexp",
180
 		"Skips nodes matching regexp",
185
 		"Discard nodes that match this location.",
181
 		"Discard nodes that match this location.",
186
 		"Other nodes from samples that include this location will be shown.",
182
 		"Other nodes from samples that include this location will be shown.",
187
-		"Matching includes the function name, filename or object name.")},
188
-	"show": &variable{stringKind, "", "", helpText(
183
+		"Matching includes the function name, filename or object name."),
184
+	"show": helpText(
189
 		"Only show nodes matching regexp",
185
 		"Only show nodes matching regexp",
190
 		"If set, only show nodes that match this location.",
186
 		"If set, only show nodes that match this location.",
191
-		"Matching includes the function name, filename or object name.")},
192
-	"show_from": &variable{stringKind, "", "", helpText(
187
+		"Matching includes the function name, filename or object name."),
188
+	"show_from": helpText(
193
 		"Drops functions above the highest matched frame.",
189
 		"Drops functions above the highest matched frame.",
194
 		"If set, all frames above the highest match are dropped from every sample.",
190
 		"If set, all frames above the highest match are dropped from every sample.",
195
-		"Matching includes the function name, filename or object name.")},
196
-	"tagfocus": &variable{stringKind, "", "", helpText(
191
+		"Matching includes the function name, filename or object name."),
192
+	"tagfocus": helpText(
197
 		"Restricts to samples with tags in range or matched by regexp",
193
 		"Restricts to samples with tags in range or matched by regexp",
198
 		"Use name=value syntax to limit the matching to a specific tag.",
194
 		"Use name=value syntax to limit the matching to a specific tag.",
199
 		"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
195
 		"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
200
-		"String tag filter examples: foo, foo.*bar, mytag=foo.*bar")},
201
-	"tagignore": &variable{stringKind, "", "", helpText(
196
+		"String tag filter examples: foo, foo.*bar, mytag=foo.*bar"),
197
+	"tagignore": helpText(
202
 		"Discard samples with tags in range or matched by regexp",
198
 		"Discard samples with tags in range or matched by regexp",
203
 		"Use name=value syntax to limit the matching to a specific tag.",
199
 		"Use name=value syntax to limit the matching to a specific tag.",
204
 		"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
200
 		"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
205
-		"String tag filter examples: foo, foo.*bar, mytag=foo.*bar")},
206
-	"tagshow": &variable{stringKind, "", "", helpText(
201
+		"String tag filter examples: foo, foo.*bar, mytag=foo.*bar"),
202
+	"tagshow": helpText(
207
 		"Only consider tags matching this regexp",
203
 		"Only consider tags matching this regexp",
208
-		"Discard tags that do not match this regexp")},
209
-	"taghide": &variable{stringKind, "", "", helpText(
204
+		"Discard tags that do not match this regexp"),
205
+	"taghide": helpText(
210
 		"Skip tags matching this regexp",
206
 		"Skip tags matching this regexp",
211
-		"Discard tags that match this regexp")},
207
+		"Discard tags that match this regexp"),
212
 	// Heap profile options
208
 	// Heap profile options
213
-	"divide_by": &variable{floatKind, "1", "", helpText(
209
+	"divide_by": helpText(
214
 		"Ratio to divide all samples before visualization",
210
 		"Ratio to divide all samples before visualization",
215
-		"Divide all samples values by a constant, eg the number of processors or jobs.")},
216
-	"mean": &variable{boolKind, "f", "", helpText(
211
+		"Divide all samples values by a constant, eg the number of processors or jobs."),
212
+	"mean": helpText(
217
 		"Average sample value over first value (count)",
213
 		"Average sample value over first value (count)",
218
 		"For memory profiles, report average memory per allocation.",
214
 		"For memory profiles, report average memory per allocation.",
219
-		"For time-based profiles, report average time per event.")},
220
-	"sample_index": &variable{stringKind, "", "", helpText(
215
+		"For time-based profiles, report average time per event."),
216
+	"sample_index": helpText(
221
 		"Sample value to report (0-based index or name)",
217
 		"Sample value to report (0-based index or name)",
222
 		"Profiles contain multiple values per sample.",
218
 		"Profiles contain multiple values per sample.",
223
-		"Use sample_index=i to select the ith value (starting at 0).")},
224
-	"normalize": &variable{boolKind, "f", "", helpText(
225
-		"Scales profile based on the base profile.")},
219
+		"Use sample_index=i to select the ith value (starting at 0)."),
220
+	"normalize": helpText(
221
+		"Scales profile based on the base profile."),
226
 
222
 
227
 	// Data sorting criteria
223
 	// Data sorting criteria
228
-	"flat": &variable{boolKind, "t", "cumulative", helpText("Sort entries based on own weight")},
229
-	"cum":  &variable{boolKind, "f", "cumulative", helpText("Sort entries based on cumulative weight")},
224
+	"flat": helpText("Sort entries based on own weight"),
225
+	"cum":  helpText("Sort entries based on cumulative weight"),
230
 
226
 
231
 	// Output granularity
227
 	// Output granularity
232
-	"functions": &variable{boolKind, "t", "granularity", helpText(
228
+	"functions": helpText(
233
 		"Aggregate at the function level.",
229
 		"Aggregate at the function level.",
234
-		"Ignores the filename where the function was defined.")},
235
-	"filefunctions": &variable{boolKind, "t", "granularity", helpText(
230
+		"Ignores the filename where the function was defined."),
231
+	"filefunctions": helpText(
236
 		"Aggregate at the function level.",
232
 		"Aggregate at the function level.",
237
-		"Takes into account the filename where the function was defined.")},
238
-	"files": &variable{boolKind, "f", "granularity", "Aggregate at the file level."},
239
-	"lines": &variable{boolKind, "f", "granularity", "Aggregate at the source code line level."},
240
-	"addresses": &variable{boolKind, "f", "granularity", helpText(
233
+		"Takes into account the filename where the function was defined."),
234
+	"files": "Aggregate at the file level.",
235
+	"lines": "Aggregate at the source code line level.",
236
+	"addresses": helpText(
241
 		"Aggregate at the address level.",
237
 		"Aggregate at the address level.",
242
-		"Includes functions' addresses in the output.")},
243
-	"noinlines": &variable{boolKind, "f", "", helpText(
238
+		"Includes functions' addresses in the output."),
239
+	"noinlines": helpText(
244
 		"Ignore inlines.",
240
 		"Ignore inlines.",
245
-		"Attributes inlined functions to their first out-of-line caller.")},
241
+		"Attributes inlined functions to their first out-of-line caller."),
246
 }
242
 }
247
 
243
 
248
 func helpText(s ...string) string {
244
 func helpText(s ...string) string {
249
 	return strings.Join(s, "\n") + "\n"
245
 	return strings.Join(s, "\n") + "\n"
250
 }
246
 }
251
 
247
 
252
-// usage returns a string describing the pprof commands and variables.
253
-// if commandLine is set, the output reflect cli usage.
248
+// usage returns a string describing the pprof commands and configuration
249
+// options.  if commandLine is set, the output reflect cli usage.
254
 func usage(commandLine bool) string {
250
 func usage(commandLine bool) string {
255
 	var prefix string
251
 	var prefix string
256
 	if commandLine {
252
 	if commandLine {
278
 	help = help + strings.Join(commands, "\n") + "\n\n" +
274
 	help = help + strings.Join(commands, "\n") + "\n\n" +
279
 		"  Options:\n"
275
 		"  Options:\n"
280
 
276
 
281
-	// Print help for variables after sorting them.
282
-	// Collect radio variables by their group name to print them together.
283
-	radioOptions := make(map[string][]string)
277
+	// Print help for configuration options after sorting them.
278
+	// Collect choices for multi-choice options print them together.
284
 	var variables []string
279
 	var variables []string
285
-	for name, vr := range pprofVariables {
286
-		if vr.group != "" {
287
-			radioOptions[vr.group] = append(radioOptions[vr.group], name)
280
+	var radioStrings []string
281
+	for _, f := range configFields {
282
+		if len(f.choices) == 0 {
283
+			variables = append(variables, fmtHelp(prefix+f.name, configHelp[f.name]))
288
 			continue
284
 			continue
289
 		}
285
 		}
290
-		variables = append(variables, fmtHelp(prefix+name, vr.help))
291
-	}
292
-	sort.Strings(variables)
293
-
294
-	help = help + strings.Join(variables, "\n") + "\n\n" +
295
-		"  Option groups (only set one per group):\n"
296
-
297
-	var radioStrings []string
298
-	for radio, ops := range radioOptions {
299
-		sort.Strings(ops)
300
-		s := []string{fmtHelp(radio, "")}
301
-		for _, op := range ops {
302
-			s = append(s, "  "+fmtHelp(prefix+op, pprofVariables[op].help))
286
+		// Format help for for this group.
287
+		s := []string{fmtHelp(f.name, "")}
288
+		for _, choice := range f.choices {
289
+			s = append(s, "  "+fmtHelp(prefix+choice, configHelp[choice]))
303
 		}
290
 		}
304
-
305
 		radioStrings = append(radioStrings, strings.Join(s, "\n"))
291
 		radioStrings = append(radioStrings, strings.Join(s, "\n"))
306
 	}
292
 	}
293
+	sort.Strings(variables)
307
 	sort.Strings(radioStrings)
294
 	sort.Strings(radioStrings)
308
-	return help + strings.Join(radioStrings, "\n")
295
+	return help + strings.Join(variables, "\n") + "\n\n" +
296
+		"  Option groups (only set one per group):\n" +
297
+		strings.Join(radioStrings, "\n")
309
 }
298
 }
310
 
299
 
311
 func reportHelp(c string, cum, redirect bool) string {
300
 func reportHelp(c string, cum, redirect bool) string {
448
 	}
437
 	}
449
 }
438
 }
450
 
439
 
451
-// variables describe the configuration parameters recognized by pprof.
452
-type variables map[string]*variable
453
-
454
-// variable is a single configuration parameter.
455
-type variable struct {
456
-	kind  int    // How to interpret the value, must be one of the enums below.
457
-	value string // Effective value. Only values appropriate for the Kind should be set.
458
-	group string // boolKind variables with the same Group != "" cannot be set simultaneously.
459
-	help  string // Text describing the variable, in multiple lines separated by newline.
460
-}
461
-
462
-const (
463
-	// variable.kind must be one of these variables.
464
-	boolKind = iota
465
-	intKind
466
-	floatKind
467
-	stringKind
468
-)
469
-
470
-// set updates the value of a variable, checking that the value is
471
-// suitable for the variable Kind.
472
-func (vars variables) set(name, value string) error {
473
-	v := vars[name]
474
-	if v == nil {
475
-		return fmt.Errorf("no variable %s", name)
476
-	}
477
-	var err error
478
-	switch v.kind {
479
-	case boolKind:
480
-		var b bool
481
-		if b, err = stringToBool(value); err == nil {
482
-			if v.group != "" && !b {
483
-				err = fmt.Errorf("%q can only be set to true", name)
484
-			}
485
-		}
486
-	case intKind:
487
-		_, err = strconv.Atoi(value)
488
-	case floatKind:
489
-		_, err = strconv.ParseFloat(value, 64)
490
-	case stringKind:
491
-		// Remove quotes, particularly useful for empty values.
492
-		if len(value) > 1 && strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
493
-			value = value[1 : len(value)-1]
494
-		}
495
-	}
496
-	if err != nil {
497
-		return err
498
-	}
499
-	vars[name].value = value
500
-	if group := vars[name].group; group != "" {
501
-		for vname, vvar := range vars {
502
-			if vvar.group == group && vname != name {
503
-				vvar.value = "f"
504
-			}
505
-		}
506
-	}
507
-	return err
508
-}
509
-
510
-// boolValue returns the value of a boolean variable.
511
-func (v *variable) boolValue() bool {
512
-	b, err := stringToBool(v.value)
513
-	if err != nil {
514
-		panic("unexpected value " + v.value + " for bool ")
515
-	}
516
-	return b
517
-}
518
-
519
-// intValue returns the value of an intKind variable.
520
-func (v *variable) intValue() int {
521
-	i, err := strconv.Atoi(v.value)
522
-	if err != nil {
523
-		panic("unexpected value " + v.value + " for int ")
524
-	}
525
-	return i
526
-}
527
-
528
-// floatValue returns the value of a Float variable.
529
-func (v *variable) floatValue() float64 {
530
-	f, err := strconv.ParseFloat(v.value, 64)
531
-	if err != nil {
532
-		panic("unexpected value " + v.value + " for float ")
533
-	}
534
-	return f
535
-}
536
-
537
-// stringValue returns a canonical representation for a variable.
538
-func (v *variable) stringValue() string {
539
-	switch v.kind {
540
-	case boolKind:
541
-		return fmt.Sprint(v.boolValue())
542
-	case intKind:
543
-		return fmt.Sprint(v.intValue())
544
-	case floatKind:
545
-		return fmt.Sprint(v.floatValue())
546
-	}
547
-	return v.value
548
-}
549
-
440
+// stringToBool is a custom parser for bools. We avoid using strconv.ParseBool
441
+// to remain compatible with old pprof behavior (e.g., treating "" as true).
550
 func stringToBool(s string) (bool, error) {
442
 func stringToBool(s string) (bool, error) {
551
 	switch strings.ToLower(s) {
443
 	switch strings.ToLower(s) {
552
 	case "true", "t", "yes", "y", "1", "":
444
 	case "true", "t", "yes", "y", "1", "":
557
 		return false, fmt.Errorf(`illegal value "%s" for bool variable`, s)
449
 		return false, fmt.Errorf(`illegal value "%s" for bool variable`, s)
558
 	}
450
 	}
559
 }
451
 }
560
-
561
-// makeCopy returns a duplicate of a set of shell variables.
562
-func (vars variables) makeCopy() variables {
563
-	varscopy := make(variables, len(vars))
564
-	for n, v := range vars {
565
-		vcopy := *v
566
-		varscopy[n] = &vcopy
567
-	}
568
-	return varscopy
569
-}

+ 367
- 0
internal/driver/config.go Vedi File

1
+package driver
2
+
3
+import (
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.
15
+type 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.
57
+func 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.
72
+var currentCfg = defaultConfig()
73
+var currentMu sync.Mutex
74
+
75
+func currentConfig() config {
76
+	currentMu.Lock()
77
+	defer currentMu.Unlock()
78
+	return currentCfg
79
+}
80
+
81
+func setCurrentConfig(cfg config) {
82
+	currentMu.Lock()
83
+	defer currentMu.Unlock()
84
+	currentCfg = cfg
85
+}
86
+
87
+// configField contains metadata for a single configuration field.
88
+type 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
+
97
+var (
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
+
105
+func 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.
193
+func (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.
203
+func (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.
218
+func (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.
258
+func 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.
265
+func 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.
279
+func 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.
291
+func 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.
311
+func (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.
321
+func (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.
339
+func (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
+}

+ 59
- 51
internal/driver/driver.go Vedi File

50
 	}
50
 	}
51
 
51
 
52
 	if cmd != nil {
52
 	if cmd != nil {
53
-		return generateReport(p, cmd, pprofVariables, o)
53
+		return generateReport(p, cmd, currentConfig(), o)
54
 	}
54
 	}
55
 
55
 
56
 	if src.HTTPHostport != "" {
56
 	if src.HTTPHostport != "" {
59
 	return interactive(p, o)
59
 	return interactive(p, o)
60
 }
60
 }
61
 
61
 
62
-func generateRawReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) (*command, *report.Report, error) {
62
+func generateRawReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) (*command, *report.Report, error) {
63
 	p = p.Copy() // Prevent modification to the incoming profile.
63
 	p = p.Copy() // Prevent modification to the incoming profile.
64
 
64
 
65
 	// Identify units of numeric tags in profile.
65
 	// Identify units of numeric tags in profile.
71
 		panic("unexpected nil command")
71
 		panic("unexpected nil command")
72
 	}
72
 	}
73
 
73
 
74
-	vars = applyCommandOverrides(cmd[0], c.format, vars)
74
+	cfg = applyCommandOverrides(cmd[0], c.format, cfg)
75
 
75
 
76
 	// Delay focus after configuring report to get percentages on all samples.
76
 	// Delay focus after configuring report to get percentages on all samples.
77
-	relative := vars["relative_percentages"].boolValue()
77
+	relative := cfg.RelativePercentages
78
 	if relative {
78
 	if relative {
79
-		if err := applyFocus(p, numLabelUnits, vars, o.UI); err != nil {
79
+		if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil {
80
 			return nil, nil, err
80
 			return nil, nil, err
81
 		}
81
 		}
82
 	}
82
 	}
83
-	ropt, err := reportOptions(p, numLabelUnits, vars)
83
+	ropt, err := reportOptions(p, numLabelUnits, cfg)
84
 	if err != nil {
84
 	if err != nil {
85
 		return nil, nil, err
85
 		return nil, nil, err
86
 	}
86
 	}
95
 
95
 
96
 	rpt := report.New(p, ropt)
96
 	rpt := report.New(p, ropt)
97
 	if !relative {
97
 	if !relative {
98
-		if err := applyFocus(p, numLabelUnits, vars, o.UI); err != nil {
98
+		if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil {
99
 			return nil, nil, err
99
 			return nil, nil, err
100
 		}
100
 		}
101
 	}
101
 	}
102
-	if err := aggregate(p, vars); err != nil {
102
+	if err := aggregate(p, cfg); err != nil {
103
 		return nil, nil, err
103
 		return nil, nil, err
104
 	}
104
 	}
105
 
105
 
106
 	return c, rpt, nil
106
 	return c, rpt, nil
107
 }
107
 }
108
 
108
 
109
-func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
110
-	c, rpt, err := generateRawReport(p, cmd, vars, o)
109
+func generateReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
110
+	c, rpt, err := generateRawReport(p, cmd, cfg, o)
111
 	if err != nil {
111
 	if err != nil {
112
 		return err
112
 		return err
113
 	}
113
 	}
129
 	}
129
 	}
130
 
130
 
131
 	// If no output is specified, use default visualizer.
131
 	// If no output is specified, use default visualizer.
132
-	output := vars["output"].value
132
+	output := cfg.Output
133
 	if output == "" {
133
 	if output == "" {
134
 		if c.visualizer != nil {
134
 		if c.visualizer != nil {
135
 			return c.visualizer(src, os.Stdout, o.UI)
135
 			return c.visualizer(src, os.Stdout, o.UI)
151
 	return out.Close()
151
 	return out.Close()
152
 }
152
 }
153
 
153
 
154
-func applyCommandOverrides(cmd string, outputFormat int, v variables) variables {
154
+func applyCommandOverrides(cmd string, outputFormat int, cfg config) config {
155
 	// Some report types override the trim flag to false below. This is to make
155
 	// Some report types override the trim flag to false below. This is to make
156
 	// sure the default heuristics of excluding insignificant nodes and edges
156
 	// sure the default heuristics of excluding insignificant nodes and edges
157
 	// from the call graph do not apply. One example where it is important is
157
 	// from the call graph do not apply. One example where it is important is
160
 	// data is selected. So, with trimming enabled, the report could end up
160
 	// data is selected. So, with trimming enabled, the report could end up
161
 	// showing no data if the specified function is "uninteresting" as far as the
161
 	// showing no data if the specified function is "uninteresting" as far as the
162
 	// trimming is concerned.
162
 	// trimming is concerned.
163
-	trim := v["trim"].boolValue()
163
+	trim := cfg.Trim
164
 
164
 
165
 	switch cmd {
165
 	switch cmd {
166
 	case "disasm", "weblist":
166
 	case "disasm", "weblist":
167
 		trim = false
167
 		trim = false
168
-		v.set("addresses", "t")
168
+		cfg.Granularity = "addresses"
169
 		// Force the 'noinlines' mode so that source locations for a given address
169
 		// Force the 'noinlines' mode so that source locations for a given address
170
 		// collapse and there is only one for the given address. Without this
170
 		// collapse and there is only one for the given address. Without this
171
 		// cumulative metrics would be double-counted when annotating the assembly.
171
 		// cumulative metrics would be double-counted when annotating the assembly.
172
 		// This is because the merge is done by address and in case of an inlined
172
 		// This is because the merge is done by address and in case of an inlined
173
 		// stack each of the inlined entries is a separate callgraph node.
173
 		// stack each of the inlined entries is a separate callgraph node.
174
-		v.set("noinlines", "t")
174
+		cfg.NoInlines = true
175
 	case "peek":
175
 	case "peek":
176
 		trim = false
176
 		trim = false
177
 	case "list":
177
 	case "list":
178
 		trim = false
178
 		trim = false
179
-		v.set("lines", "t")
179
+		cfg.Granularity = "lines"
180
 		// Do not force 'noinlines' to be false so that specifying
180
 		// Do not force 'noinlines' to be false so that specifying
181
 		// "-list foo -noinlines" is supported and works as expected.
181
 		// "-list foo -noinlines" is supported and works as expected.
182
 	case "text", "top", "topproto":
182
 	case "text", "top", "topproto":
183
-		if v["nodecount"].intValue() == -1 {
184
-			v.set("nodecount", "0")
183
+		if cfg.NodeCount == -1 {
184
+			cfg.NodeCount = 0
185
 		}
185
 		}
186
 	default:
186
 	default:
187
-		if v["nodecount"].intValue() == -1 {
188
-			v.set("nodecount", "80")
187
+		if cfg.NodeCount == -1 {
188
+			cfg.NodeCount = 80
189
 		}
189
 		}
190
 	}
190
 	}
191
 
191
 
192
 	switch outputFormat {
192
 	switch outputFormat {
193
 	case report.Proto, report.Raw, report.Callgrind:
193
 	case report.Proto, report.Raw, report.Callgrind:
194
 		trim = false
194
 		trim = false
195
-		v.set("addresses", "t")
196
-		v.set("noinlines", "f")
195
+		cfg.Granularity = "addresses"
196
+		cfg.NoInlines = false
197
 	}
197
 	}
198
 
198
 
199
 	if !trim {
199
 	if !trim {
200
-		v.set("nodecount", "0")
201
-		v.set("nodefraction", "0")
202
-		v.set("edgefraction", "0")
200
+		cfg.NodeCount = 0
201
+		cfg.NodeFraction = 0
202
+		cfg.EdgeFraction = 0
203
 	}
203
 	}
204
-	return v
204
+	return cfg
205
 }
205
 }
206
 
206
 
207
-func aggregate(prof *profile.Profile, v variables) error {
207
+func aggregate(prof *profile.Profile, cfg config) error {
208
 	var function, filename, linenumber, address bool
208
 	var function, filename, linenumber, address bool
209
-	inlines := !v["noinlines"].boolValue()
210
-	switch {
211
-	case v["addresses"].boolValue():
209
+	inlines := !cfg.NoInlines
210
+	switch cfg.Granularity {
211
+	case "addresses":
212
 		if inlines {
212
 		if inlines {
213
 			return nil
213
 			return nil
214
 		}
214
 		}
216
 		filename = true
216
 		filename = true
217
 		linenumber = true
217
 		linenumber = true
218
 		address = true
218
 		address = true
219
-	case v["lines"].boolValue():
219
+	case "lines":
220
 		function = true
220
 		function = true
221
 		filename = true
221
 		filename = true
222
 		linenumber = true
222
 		linenumber = true
223
-	case v["files"].boolValue():
223
+	case "files":
224
 		filename = true
224
 		filename = true
225
-	case v["functions"].boolValue():
225
+	case "functions":
226
 		function = true
226
 		function = true
227
-	case v["filefunctions"].boolValue():
227
+	case "filefunctions":
228
 		function = true
228
 		function = true
229
 		filename = true
229
 		filename = true
230
 	default:
230
 	default:
233
 	return prof.Aggregate(inlines, function, filename, linenumber, address)
233
 	return prof.Aggregate(inlines, function, filename, linenumber, address)
234
 }
234
 }
235
 
235
 
236
-func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars variables) (*report.Options, error) {
237
-	si, mean := vars["sample_index"].value, vars["mean"].boolValue()
236
+func reportOptions(p *profile.Profile, numLabelUnits map[string]string, cfg config) (*report.Options, error) {
237
+	si, mean := cfg.SampleIndex, cfg.Mean
238
 	value, meanDiv, sample, err := sampleFormat(p, si, mean)
238
 	value, meanDiv, sample, err := sampleFormat(p, si, mean)
239
 	if err != nil {
239
 	if err != nil {
240
 		return nil, err
240
 		return nil, err
245
 		stype = "mean_" + stype
245
 		stype = "mean_" + stype
246
 	}
246
 	}
247
 
247
 
248
-	if vars["divide_by"].floatValue() == 0 {
248
+	if cfg.DivideBy == 0 {
249
 		return nil, fmt.Errorf("zero divisor specified")
249
 		return nil, fmt.Errorf("zero divisor specified")
250
 	}
250
 	}
251
 
251
 
252
 	var filters []string
252
 	var filters []string
253
-	for _, k := range []string{"focus", "ignore", "hide", "show", "show_from", "tagfocus", "tagignore", "tagshow", "taghide"} {
254
-		v := vars[k].value
253
+	addFilter := func(k string, v string) {
255
 		if v != "" {
254
 		if v != "" {
256
 			filters = append(filters, k+"="+v)
255
 			filters = append(filters, k+"="+v)
257
 		}
256
 		}
258
 	}
257
 	}
258
+	addFilter("focus", cfg.Focus)
259
+	addFilter("ignore", cfg.Ignore)
260
+	addFilter("hide", cfg.Hide)
261
+	addFilter("show", cfg.Show)
262
+	addFilter("show_from", cfg.ShowFrom)
263
+	addFilter("tagfocus", cfg.TagFocus)
264
+	addFilter("tagignore", cfg.TagIgnore)
265
+	addFilter("tagshow", cfg.TagShow)
266
+	addFilter("taghide", cfg.TagHide)
259
 
267
 
260
 	ropt := &report.Options{
268
 	ropt := &report.Options{
261
-		CumSort:      vars["cum"].boolValue(),
262
-		CallTree:     vars["call_tree"].boolValue(),
263
-		DropNegative: vars["drop_negative"].boolValue(),
269
+		CumSort:      cfg.Sort == "cum",
270
+		CallTree:     cfg.CallTree,
271
+		DropNegative: cfg.DropNegative,
264
 
272
 
265
-		CompactLabels: vars["compact_labels"].boolValue(),
266
-		Ratio:         1 / vars["divide_by"].floatValue(),
273
+		CompactLabels: cfg.CompactLabels,
274
+		Ratio:         1 / cfg.DivideBy,
267
 
275
 
268
-		NodeCount:    vars["nodecount"].intValue(),
269
-		NodeFraction: vars["nodefraction"].floatValue(),
270
-		EdgeFraction: vars["edgefraction"].floatValue(),
276
+		NodeCount:    cfg.NodeCount,
277
+		NodeFraction: cfg.NodeFraction,
278
+		EdgeFraction: cfg.EdgeFraction,
271
 
279
 
272
 		ActiveFilters: filters,
280
 		ActiveFilters: filters,
273
 		NumLabelUnits: numLabelUnits,
281
 		NumLabelUnits: numLabelUnits,
277
 		SampleType:        stype,
285
 		SampleType:        stype,
278
 		SampleUnit:        sample.Unit,
286
 		SampleUnit:        sample.Unit,
279
 
287
 
280
-		OutputUnit: vars["unit"].value,
288
+		OutputUnit: cfg.Unit,
281
 
289
 
282
-		SourcePath: vars["source_path"].stringValue(),
283
-		TrimPath:   vars["trim_path"].stringValue(),
290
+		SourcePath: cfg.SourcePath,
291
+		TrimPath:   cfg.TrimPath,
284
 
292
 
285
-		IntelSyntax: vars["intel_syntax"].boolValue(),
293
+		IntelSyntax: cfg.IntelSyntax,
286
 	}
294
 	}
287
 
295
 
288
 	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
296
 	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {

+ 11
- 11
internal/driver/driver_focus.go Vedi File

28
 var tagFilterRangeRx = regexp.MustCompile("([+-]?[[:digit:]]+)([[:alpha:]]+)?")
28
 var tagFilterRangeRx = regexp.MustCompile("([+-]?[[:digit:]]+)([[:alpha:]]+)?")
29
 
29
 
30
 // applyFocus filters samples based on the focus/ignore options
30
 // applyFocus filters samples based on the focus/ignore options
31
-func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variables, ui plugin.UI) error {
32
-	focus, err := compileRegexOption("focus", v["focus"].value, nil)
33
-	ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
34
-	hide, err := compileRegexOption("hide", v["hide"].value, err)
35
-	show, err := compileRegexOption("show", v["show"].value, err)
36
-	showfrom, err := compileRegexOption("show_from", v["show_from"].value, err)
37
-	tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
38
-	tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
39
-	prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, err)
31
+func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, cfg config, ui plugin.UI) error {
32
+	focus, err := compileRegexOption("focus", cfg.Focus, nil)
33
+	ignore, err := compileRegexOption("ignore", cfg.Ignore, err)
34
+	hide, err := compileRegexOption("hide", cfg.Hide, err)
35
+	show, err := compileRegexOption("show", cfg.Show, err)
36
+	showfrom, err := compileRegexOption("show_from", cfg.ShowFrom, err)
37
+	tagfocus, err := compileTagFilter("tagfocus", cfg.TagFocus, numLabelUnits, ui, err)
38
+	tagignore, err := compileTagFilter("tagignore", cfg.TagIgnore, numLabelUnits, ui, err)
39
+	prunefrom, err := compileRegexOption("prune_from", cfg.PruneFrom, err)
40
 	if err != nil {
40
 	if err != nil {
41
 		return err
41
 		return err
42
 	}
42
 	}
54
 	warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
54
 	warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
55
 	warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)
55
 	warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)
56
 
56
 
57
-	tagshow, err := compileRegexOption("tagshow", v["tagshow"].value, err)
58
-	taghide, err := compileRegexOption("taghide", v["taghide"].value, err)
57
+	tagshow, err := compileRegexOption("tagshow", cfg.TagShow, err)
58
+	taghide, err := compileRegexOption("taghide", cfg.TagHide, err)
59
 	tns, tnh := prof.FilterTagsByName(tagshow, taghide)
59
 	tns, tnh := prof.FilterTagsByName(tagshow, taghide)
60
 	warnNoMatches(tagshow == nil || tns, "TagShow", ui)
60
 	warnNoMatches(tagshow == nil || tns, "TagShow", ui)
61
 	warnNoMatches(tagignore == nil || tnh, "TagHide", ui)
61
 	warnNoMatches(tagignore == nil || tnh, "TagHide", ui)

+ 25
- 9
internal/driver/driver_test.go Vedi File

102
 		{"text", "long_name_funcs"},
102
 		{"text", "long_name_funcs"},
103
 	}
103
 	}
104
 
104
 
105
-	baseVars := pprofVariables
106
-	defer func() { pprofVariables = baseVars }()
105
+	baseConfig := currentConfig()
106
+	defer setCurrentConfig(baseConfig)
107
 	for _, tc := range testcase {
107
 	for _, tc := range testcase {
108
 		t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
108
 		t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
109
-			// Reset the pprof variables before processing
110
-			pprofVariables = baseVars.makeCopy()
109
+			// Reset config before processing
110
+			setCurrentConfig(baseConfig)
111
 
111
 
112
 			testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
112
 			testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
113
 
113
 
141
 			if err := PProf(o1); err != nil {
141
 			if err := PProf(o1); err != nil {
142
 				t.Fatalf("%s %q:  %v", tc.source, tc.flags, err)
142
 				t.Fatalf("%s %q:  %v", tc.source, tc.flags, err)
143
 			}
143
 			}
144
-			// Reset the pprof variables after the proto invocation
145
-			pprofVariables = baseVars.makeCopy()
144
+			// Reset config after the proto invocation
145
+			setCurrentConfig(baseConfig)
146
 
146
 
147
 			// Read the profile from the encoded protobuf
147
 			// Read the profile from the encoded protobuf
148
 			outputTempFile, err := ioutil.TempFile("", "profile_output")
148
 			outputTempFile, err := ioutil.TempFile("", "profile_output")
1492
 	}
1492
 	}
1493
 }
1493
 }
1494
 
1494
 
1495
+// TestOptionsHaveHelp tests that a help message is supplied for every
1496
+// selectable option.
1497
+func TestOptionsHaveHelp(t *testing.T) {
1498
+	for _, f := range configFields {
1499
+		// Check all choices if this is a group, else check f.name.
1500
+		names := f.choices
1501
+		if len(names) == 0 {
1502
+			names = []string{f.name}
1503
+		}
1504
+		for _, name := range names {
1505
+			if _, ok := configHelp[name]; !ok {
1506
+				t.Errorf("missing help message for %q", name)
1507
+			}
1508
+		}
1509
+	}
1510
+}
1511
+
1495
 type testSymbolzMergeFetcher struct{}
1512
 type testSymbolzMergeFetcher struct{}
1496
 
1513
 
1497
 func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
1514
 func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
1513
 }
1530
 }
1514
 
1531
 
1515
 func TestSymbolzAfterMerge(t *testing.T) {
1532
 func TestSymbolzAfterMerge(t *testing.T) {
1516
-	baseVars := pprofVariables
1517
-	pprofVariables = baseVars.makeCopy()
1518
-	defer func() { pprofVariables = baseVars }()
1533
+	baseConfig := currentConfig()
1534
+	defer setCurrentConfig(baseConfig)
1519
 
1535
 
1520
 	f := baseFlags()
1536
 	f := baseFlags()
1521
 	f.args = []string{
1537
 	f.args = []string{

+ 7
- 9
internal/driver/fetch_test.go Vedi File

204
 }
204
 }
205
 
205
 
206
 func TestFetchWithBase(t *testing.T) {
206
 func TestFetchWithBase(t *testing.T) {
207
-	baseVars := pprofVariables
208
-	defer func() { pprofVariables = baseVars }()
207
+	baseConfig := currentConfig()
208
+	defer setCurrentConfig(baseConfig)
209
 
209
 
210
 	type WantSample struct {
210
 	type WantSample struct {
211
 		values []int64
211
 		values []int64
433
 
433
 
434
 	for _, tc := range testcases {
434
 	for _, tc := range testcases {
435
 		t.Run(tc.desc, func(t *testing.T) {
435
 		t.Run(tc.desc, func(t *testing.T) {
436
-			pprofVariables = baseVars.makeCopy()
436
+			setCurrentConfig(baseConfig)
437
 			f := testFlags{
437
 			f := testFlags{
438
 				stringLists: map[string][]string{
438
 				stringLists: map[string][]string{
439
 					"base":      tc.bases,
439
 					"base":      tc.bases,
542
 	os.Setenv(homeEnv(), tempdir)
542
 	os.Setenv(homeEnv(), tempdir)
543
 	defer os.Setenv(homeEnv(), saveHome)
543
 	defer os.Setenv(homeEnv(), saveHome)
544
 
544
 
545
-	baseVars := pprofVariables
546
-	pprofVariables = baseVars.makeCopy()
547
-	defer func() { pprofVariables = baseVars }()
545
+	baseConfig := currentConfig()
546
+	defer setCurrentConfig(baseConfig)
548
 
547
 
549
 	tlsCert, _, _ := selfSignedCert(t, "")
548
 	tlsCert, _, _ := selfSignedCert(t, "")
550
 	tlsConfig := &tls.Config{Certificates: []tls.Certificate{tlsCert}}
549
 	tlsConfig := &tls.Config{Certificates: []tls.Certificate{tlsCert}}
616
 	os.Setenv(homeEnv(), tempdir)
615
 	os.Setenv(homeEnv(), tempdir)
617
 	defer os.Setenv(homeEnv(), saveHome)
616
 	defer os.Setenv(homeEnv(), saveHome)
618
 
617
 
619
-	baseVars := pprofVariables
620
-	pprofVariables = baseVars.makeCopy()
621
-	defer func() { pprofVariables = baseVars }()
618
+	baseConfig := currentConfig()
619
+	defer setCurrentConfig(baseConfig)
622
 
620
 
623
 	cert, certBytes, keyBytes := selfSignedCert(t, "localhost")
621
 	cert, certBytes, keyBytes := selfSignedCert(t, "localhost")
624
 	cas := x509.NewCertPool()
622
 	cas := x509.NewCertPool()

+ 5
- 2
internal/driver/flamegraph.go Vedi File

38
 func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
38
 func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
39
 	// Force the call tree so that the graph is a tree.
39
 	// Force the call tree so that the graph is a tree.
40
 	// Also do not trim the tree so that the flame graph contains all functions.
40
 	// Also do not trim the tree so that the flame graph contains all functions.
41
-	rpt, errList := ui.makeReport(w, req, []string{"svg"}, "call_tree", "true", "trim", "false")
41
+	rpt, errList := ui.makeReport(w, req, []string{"svg"}, func(cfg *config) {
42
+		cfg.CallTree = true
43
+		cfg.Trim = false
44
+	})
42
 	if rpt == nil {
45
 	if rpt == nil {
43
 		return // error already reported
46
 		return // error already reported
44
 	}
47
 	}
96
 		return
99
 		return
97
 	}
100
 	}
98
 
101
 
99
-	ui.render(w, "flamegraph", rpt, errList, config.Labels, webArgs{
102
+	ui.render(w, req, "flamegraph", rpt, errList, config.Labels, webArgs{
100
 		FlameGraph: template.JS(b),
103
 		FlameGraph: template.JS(b),
101
 		Nodes:      nodeArr,
104
 		Nodes:      nodeArr,
102
 	})
105
 	})

+ 62
- 111
internal/driver/interactive.go Vedi File

34
 func interactive(p *profile.Profile, o *plugin.Options) error {
34
 func interactive(p *profile.Profile, o *plugin.Options) error {
35
 	// Enter command processing loop.
35
 	// Enter command processing loop.
36
 	o.UI.SetAutoComplete(newCompleter(functionNames(p)))
36
 	o.UI.SetAutoComplete(newCompleter(functionNames(p)))
37
-	pprofVariables.set("compact_labels", "true")
38
-	pprofVariables["sample_index"].help += fmt.Sprintf("Or use sample_index=name, with name in %v.\n", sampleTypes(p))
37
+	configure("compact_labels", "true")
38
+	configHelp["sample_index"] += fmt.Sprintf("Or use sample_index=name, with name in %v.\n", sampleTypes(p))
39
 
39
 
40
 	// Do not wait for the visualizer to complete, to allow multiple
40
 	// Do not wait for the visualizer to complete, to allow multiple
41
 	// graphs to be visualized simultaneously.
41
 	// graphs to be visualized simultaneously.
42
 	interactiveMode = true
42
 	interactiveMode = true
43
 	shortcuts := profileShortcuts(p)
43
 	shortcuts := profileShortcuts(p)
44
 
44
 
45
-	// Get all groups in pprofVariables to allow for clearer error messages.
46
-	groups := groupOptions(pprofVariables)
47
-
48
 	greetings(p, o.UI)
45
 	greetings(p, o.UI)
49
 	for {
46
 	for {
50
 		input, err := o.UI.ReadLine("(pprof) ")
47
 		input, err := o.UI.ReadLine("(pprof) ")
69
 					}
66
 					}
70
 					value = strings.TrimSpace(value)
67
 					value = strings.TrimSpace(value)
71
 				}
68
 				}
72
-				if v := pprofVariables[name]; v != nil {
69
+				if isConfigurable(name) {
73
 					// All non-bool options require inputs
70
 					// All non-bool options require inputs
74
-					if v.kind != boolKind && value == "" {
71
+					if len(s) == 1 && !isBoolConfig(name) {
75
 						o.UI.PrintErr(fmt.Errorf("please specify a value, e.g. %s=<val>", name))
72
 						o.UI.PrintErr(fmt.Errorf("please specify a value, e.g. %s=<val>", name))
76
 						continue
73
 						continue
77
 					}
74
 					}
82
 							o.UI.PrintErr(err)
79
 							o.UI.PrintErr(err)
83
 							continue
80
 							continue
84
 						}
81
 						}
82
+						if index < 0 || index >= len(p.SampleType) {
83
+							o.UI.PrintErr(fmt.Errorf("invalid sample_index %q", value))
84
+							continue
85
+						}
85
 						value = p.SampleType[index].Type
86
 						value = p.SampleType[index].Type
86
 					}
87
 					}
87
-					if err := pprofVariables.set(name, value); err != nil {
88
+					if err := configure(name, value); err != nil {
88
 						o.UI.PrintErr(err)
89
 						o.UI.PrintErr(err)
89
 					}
90
 					}
90
 					continue
91
 					continue
91
 				}
92
 				}
92
-				// Allow group=variable syntax by converting into variable="".
93
-				if v := pprofVariables[value]; v != nil && v.group == name {
94
-					if err := pprofVariables.set(value, ""); err != nil {
95
-						o.UI.PrintErr(err)
96
-					}
97
-					continue
98
-				} else if okValues := groups[name]; okValues != nil {
99
-					o.UI.PrintErr(fmt.Errorf("unrecognized value for %s: %q. Use one of %s", name, value, strings.Join(okValues, ", ")))
100
-					continue
101
-				}
102
 			}
93
 			}
103
 
94
 
104
 			tokens := strings.Fields(input)
95
 			tokens := strings.Fields(input)
117
 				continue
108
 				continue
118
 			}
109
 			}
119
 
110
 
120
-			args, vars, err := parseCommandLine(tokens)
111
+			args, cfg, err := parseCommandLine(tokens)
121
 			if err == nil {
112
 			if err == nil {
122
-				err = generateReportWrapper(p, args, vars, o)
113
+				err = generateReportWrapper(p, args, cfg, o)
123
 			}
114
 			}
124
 
115
 
125
 			if err != nil {
116
 			if err != nil {
129
 	}
120
 	}
130
 }
121
 }
131
 
122
 
132
-// groupOptions returns a map containing all non-empty groups
133
-// mapped to an array of the option names in that group in
134
-// sorted order.
135
-func groupOptions(vars variables) map[string][]string {
136
-	groups := make(map[string][]string)
137
-	for name, option := range vars {
138
-		group := option.group
139
-		if group != "" {
140
-			groups[group] = append(groups[group], name)
141
-		}
142
-	}
143
-	for _, names := range groups {
144
-		sort.Strings(names)
145
-	}
146
-	return groups
147
-}
148
-
149
 var generateReportWrapper = generateReport // For testing purposes.
123
 var generateReportWrapper = generateReport // For testing purposes.
150
 
124
 
151
 // greetings prints a brief welcome and some overall profile
125
 // greetings prints a brief welcome and some overall profile
152
 // information before accepting interactive commands.
126
 // information before accepting interactive commands.
153
 func greetings(p *profile.Profile, ui plugin.UI) {
127
 func greetings(p *profile.Profile, ui plugin.UI) {
154
 	numLabelUnits := identifyNumLabelUnits(p, ui)
128
 	numLabelUnits := identifyNumLabelUnits(p, ui)
155
-	ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
129
+	ropt, err := reportOptions(p, numLabelUnits, currentConfig())
156
 	if err == nil {
130
 	if err == nil {
157
 		rpt := report.New(p, ropt)
131
 		rpt := report.New(p, ropt)
158
 		ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
132
 		ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
205
 
179
 
206
 func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
180
 func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
207
 	var args []string
181
 	var args []string
208
-	type groupInfo struct {
209
-		set    string
210
-		values []string
211
-	}
212
-	groups := make(map[string]*groupInfo)
213
-	for n, o := range pprofVariables {
214
-		v := o.stringValue()
182
+	current := currentConfig()
183
+	for _, f := range configFields {
184
+		n := f.name
185
+		v := current.get(f)
215
 		comment := ""
186
 		comment := ""
216
-		if g := o.group; g != "" {
217
-			gi, ok := groups[g]
218
-			if !ok {
219
-				gi = &groupInfo{}
220
-				groups[g] = gi
221
-			}
222
-			if o.boolValue() {
223
-				gi.set = n
224
-			}
225
-			gi.values = append(gi.values, n)
226
-			continue
227
-		}
228
 		switch {
187
 		switch {
188
+		case len(f.choices) > 0:
189
+			values := append([]string{}, f.choices...)
190
+			sort.Strings(values)
191
+			comment = "[" + strings.Join(values, " | ") + "]"
229
 		case n == "sample_index":
192
 		case n == "sample_index":
230
 			st := sampleTypes(p)
193
 			st := sampleTypes(p)
231
 			if v == "" {
194
 			if v == "" {
247
 		}
210
 		}
248
 		args = append(args, fmt.Sprintf("  %-25s = %-20s %s", n, v, comment))
211
 		args = append(args, fmt.Sprintf("  %-25s = %-20s %s", n, v, comment))
249
 	}
212
 	}
250
-	for g, vars := range groups {
251
-		sort.Strings(vars.values)
252
-		comment := commentStart + " [" + strings.Join(vars.values, " | ") + "]"
253
-		args = append(args, fmt.Sprintf("  %-25s = %-20s %s", g, vars.set, comment))
254
-	}
255
 	sort.Strings(args)
213
 	sort.Strings(args)
256
 	ui.Print(strings.Join(args, "\n"))
214
 	ui.Print(strings.Join(args, "\n"))
257
 }
215
 }
258
 
216
 
259
 // parseCommandLine parses a command and returns the pprof command to
217
 // parseCommandLine parses a command and returns the pprof command to
260
-// execute and a set of variables for the report.
261
-func parseCommandLine(input []string) ([]string, variables, error) {
218
+// execute and the configuration to use for the report.
219
+func parseCommandLine(input []string) ([]string, config, error) {
262
 	cmd, args := input[:1], input[1:]
220
 	cmd, args := input[:1], input[1:]
263
 	name := cmd[0]
221
 	name := cmd[0]
264
 
222
 
272
 		}
230
 		}
273
 	}
231
 	}
274
 	if c == nil {
232
 	if c == nil {
275
-		if v := pprofVariables[name]; v != nil {
276
-			return nil, nil, fmt.Errorf("did you mean: %s=%s", name, args[0])
233
+		if _, ok := configHelp[name]; ok {
234
+			value := "<val>"
235
+			if len(args) > 0 {
236
+				value = args[0]
237
+			}
238
+			return nil, config{}, fmt.Errorf("did you mean: %s=%s", name, value)
277
 		}
239
 		}
278
-		return nil, nil, fmt.Errorf("unrecognized command: %q", name)
240
+		return nil, config{}, fmt.Errorf("unrecognized command: %q", name)
279
 	}
241
 	}
280
 
242
 
281
 	if c.hasParam {
243
 	if c.hasParam {
282
 		if len(args) == 0 {
244
 		if len(args) == 0 {
283
-			return nil, nil, fmt.Errorf("command %s requires an argument", name)
245
+			return nil, config{}, fmt.Errorf("command %s requires an argument", name)
284
 		}
246
 		}
285
 		cmd = append(cmd, args[0])
247
 		cmd = append(cmd, args[0])
286
 		args = args[1:]
248
 		args = args[1:]
287
 	}
249
 	}
288
 
250
 
289
-	// Copy the variables as options set in the command line are not persistent.
290
-	vcopy := pprofVariables.makeCopy()
251
+	// Copy config since options set in the command line should not persist.
252
+	vcopy := currentConfig()
291
 
253
 
292
 	var focus, ignore string
254
 	var focus, ignore string
293
 	for i := 0; i < len(args); i++ {
255
 	for i := 0; i < len(args); i++ {
294
 		t := args[i]
256
 		t := args[i]
295
-		if _, err := strconv.ParseInt(t, 10, 32); err == nil {
296
-			vcopy.set("nodecount", t)
257
+		if n, err := strconv.ParseInt(t, 10, 32); err == nil {
258
+			vcopy.NodeCount = int(n)
297
 			continue
259
 			continue
298
 		}
260
 		}
299
 		switch t[0] {
261
 		switch t[0] {
302
 			if outputFile == "" {
264
 			if outputFile == "" {
303
 				i++
265
 				i++
304
 				if i >= len(args) {
266
 				if i >= len(args) {
305
-					return nil, nil, fmt.Errorf("unexpected end of line after >")
267
+					return nil, config{}, fmt.Errorf("unexpected end of line after >")
306
 				}
268
 				}
307
 				outputFile = args[i]
269
 				outputFile = args[i]
308
 			}
270
 			}
309
-			vcopy.set("output", outputFile)
271
+			vcopy.Output = outputFile
310
 		case '-':
272
 		case '-':
311
 			if t == "--cum" || t == "-cum" {
273
 			if t == "--cum" || t == "-cum" {
312
-				vcopy.set("cum", "t")
274
+				vcopy.Sort = "cum"
313
 				continue
275
 				continue
314
 			}
276
 			}
315
 			ignore = catRegex(ignore, t[1:])
277
 			ignore = catRegex(ignore, t[1:])
319
 	}
281
 	}
320
 
282
 
321
 	if name == "tags" {
283
 	if name == "tags" {
322
-		updateFocusIgnore(vcopy, "tag", focus, ignore)
284
+		if focus != "" {
285
+			vcopy.TagFocus = focus
286
+		}
287
+		if ignore != "" {
288
+			vcopy.TagIgnore = ignore
289
+		}
323
 	} else {
290
 	} else {
324
-		updateFocusIgnore(vcopy, "", focus, ignore)
291
+		if focus != "" {
292
+			vcopy.Focus = focus
293
+		}
294
+		if ignore != "" {
295
+			vcopy.Ignore = ignore
296
+		}
325
 	}
297
 	}
326
-
327
-	if vcopy["nodecount"].intValue() == -1 && (name == "text" || name == "top") {
328
-		vcopy.set("nodecount", "10")
298
+	if vcopy.NodeCount == -1 && (name == "text" || name == "top") {
299
+		vcopy.NodeCount = 10
329
 	}
300
 	}
330
 
301
 
331
 	return cmd, vcopy, nil
302
 	return cmd, vcopy, nil
332
 }
303
 }
333
 
304
 
334
-func updateFocusIgnore(v variables, prefix, f, i string) {
335
-	if f != "" {
336
-		focus := prefix + "focus"
337
-		v.set(focus, catRegex(v[focus].value, f))
338
-	}
339
-
340
-	if i != "" {
341
-		ignore := prefix + "ignore"
342
-		v.set(ignore, catRegex(v[ignore].value, i))
343
-	}
344
-}
345
-
346
 func catRegex(a, b string) string {
305
 func catRegex(a, b string) string {
347
 	if a != "" && b != "" {
306
 	if a != "" && b != "" {
348
 		return a + "|" + b
307
 		return a + "|" + b
370
 		return
329
 		return
371
 	}
330
 	}
372
 
331
 
373
-	if v := pprofVariables[args]; v != nil {
374
-		ui.Print(v.help + "\n")
332
+	if help, ok := configHelp[args]; ok {
333
+		ui.Print(help + "\n")
375
 		return
334
 		return
376
 	}
335
 	}
377
 
336
 
381
 // newCompleter creates an autocompletion function for a set of commands.
340
 // newCompleter creates an autocompletion function for a set of commands.
382
 func newCompleter(fns []string) func(string) string {
341
 func newCompleter(fns []string) func(string) string {
383
 	return func(line string) string {
342
 	return func(line string) string {
384
-		v := pprofVariables
385
 		switch tokens := strings.Fields(line); len(tokens) {
343
 		switch tokens := strings.Fields(line); len(tokens) {
386
 		case 0:
344
 		case 0:
387
 			// Nothing to complete
345
 			// Nothing to complete
388
 		case 1:
346
 		case 1:
389
 			// Single token -- complete command name
347
 			// Single token -- complete command name
390
-			if match := matchVariableOrCommand(v, tokens[0]); match != "" {
348
+			if match := matchVariableOrCommand(tokens[0]); match != "" {
391
 				return match
349
 				return match
392
 			}
350
 			}
393
 		case 2:
351
 		case 2:
394
 			if tokens[0] == "help" {
352
 			if tokens[0] == "help" {
395
-				if match := matchVariableOrCommand(v, tokens[1]); match != "" {
353
+				if match := matchVariableOrCommand(tokens[1]); match != "" {
396
 					return tokens[0] + " " + match
354
 					return tokens[0] + " " + match
397
 				}
355
 				}
398
 				return line
356
 				return line
416
 }
374
 }
417
 
375
 
418
 // matchVariableOrCommand attempts to match a string token to the prefix of a Command.
376
 // matchVariableOrCommand attempts to match a string token to the prefix of a Command.
419
-func matchVariableOrCommand(v variables, token string) string {
377
+func matchVariableOrCommand(token string) string {
420
 	token = strings.ToLower(token)
378
 	token = strings.ToLower(token)
421
-	found := ""
379
+	var matches []string
422
 	for cmd := range pprofCommands {
380
 	for cmd := range pprofCommands {
423
 		if strings.HasPrefix(cmd, token) {
381
 		if strings.HasPrefix(cmd, token) {
424
-			if found != "" {
425
-				return ""
426
-			}
427
-			found = cmd
382
+			matches = append(matches, cmd)
428
 		}
383
 		}
429
 	}
384
 	}
430
-	for variable := range v {
431
-		if strings.HasPrefix(variable, token) {
432
-			if found != "" {
433
-				return ""
434
-			}
435
-			found = variable
436
-		}
385
+	matches = append(matches, completeConfig(token)...)
386
+	if len(matches) == 1 {
387
+		return matches[0]
437
 	}
388
 	}
438
-	return found
389
+	return ""
439
 }
390
 }
440
 
391
 
441
 // functionCompleter replaces provided substring with a function
392
 // functionCompleter replaces provided substring with a function

+ 46
- 72
internal/driver/interactive_test.go Vedi File

37
 	savedCommands, pprofCommands = pprofCommands, testCommands
37
 	savedCommands, pprofCommands = pprofCommands, testCommands
38
 	defer func() { pprofCommands = savedCommands }()
38
 	defer func() { pprofCommands = savedCommands }()
39
 
39
 
40
-	savedVariables := pprofVariables
41
-	defer func() { pprofVariables = savedVariables }()
40
+	savedConfig := currentConfig()
41
+	defer setCurrentConfig(savedConfig)
42
 
42
 
43
 	shortcuts1, scScript1 := makeShortcuts(interleave(script, 2), 1)
43
 	shortcuts1, scScript1 := makeShortcuts(interleave(script, 2), 1)
44
 	shortcuts2, scScript2 := makeShortcuts(interleave(script, 1), 2)
44
 	shortcuts2, scScript2 := makeShortcuts(interleave(script, 1), 2)
55
 		{"Random interleave of independent scripts 2", interleave(script, 1), pprofShortcuts, "", 0, false},
55
 		{"Random interleave of independent scripts 2", interleave(script, 1), pprofShortcuts, "", 0, false},
56
 		{"Random interleave of independent scripts with shortcuts 1", scScript1, shortcuts1, "", 0, false},
56
 		{"Random interleave of independent scripts with shortcuts 1", scScript1, shortcuts1, "", 0, false},
57
 		{"Random interleave of independent scripts with shortcuts 2", scScript2, shortcuts2, "", 0, false},
57
 		{"Random interleave of independent scripts with shortcuts 2", scScript2, shortcuts2, "", 0, false},
58
-		{"Group with invalid value", []string{"cumulative=this"}, pprofShortcuts, `unrecognized value for cumulative: "this". Use one of cum, flat`, 1, false},
58
+		{"Group with invalid value", []string{"sort=this"}, pprofShortcuts, `invalid "sort" value`, 1, false},
59
 		{"No special value provided for the option", []string{"sample_index"}, pprofShortcuts, `please specify a value, e.g. sample_index=<val>`, 1, false},
59
 		{"No special value provided for the option", []string{"sample_index"}, pprofShortcuts, `please specify a value, e.g. sample_index=<val>`, 1, false},
60
-		{"No string value provided for the option", []string{"focus="}, pprofShortcuts, `please specify a value, e.g. focus=<val>`, 1, false},
60
+		{"No string value provided for the option", []string{"focus"}, pprofShortcuts, `please specify a value, e.g. focus=<val>`, 1, false},
61
 		{"No float value provided for the option", []string{"divide_by"}, pprofShortcuts, `please specify a value, e.g. divide_by=<val>`, 1, false},
61
 		{"No float value provided for the option", []string{"divide_by"}, pprofShortcuts, `please specify a value, e.g. divide_by=<val>`, 1, false},
62
 		{"Helpful input format reminder", []string{"sample_index 0"}, pprofShortcuts, `did you mean: sample_index=0`, 1, false},
62
 		{"Helpful input format reminder", []string{"sample_index 0"}, pprofShortcuts, `did you mean: sample_index=0`, 1, false},
63
 		{"Verify propagation of IO errors", []string{"**error**"}, pprofShortcuts, "", 0, true},
63
 		{"Verify propagation of IO errors", []string{"**error**"}, pprofShortcuts, "", 0, true},
66
 	o := setDefaults(&plugin.Options{HTTPTransport: transport.New(nil)})
66
 	o := setDefaults(&plugin.Options{HTTPTransport: transport.New(nil)})
67
 	for _, tc := range testcases {
67
 	for _, tc := range testcases {
68
 		t.Run(tc.name, func(t *testing.T) {
68
 		t.Run(tc.name, func(t *testing.T) {
69
-			pprofVariables = testVariables(savedVariables)
69
+			setCurrentConfig(savedConfig)
70
 			pprofShortcuts = tc.shortcuts
70
 			pprofShortcuts = tc.shortcuts
71
 			ui := &proftest.TestUI{
71
 			ui := &proftest.TestUI{
72
 				T:       t,
72
 				T:       t,
93
 	"check": &command{report.Raw, nil, nil, true, "", ""},
93
 	"check": &command{report.Raw, nil, nil, true, "", ""},
94
 }
94
 }
95
 
95
 
96
-func testVariables(base variables) variables {
97
-	v := base.makeCopy()
98
-
99
-	v["b"] = &variable{boolKind, "f", "", ""}
100
-	v["bb"] = &variable{boolKind, "f", "", ""}
101
-	v["i"] = &variable{intKind, "0", "", ""}
102
-	v["ii"] = &variable{intKind, "0", "", ""}
103
-	v["f"] = &variable{floatKind, "0", "", ""}
104
-	v["ff"] = &variable{floatKind, "0", "", ""}
105
-	v["s"] = &variable{stringKind, "", "", ""}
106
-	v["ss"] = &variable{stringKind, "", "", ""}
107
-
108
-	v["ta"] = &variable{boolKind, "f", "radio", ""}
109
-	v["tb"] = &variable{boolKind, "f", "radio", ""}
110
-	v["tc"] = &variable{boolKind, "t", "radio", ""}
111
-
112
-	return v
113
-}
114
-
115
 // script contains sequences of commands to be executed for testing. Commands
96
 // script contains sequences of commands to be executed for testing. Commands
116
 // are split by semicolon and interleaved randomly, so they must be
97
 // are split by semicolon and interleaved randomly, so they must be
117
 // independent from each other.
98
 // independent from each other.
118
 var script = []string{
99
 var script = []string{
119
-	"bb=true;bb=false;check bb=false;bb=yes;check bb=true",
120
-	"b=1;check b=true;b=n;check b=false",
121
-	"i=-1;i=-2;check i=-2;i=999999;check i=999999",
122
-	"check ii=0;ii=-1;check ii=-1;ii=100;check ii=100",
123
-	"f=-1;f=-2.5;check f=-2.5;f=0.0001;check f=0.0001",
124
-	"check ff=0;ff=-1.01;check ff=-1.01;ff=100;check ff=100",
125
-	"s=one;s=two;check s=two",
126
-	"ss=tree;check ss=tree;ss=forest;check ss=forest",
127
-	"ta=true;check ta=true;check tb=false;check tc=false;tb=1;check tb=true;check ta=false;check tc=false;tc=yes;check tb=false;check ta=false;check tc=true",
100
+	"call_tree=true;call_tree=false;check call_tree=false;call_tree=yes;check call_tree=true",
101
+	"mean=1;check mean=true;mean=n;check mean=false",
102
+	"nodecount=-1;nodecount=-2;check nodecount=-2;nodecount=999999;check nodecount=999999",
103
+	"nodefraction=-1;nodefraction=-2.5;check nodefraction=-2.5;nodefraction=0.0001;check nodefraction=0.0001",
104
+	"focus=one;focus=two;check focus=two",
105
+	"flat=true;check sort=flat;cum=1;check sort=cum",
128
 }
106
 }
129
 
107
 
130
 func makeShortcuts(input []string, seed int) (shortcuts, []string) {
108
 func makeShortcuts(input []string, seed int) (shortcuts, []string) {
153
 	return s, output
131
 	return s, output
154
 }
132
 }
155
 
133
 
156
-func checkValue(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
134
+func checkValue(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
157
 	if len(cmd) != 2 {
135
 	if len(cmd) != 2 {
158
 		return fmt.Errorf("expected len(cmd)==2, got %v", cmd)
136
 		return fmt.Errorf("expected len(cmd)==2, got %v", cmd)
159
 	}
137
 	}
168
 		value = args[1]
146
 		value = args[1]
169
 	}
147
 	}
170
 
148
 
171
-	gotv := vars[name]
172
-	if gotv == nil {
149
+	f, ok := configFieldMap[name]
150
+	if !ok {
173
 		return fmt.Errorf("Could not find variable named %s", name)
151
 		return fmt.Errorf("Could not find variable named %s", name)
174
 	}
152
 	}
175
 
153
 
176
-	if got := gotv.stringValue(); got != value {
154
+	if got := cfg.get(f); got != value {
177
 		return fmt.Errorf("Variable %s, want %s, got %s", name, value, got)
155
 		return fmt.Errorf("Variable %s, want %s, got %s", name, value, got)
178
 	}
156
 	}
179
 	return nil
157
 	return nil
208
 		{
186
 		{
209
 			"top 10 --cum focus1 -ignore focus2",
187
 			"top 10 --cum focus1 -ignore focus2",
210
 			map[string]string{
188
 			map[string]string{
211
-				"functions": "true",
212
-				"nodecount": "10",
213
-				"cum":       "true",
214
-				"focus":     "focus1|focus2",
215
-				"ignore":    "ignore",
189
+				"granularity": "functions",
190
+				"nodecount":   "10",
191
+				"sort":        "cum",
192
+				"focus":       "focus1|focus2",
193
+				"ignore":      "ignore",
216
 			},
194
 			},
217
 		},
195
 		},
218
 		{
196
 		{
219
 			"top10 --cum focus1 -ignore focus2",
197
 			"top10 --cum focus1 -ignore focus2",
220
 			map[string]string{
198
 			map[string]string{
221
-				"functions": "true",
222
-				"nodecount": "10",
223
-				"cum":       "true",
224
-				"focus":     "focus1|focus2",
225
-				"ignore":    "ignore",
199
+				"granularity": "functions",
200
+				"nodecount":   "10",
201
+				"sort":        "cum",
202
+				"focus":       "focus1|focus2",
203
+				"ignore":      "ignore",
226
 			},
204
 			},
227
 		},
205
 		},
228
 		{
206
 		{
229
 			"dot",
207
 			"dot",
230
 			map[string]string{
208
 			map[string]string{
231
-				"functions": "true",
232
-				"nodecount": "80",
233
-				"cum":       "false",
209
+				"granularity": "functions",
210
+				"nodecount":   "80",
211
+				"sort":        "flat",
234
 			},
212
 			},
235
 		},
213
 		},
236
 		{
214
 		{
237
 			"tags   -ignore1 -ignore2 focus1 >out",
215
 			"tags   -ignore1 -ignore2 focus1 >out",
238
 			map[string]string{
216
 			map[string]string{
239
-				"functions": "true",
240
-				"nodecount": "80",
241
-				"cum":       "false",
242
-				"output":    "out",
243
-				"tagfocus":  "focus1",
244
-				"tagignore": "ignore1|ignore2",
217
+				"granularity": "functions",
218
+				"nodecount":   "80",
219
+				"sort":        "flat",
220
+				"output":      "out",
221
+				"tagfocus":    "focus1",
222
+				"tagignore":   "ignore1|ignore2",
245
 			},
223
 			},
246
 		},
224
 		},
247
 		{
225
 		{
248
 			"weblist  find -test",
226
 			"weblist  find -test",
249
 			map[string]string{
227
 			map[string]string{
250
-				"functions": "false",
251
-				"addresses": "true",
252
-				"noinlines": "true",
253
-				"nodecount": "0",
254
-				"cum":       "false",
255
-				"flat":      "true",
256
-				"ignore":    "test",
228
+				"granularity": "addresses",
229
+				"noinlines":   "true",
230
+				"nodecount":   "0",
231
+				"sort":        "flat",
232
+				"ignore":      "test",
257
 			},
233
 			},
258
 		},
234
 		},
259
 		{
235
 		{
260
 			"callgrind   fun -ignore  >out",
236
 			"callgrind   fun -ignore  >out",
261
 			map[string]string{
237
 			map[string]string{
262
-				"functions": "false",
263
-				"addresses": "true",
264
-				"nodecount": "0",
265
-				"cum":       "false",
266
-				"flat":      "true",
267
-				"output":    "out",
238
+				"granularity": "addresses",
239
+				"nodecount":   "0",
240
+				"sort":        "flat",
241
+				"output":      "out",
268
 			},
242
 			},
269
 		},
243
 		},
270
 		{
244
 		{
274
 	}
248
 	}
275
 
249
 
276
 	for _, tc := range testcases {
250
 	for _, tc := range testcases {
277
-		cmd, vars, err := parseCommandLine(strings.Fields(tc.input))
251
+		cmd, cfg, err := parseCommandLine(strings.Fields(tc.input))
278
 		if tc.want == nil && err != nil {
252
 		if tc.want == nil && err != nil {
279
 			// Error expected
253
 			// Error expected
280
 			continue
254
 			continue
289
 		if c == nil {
263
 		if c == nil {
290
 			t.Fatalf("unexpected nil command")
264
 			t.Fatalf("unexpected nil command")
291
 		}
265
 		}
292
-		vars = applyCommandOverrides(cmd[0], c.format, vars)
266
+		cfg = applyCommandOverrides(cmd[0], c.format, cfg)
293
 
267
 
294
 		for n, want := range tc.want {
268
 		for n, want := range tc.want {
295
-			if got := vars[n].stringValue(); got != want {
269
+			if got := cfg.get(configFieldMap[n]); got != want {
296
 				t.Errorf("failed on %q, cmd=%q, %s got %s, want %s", tc.input, cmd, n, got, want)
270
 				t.Errorf("failed on %q, cmd=%q, %s got %s, want %s", tc.input, cmd, n, got, want)
297
 			}
271
 			}
298
 		}
272
 		}

+ 153
- 0
internal/driver/settings.go Vedi File

1
+package driver
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/url"
8
+	"os"
9
+	"path/filepath"
10
+)
11
+
12
+// settings holds pprof settings.
13
+type settings struct {
14
+	// Configs holds a list of named UI configurations.
15
+	Configs []namedConfig `json:"configs"`
16
+}
17
+
18
+// namedConfig associates a name with a config.
19
+type namedConfig struct {
20
+	Name string `json:"name"`
21
+	config
22
+}
23
+
24
+// settingsFileName returns the name of the file where settings should be saved.
25
+func settingsFileName() (string, error) {
26
+	// Return "pprof/settings.json" under os.UserConfigDir().
27
+	dir, err := os.UserConfigDir()
28
+	if err != nil {
29
+		return "", err
30
+	}
31
+	dir = filepath.Join(dir, "pprof")
32
+	if err := os.MkdirAll(dir, 0755); err != nil {
33
+		return "", err
34
+	}
35
+	return filepath.Join(dir, "settings.json"), nil
36
+}
37
+
38
+// readSettings reads settings from fname.
39
+func readSettings(fname string) (*settings, error) {
40
+	data, err := ioutil.ReadFile(fname)
41
+	if err != nil {
42
+		if os.IsNotExist(err) {
43
+			return &settings{}, nil
44
+		}
45
+		return nil, fmt.Errorf("could not read settings: %w", err)
46
+	}
47
+	settings := &settings{}
48
+	if err := json.Unmarshal(data, settings); err != nil {
49
+		return nil, fmt.Errorf("could not parse settings: %w", err)
50
+	}
51
+	for i := range settings.Configs {
52
+		settings.Configs[i].resetTransient()
53
+	}
54
+	return settings, nil
55
+}
56
+
57
+// writeSettings saves settings to fname.
58
+func writeSettings(fname string, settings *settings) error {
59
+	data, err := json.MarshalIndent(settings, "", "  ")
60
+	if err != nil {
61
+		return fmt.Errorf("could not encode settings: %w", err)
62
+	}
63
+	if err := ioutil.WriteFile(fname, data, 0644); err != nil {
64
+		return fmt.Errorf("failed to write settings: %w", err)
65
+	}
66
+	return nil
67
+}
68
+
69
+// configMenuEntry holds information for a single config menu entry.
70
+type configMenuEntry struct {
71
+	Name       string
72
+	URL        string
73
+	Current    bool // Is this the currently selected config?
74
+	UserConfig bool // Is this a user-provided config?
75
+}
76
+
77
+// configMenu returns a list of items to add to a menu in the web UI.
78
+func configMenu(fname string, url url.URL) []configMenuEntry {
79
+	// Start with system configs.
80
+	configs := []namedConfig{{Name: "Default", config: defaultConfig()}}
81
+	if settings, err := readSettings(fname); err == nil {
82
+		// Add user configs.
83
+		configs = append(configs, settings.Configs...)
84
+	}
85
+
86
+	// Convert to menu entries.
87
+	result := make([]configMenuEntry, len(configs))
88
+	lastMatch := -1
89
+	for i, cfg := range configs {
90
+		dst, changed := cfg.config.makeURL(url)
91
+		if !changed {
92
+			lastMatch = i
93
+		}
94
+		result[i] = configMenuEntry{
95
+			Name:       cfg.Name,
96
+			URL:        dst.String(),
97
+			UserConfig: (i != 0),
98
+		}
99
+	}
100
+	// Mark the last matching config as currennt
101
+	if lastMatch >= 0 {
102
+		result[lastMatch].Current = true
103
+	}
104
+	return result
105
+}
106
+
107
+// editSettings edits settings by applying fn to them.
108
+func editSettings(fname string, fn func(s *settings) error) error {
109
+	settings, err := readSettings(fname)
110
+	if err != nil {
111
+		return err
112
+	}
113
+	if err := fn(settings); err != nil {
114
+		return err
115
+	}
116
+	return writeSettings(fname, settings)
117
+}
118
+
119
+// setConfig saves the config specified in request to fname.
120
+func setConfig(fname string, request url.URL) error {
121
+	q := request.Query()
122
+	name := q.Get("config")
123
+	if name == "" {
124
+		return fmt.Errorf("invalid config name")
125
+	}
126
+	cfg := currentConfig()
127
+	if err := cfg.applyURL(q); err != nil {
128
+		return err
129
+	}
130
+	return editSettings(fname, func(s *settings) error {
131
+		for i, c := range s.Configs {
132
+			if c.Name == name {
133
+				s.Configs[i].config = cfg
134
+				return nil
135
+			}
136
+		}
137
+		s.Configs = append(s.Configs, namedConfig{Name: name, config: cfg})
138
+		return nil
139
+	})
140
+}
141
+
142
+// removeConfig removes config from fname.
143
+func removeConfig(fname, config string) error {
144
+	return editSettings(fname, func(s *settings) error {
145
+		for i, c := range s.Configs {
146
+			if c.Name == config {
147
+				s.Configs = append(s.Configs[:i], s.Configs[i+1:]...)
148
+				return nil
149
+			}
150
+		}
151
+		return fmt.Errorf("config %s not found", config)
152
+	})
153
+}

+ 247
- 0
internal/driver/settings_test.go Vedi File

1
+package driver
2
+
3
+import (
4
+	"io/ioutil"
5
+	"net/url"
6
+	"os"
7
+	"path/filepath"
8
+	"reflect"
9
+	"testing"
10
+)
11
+
12
+// settingsDirAndFile returns a directory in which settings should be stored
13
+// and the name of the settings file. The caller must delete the directory when
14
+// done.
15
+func settingsDirAndFile(t *testing.T) (string, string) {
16
+	tmpDir, err := ioutil.TempDir("", "pprof_settings_test")
17
+	if err != nil {
18
+		t.Fatalf("error creating temporary directory: %v", err)
19
+	}
20
+	return tmpDir, filepath.Join(tmpDir, "settings.json")
21
+}
22
+
23
+func TestSettings(t *testing.T) {
24
+	tmpDir, fname := settingsDirAndFile(t)
25
+	defer os.RemoveAll(tmpDir)
26
+	s, err := readSettings(fname)
27
+	if err != nil {
28
+		t.Fatalf("error reading empty settings: %v", err)
29
+	}
30
+	if len(s.Configs) != 0 {
31
+		t.Fatalf("expected empty settings; got %v", s)
32
+	}
33
+	s.Configs = append(s.Configs, namedConfig{
34
+		Name: "Foo",
35
+		config: config{
36
+			Focus: "focus",
37
+			// Ensure that transient fields are not saved/restored.
38
+			Output:     "output",
39
+			SourcePath: "source",
40
+			TrimPath:   "trim",
41
+			DivideBy:   -2,
42
+		},
43
+	})
44
+	if err := writeSettings(fname, s); err != nil {
45
+		t.Fatal(err)
46
+	}
47
+	s2, err := readSettings(fname)
48
+	if err != nil {
49
+		t.Fatal(err)
50
+	}
51
+
52
+	// Change the transient fields to their expected values.
53
+	s.Configs[0].resetTransient()
54
+	if !reflect.DeepEqual(s, s2) {
55
+		t.Fatalf("ReadSettings = %v; expected %v", s2, s)
56
+	}
57
+}
58
+
59
+func TestParseConfig(t *testing.T) {
60
+	// Use all the fields to check they are saved/restored from URL.
61
+	cfg := config{
62
+		Output:              "",
63
+		DropNegative:        true,
64
+		CallTree:            true,
65
+		RelativePercentages: true,
66
+		Unit:                "auto",
67
+		CompactLabels:       true,
68
+		SourcePath:          "",
69
+		TrimPath:            "",
70
+		NodeCount:           10,
71
+		NodeFraction:        0.1,
72
+		EdgeFraction:        0.2,
73
+		Trim:                true,
74
+		Focus:               "focus",
75
+		Ignore:              "ignore",
76
+		PruneFrom:           "prune_from",
77
+		Hide:                "hide",
78
+		Show:                "show",
79
+		ShowFrom:            "show_from",
80
+		TagFocus:            "tagfocus",
81
+		TagIgnore:           "tagignore",
82
+		TagShow:             "tagshow",
83
+		TagHide:             "taghide",
84
+		DivideBy:            1,
85
+		Mean:                true,
86
+		Normalize:           true,
87
+		Sort:                "cum",
88
+		Granularity:         "functions",
89
+		NoInlines:           true,
90
+	}
91
+	url, changed := cfg.makeURL(url.URL{})
92
+	if !changed {
93
+		t.Error("applyConfig returned changed=false after applying non-empty config")
94
+	}
95
+	cfg2 := defaultConfig()
96
+	if err := cfg2.applyURL(url.Query()); err != nil {
97
+		t.Fatalf("fromURL failed: %v", err)
98
+	}
99
+	if !reflect.DeepEqual(cfg, cfg2) {
100
+		t.Fatalf("parsed config = %+v; expected match with %+v", cfg2, cfg)
101
+	}
102
+	if url2, changed := cfg.makeURL(url); changed {
103
+		t.Errorf("ApplyConfig returned changed=true after applying same config (%q instead of expected %q", url2.String(), url.String())
104
+	}
105
+}
106
+
107
+// TestDefaultConfig verifies that default config values are omitted from URL.
108
+func TestDefaultConfig(t *testing.T) {
109
+	cfg := defaultConfig()
110
+	url, changed := cfg.makeURL(url.URL{})
111
+	if changed {
112
+		t.Error("applyConfig returned changed=true after applying default config")
113
+	}
114
+	if url.String() != "" {
115
+		t.Errorf("applyConfig returned %q; expecting %q", url.String(), "")
116
+	}
117
+}
118
+
119
+func TestConfigMenu(t *testing.T) {
120
+	// Save some test settings.
121
+	tmpDir, fname := settingsDirAndFile(t)
122
+	defer os.RemoveAll(tmpDir)
123
+	a, b := defaultConfig(), defaultConfig()
124
+	a.Focus, b.Focus = "foo", "bar"
125
+	s := &settings{
126
+		Configs: []namedConfig{
127
+			{Name: "A", config: a},
128
+			{Name: "B", config: b},
129
+		},
130
+	}
131
+	if err := writeSettings(fname, s); err != nil {
132
+		t.Fatal("error writing settings", err)
133
+	}
134
+
135
+	pageURL, _ := url.Parse("/top?f=foo")
136
+	menu := configMenu(fname, *pageURL)
137
+	want := []configMenuEntry{
138
+		{Name: "Default", URL: "/top", Current: false, UserConfig: false},
139
+		{Name: "A", URL: "/top?f=foo", Current: true, UserConfig: true},
140
+		{Name: "B", URL: "/top?f=bar", Current: false, UserConfig: true},
141
+	}
142
+	if !reflect.DeepEqual(menu, want) {
143
+		t.Errorf("ConfigMenu returned %v; want %v", menu, want)
144
+	}
145
+}
146
+
147
+func TestEditConfig(t *testing.T) {
148
+	tmpDir, fname := settingsDirAndFile(t)
149
+	defer os.RemoveAll(tmpDir)
150
+
151
+	type testConfig struct {
152
+		name  string
153
+		focus string
154
+		hide  string
155
+	}
156
+	type testCase struct {
157
+		remove  bool
158
+		request string
159
+		expect  []testConfig
160
+	}
161
+	for _, c := range []testCase{
162
+		// Create setting c1
163
+		{false, "/?config=c1&f=foo", []testConfig{
164
+			{"c1", "foo", ""},
165
+		}},
166
+		// Create setting c2
167
+		{false, "/?config=c2&h=bar", []testConfig{
168
+			{"c1", "foo", ""},
169
+			{"c2", "", "bar"},
170
+		}},
171
+		// Overwrite c1
172
+		{false, "/?config=c1&f=baz", []testConfig{
173
+			{"c1", "baz", ""},
174
+			{"c2", "", "bar"},
175
+		}},
176
+		// Delete c2
177
+		{true, "c2", []testConfig{
178
+			{"c1", "baz", ""},
179
+		}},
180
+	} {
181
+		if c.remove {
182
+			if err := removeConfig(fname, c.request); err != nil {
183
+				t.Errorf("error removing config %s: %v", c.request, err)
184
+				continue
185
+			}
186
+		} else {
187
+			req, err := url.Parse(c.request)
188
+			if err != nil {
189
+				t.Errorf("error parsing request %q: %v", c.request, err)
190
+				continue
191
+			}
192
+			if err := setConfig(fname, *req); err != nil {
193
+				t.Errorf("error saving request %q: %v", c.request, err)
194
+				continue
195
+			}
196
+		}
197
+
198
+		// Check resulting settings.
199
+		s, err := readSettings(fname)
200
+		if err != nil {
201
+			t.Errorf("error reading settings after applying %q: %v", c.request, err)
202
+			continue
203
+		}
204
+		// Convert to a list that can be compared to c.expect
205
+		got := make([]testConfig, len(s.Configs))
206
+		for i, c := range s.Configs {
207
+			got[i] = testConfig{c.Name, c.Focus, c.Hide}
208
+		}
209
+		if !reflect.DeepEqual(got, c.expect) {
210
+			t.Errorf("Settings after applying %q = %v; want %v", c.request, got, c.expect)
211
+		}
212
+	}
213
+}
214
+
215
+func TestAssign(t *testing.T) {
216
+	baseConfig := currentConfig()
217
+	defer setCurrentConfig(baseConfig)
218
+
219
+	// Test assigning to a simple field.
220
+	if err := configure("nodecount", "20"); err != nil {
221
+		t.Errorf("error setting nodecount: %v", err)
222
+	}
223
+	if n := currentConfig().NodeCount; n != 20 {
224
+		t.Errorf("incorrect nodecount; expecting 20, got %d", n)
225
+	}
226
+
227
+	// Test assignment to a group field.
228
+	if err := configure("granularity", "files"); err != nil {
229
+		t.Errorf("error setting granularity: %v", err)
230
+	}
231
+	if g := currentConfig().Granularity; g != "files" {
232
+		t.Errorf("incorrect granularity; expecting %v, got %v", "files", g)
233
+	}
234
+
235
+	// Test assignment to one choice of a group field.
236
+	if err := configure("lines", "t"); err != nil {
237
+		t.Errorf("error setting lines: %v", err)
238
+	}
239
+	if g := currentConfig().Granularity; g != "lines" {
240
+		t.Errorf("incorrect granularity; expecting %v, got %v", "lines", g)
241
+	}
242
+
243
+	// Test assignment to invalid choice,
244
+	if err := configure("granularity", "cheese"); err == nil {
245
+		t.Errorf("allowed assignment of invalid granularity")
246
+	}
247
+}

+ 236
- 0
internal/driver/webhtml.go Vedi File

166
   color: gray;
166
   color: gray;
167
   pointer-events: none;
167
   pointer-events: none;
168
 }
168
 }
169
+.menu-check-mark {
170
+  position: absolute;
171
+  left: 2px;
172
+}
173
+.menu-delete-btn {
174
+  position: absolute;
175
+  right: 2px;
176
+}
177
+
178
+{{/* Used to disable events when a modal dialog is displayed */}}
179
+#dialog-overlay {
180
+  display: none;
181
+  position: fixed;
182
+  left: 0px;
183
+  top: 0px;
184
+  width: 100%;
185
+  height: 100%;
186
+  background-color: rgba(1,1,1,0.1);
187
+}
188
+
189
+.dialog {
190
+  {{/* Displayed centered horizontally near the top */}}
191
+  display: none;
192
+  position: fixed;
193
+  margin: 0px;
194
+  top: 60px;
195
+  left: 50%;
196
+  transform: translateX(-50%);
197
+
198
+  z-index: 3;
199
+  font-size: 125%;
200
+  background-color: #ffffff;
201
+  box-shadow: 0 1px 5px rgba(0,0,0,.3);
202
+}
203
+.dialog-header {
204
+  font-size: 120%;
205
+  border-bottom: 1px solid #CCCCCC;
206
+  width: 100%;
207
+  text-align: center;
208
+  background: #EEEEEE;
209
+  user-select: none;
210
+}
211
+.dialog-footer {
212
+  border-top: 1px solid #CCCCCC;
213
+  width: 100%;
214
+  text-align: right;
215
+  padding: 10px;
216
+}
217
+.dialog-error {
218
+  margin: 10px;
219
+  color: red;
220
+}
221
+.dialog input {
222
+  margin: 10px;
223
+  font-size: inherit;
224
+}
225
+.dialog button {
226
+  margin-left: 10px;
227
+  font-size: inherit;
228
+}
229
+#save-dialog, #delete-dialog {
230
+  width: 50%;
231
+  max-width: 20em;
232
+}
233
+#delete-prompt {
234
+  padding: 10px;
235
+}
169
 
236
 
170
 #content {
237
 #content {
171
   overflow-y: scroll;
238
   overflow-y: scroll;
284
     </div>
351
     </div>
285
   </div>
352
   </div>
286
 
353
 
354
+  <div id="config" class="menu-item">
355
+    <div class="menu-name">
356
+      Config
357
+      <i class="downArrow"></i>
358
+    </div>
359
+    <div class="submenu">
360
+      <a title="{{.Help.save_config}}" id="save-config">Save as ...</a>
361
+      <hr>
362
+      {{range .Configs}}
363
+        <a href="{{.URL}}">
364
+          {{if .Current}}<span class="menu-check-mark">✓</span>{{end}}
365
+          {{.Name}}
366
+          {{if .UserConfig}}<span class="menu-delete-btn" data-config={{.Name}}>🗙</span>{{end}}
367
+        </a>
368
+      {{end}}
369
+    </div>
370
+  </div>
371
+
287
   <div>
372
   <div>
288
     <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
373
     <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
289
   </div>
374
   </div>
296
   </div>
381
   </div>
297
 </div>
382
 </div>
298
 
383
 
384
+<div id="dialog-overlay"></div>
385
+
386
+<div class="dialog" id="save-dialog">
387
+  <div class="dialog-header">Save options as</div>
388
+  <datalist id="config-list">
389
+    {{range .Configs}}{{if .UserConfig}}<option value="{{.Name}}" />{{end}}{{end}}
390
+  </datalist>
391
+  <input id="save-name" type="text" list="config-list" placeholder="New config" />
392
+  <div class="dialog-footer">
393
+    <span class="dialog-error" id="save-error"></span>
394
+    <button id="save-cancel">Cancel</button>
395
+    <button id="save-confirm">Save</button>
396
+  </div>
397
+</div>
398
+
399
+<div class="dialog" id="delete-dialog">
400
+  <div class="dialog-header" id="delete-dialog-title">Delete config</div>
401
+  <div id="delete-prompt"></div>
402
+  <div class="dialog-footer">
403
+    <span class="dialog-error" id="delete-error"></span>
404
+    <button id="delete-cancel">Cancel</button>
405
+    <button id="delete-confirm">Delete</button>
406
+  </div>
407
+</div>
408
+
299
 <div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
409
 <div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
300
 {{end}}
410
 {{end}}
301
 
411
 
585
   }, { passive: true, capture: true });
695
   }, { passive: true, capture: true });
586
 }
696
 }
587
 
697
 
698
+function sendURL(method, url, done) {
699
+  fetch(url.toString(), {method: method})
700
+      .then((response) => { done(response.ok); })
701
+      .catch((error) => { done(false); });
702
+}
703
+
704
+// Initialize handlers for saving/loading configurations.
705
+function initConfigManager() {
706
+  'use strict';
707
+
708
+  // Initialize various elements.
709
+  function elem(id) {
710
+    const result = document.getElementById(id);
711
+    if (!result) console.warn('element ' + id + ' not found');
712
+    return result;
713
+  }
714
+  const overlay = elem('dialog-overlay');
715
+  const saveDialog = elem('save-dialog');
716
+  const saveInput = elem('save-name');
717
+  const saveError = elem('save-error');
718
+  const delDialog = elem('delete-dialog');
719
+  const delPrompt = elem('delete-prompt');
720
+  const delError = elem('delete-error');
721
+
722
+  let currentDialog = null;
723
+  let currentDeleteTarget = null;
724
+
725
+  function showDialog(dialog) {
726
+    if (currentDialog != null) {
727
+      overlay.style.display = 'none';
728
+      currentDialog.style.display = 'none';
729
+    }
730
+    currentDialog = dialog;
731
+    if (dialog != null) {
732
+      overlay.style.display = 'block';
733
+      dialog.style.display = 'block';
734
+    }
735
+  }
736
+
737
+  function cancelDialog(e) {
738
+    showDialog(null);
739
+  }
740
+
741
+  // Show dialog for saving the current config.
742
+  function showSaveDialog(e) {
743
+    saveError.innerText = '';
744
+    showDialog(saveDialog);
745
+    saveInput.focus();
746
+  }
747
+
748
+  // Commit save config.
749
+  function commitSave(e) {
750
+    const name = saveInput.value;
751
+    const url = new URL(document.URL);
752
+    // Set path relative to existing path.
753
+    url.pathname = new URL('./saveconfig', document.URL).pathname;
754
+    url.searchParams.set('config', name);
755
+    saveError.innerText = '';
756
+    sendURL('POST', url, (ok) => {
757
+      if (!ok) {
758
+        saveError.innerText = 'Save failed';
759
+      } else {
760
+        showDialog(null);
761
+        location.reload();  // Reload to show updated config menu
762
+      }
763
+    });
764
+  }
765
+
766
+  function handleSaveInputKey(e) {
767
+    if (e.key === 'Enter') commitSave(e);
768
+  }
769
+
770
+  function deleteConfig(e, elem) {
771
+    e.preventDefault();
772
+    const config = elem.dataset.config;
773
+    delPrompt.innerText = 'Delete ' + config + '?';
774
+    currentDeleteTarget = elem;
775
+    showDialog(delDialog);
776
+  }
777
+
778
+  function commitDelete(e, elem) {
779
+    if (!currentDeleteTarget) return;
780
+    const config = currentDeleteTarget.dataset.config;
781
+    const url = new URL('./deleteconfig', document.URL);
782
+    url.searchParams.set('config', config);
783
+    delError.innerText = '';
784
+    sendURL('DELETE', url, (ok) => {
785
+      if (!ok) {
786
+        delError.innerText = 'Delete failed';
787
+        return;
788
+      }
789
+      showDialog(null);
790
+      // Remove menu entry for this config.
791
+      if (currentDeleteTarget && currentDeleteTarget.parentElement) {
792
+        currentDeleteTarget.parentElement.remove();
793
+      }
794
+    });
795
+  }
796
+
797
+  // Bind event on elem to fn.
798
+  function bind(event, elem, fn) {
799
+    if (elem == null) return;
800
+    elem.addEventListener(event, fn);
801
+    if (event == 'click') {
802
+      // Also enable via touch.
803
+      elem.addEventListener('touchstart', fn);
804
+    }
805
+  }
806
+
807
+  bind('click', elem('save-config'), showSaveDialog);
808
+  bind('click', elem('save-cancel'), cancelDialog);
809
+  bind('click', elem('save-confirm'), commitSave);
810
+  bind('keydown', saveInput, handleSaveInputKey);
811
+
812
+  bind('click', elem('delete-cancel'), cancelDialog);
813
+  bind('click', elem('delete-confirm'), commitDelete);
814
+
815
+  // Activate deletion button for all config entries in menu.
816
+  for (const del of Array.from(document.getElementsByClassName('menu-delete-btn'))) {
817
+    bind('click', del, (e) => {
818
+      deleteConfig(e, del);
819
+    });
820
+  }
821
+}
822
+
588
 function viewer(baseUrl, nodes) {
823
 function viewer(baseUrl, nodes) {
589
   'use strict';
824
   'use strict';
590
 
825
 
877
   }
1112
   }
878
 
1113
 
879
   addAction('details', handleDetails);
1114
   addAction('details', handleDetails);
1115
+  initConfigManager();
880
 
1116
 
881
   search.addEventListener('input', handleSearch);
1117
   search.addEventListener('input', handleSearch);
882
   search.addEventListener('keydown', handleKey);
1118
   search.addEventListener('keydown', handleKey);

+ 82
- 61
internal/driver/webui.go Vedi File

35
 
35
 
36
 // webInterface holds the state needed for serving a browser based interface.
36
 // webInterface holds the state needed for serving a browser based interface.
37
 type webInterface struct {
37
 type webInterface struct {
38
-	prof      *profile.Profile
39
-	options   *plugin.Options
40
-	help      map[string]string
41
-	templates *template.Template
38
+	prof         *profile.Profile
39
+	options      *plugin.Options
40
+	help         map[string]string
41
+	templates    *template.Template
42
+	settingsFile string
42
 }
43
 }
43
 
44
 
44
-func makeWebInterface(p *profile.Profile, opt *plugin.Options) *webInterface {
45
+func makeWebInterface(p *profile.Profile, opt *plugin.Options) (*webInterface, error) {
46
+	settingsFile, err := settingsFileName()
47
+	if err != nil {
48
+		return nil, err
49
+	}
45
 	templates := template.New("templategroup")
50
 	templates := template.New("templategroup")
46
 	addTemplates(templates)
51
 	addTemplates(templates)
47
 	report.AddSourceTemplates(templates)
52
 	report.AddSourceTemplates(templates)
48
 	return &webInterface{
53
 	return &webInterface{
49
-		prof:      p,
50
-		options:   opt,
51
-		help:      make(map[string]string),
52
-		templates: templates,
53
-	}
54
+		prof:         p,
55
+		options:      opt,
56
+		help:         make(map[string]string),
57
+		templates:    templates,
58
+		settingsFile: settingsFile,
59
+	}, nil
54
 }
60
 }
55
 
61
 
56
 // maxEntries is the maximum number of entries to print for text interfaces.
62
 // maxEntries is the maximum number of entries to print for text interfaces.
80
 	TextBody    string
86
 	TextBody    string
81
 	Top         []report.TextItem
87
 	Top         []report.TextItem
82
 	FlameGraph  template.JS
88
 	FlameGraph  template.JS
89
+	Configs     []configMenuEntry
83
 }
90
 }
84
 
91
 
85
 func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error {
92
 func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error {
88
 		return err
95
 		return err
89
 	}
96
 	}
90
 	interactiveMode = true
97
 	interactiveMode = true
91
-	ui := makeWebInterface(p, o)
98
+	ui, err := makeWebInterface(p, o)
99
+	if err != nil {
100
+		return err
101
+	}
92
 	for n, c := range pprofCommands {
102
 	for n, c := range pprofCommands {
93
 		ui.help[n] = c.description
103
 		ui.help[n] = c.description
94
 	}
104
 	}
95
-	for n, v := range pprofVariables {
96
-		ui.help[n] = v.help
105
+	for n, help := range configHelp {
106
+		ui.help[n] = help
97
 	}
107
 	}
98
 	ui.help["details"] = "Show information about the profile and this view"
108
 	ui.help["details"] = "Show information about the profile and this view"
99
 	ui.help["graph"] = "Display profile as a directed graph"
109
 	ui.help["graph"] = "Display profile as a directed graph"
100
 	ui.help["reset"] = "Show the entire profile"
110
 	ui.help["reset"] = "Show the entire profile"
111
+	ui.help["save_config"] = "Save current settings"
101
 
112
 
102
 	server := o.HTTPServer
113
 	server := o.HTTPServer
103
 	if server == nil {
114
 	if server == nil {
108
 		Host:     host,
119
 		Host:     host,
109
 		Port:     port,
120
 		Port:     port,
110
 		Handlers: map[string]http.Handler{
121
 		Handlers: map[string]http.Handler{
111
-			"/":           http.HandlerFunc(ui.dot),
112
-			"/top":        http.HandlerFunc(ui.top),
113
-			"/disasm":     http.HandlerFunc(ui.disasm),
114
-			"/source":     http.HandlerFunc(ui.source),
115
-			"/peek":       http.HandlerFunc(ui.peek),
116
-			"/flamegraph": http.HandlerFunc(ui.flamegraph),
122
+			"/":             http.HandlerFunc(ui.dot),
123
+			"/top":          http.HandlerFunc(ui.top),
124
+			"/disasm":       http.HandlerFunc(ui.disasm),
125
+			"/source":       http.HandlerFunc(ui.source),
126
+			"/peek":         http.HandlerFunc(ui.peek),
127
+			"/flamegraph":   http.HandlerFunc(ui.flamegraph),
128
+			"/saveconfig":   http.HandlerFunc(ui.saveConfig),
129
+			"/deleteconfig": http.HandlerFunc(ui.deleteConfig),
117
 		},
130
 		},
118
 	}
131
 	}
119
 
132
 
206
 
219
 
207
 func openBrowser(url string, o *plugin.Options) {
220
 func openBrowser(url string, o *plugin.Options) {
208
 	// Construct URL.
221
 	// Construct URL.
209
-	u, _ := gourl.Parse(url)
210
-	q := u.Query()
211
-	for _, p := range []struct{ param, key string }{
212
-		{"f", "focus"},
213
-		{"s", "show"},
214
-		{"sf", "show_from"},
215
-		{"i", "ignore"},
216
-		{"h", "hide"},
217
-		{"si", "sample_index"},
218
-	} {
219
-		if v := pprofVariables[p.key].value; v != "" {
220
-			q.Set(p.param, v)
221
-		}
222
-	}
223
-	u.RawQuery = q.Encode()
222
+	baseURL, _ := gourl.Parse(url)
223
+	current := currentConfig()
224
+	u, _ := current.makeURL(*baseURL)
224
 
225
 
225
 	// Give server a little time to get ready.
226
 	// Give server a little time to get ready.
226
 	time.Sleep(time.Millisecond * 500)
227
 	time.Sleep(time.Millisecond * 500)
240
 	o.UI.PrintErr(u.String())
241
 	o.UI.PrintErr(u.String())
241
 }
242
 }
242
 
243
 
243
-func varsFromURL(u *gourl.URL) variables {
244
-	vars := pprofVariables.makeCopy()
245
-	vars["focus"].value = u.Query().Get("f")
246
-	vars["show"].value = u.Query().Get("s")
247
-	vars["show_from"].value = u.Query().Get("sf")
248
-	vars["ignore"].value = u.Query().Get("i")
249
-	vars["hide"].value = u.Query().Get("h")
250
-	vars["sample_index"].value = u.Query().Get("si")
251
-	return vars
252
-}
253
-
254
 // makeReport generates a report for the specified command.
244
 // makeReport generates a report for the specified command.
245
+// If configEditor is not null, it is used to edit the config used for the report.
255
 func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
246
 func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
256
-	cmd []string, vars ...string) (*report.Report, []string) {
257
-	v := varsFromURL(req.URL)
258
-	for i := 0; i+1 < len(vars); i += 2 {
259
-		v[vars[i]].value = vars[i+1]
247
+	cmd []string, configEditor func(*config)) (*report.Report, []string) {
248
+	cfg := currentConfig()
249
+	if err := cfg.applyURL(req.URL.Query()); err != nil {
250
+		http.Error(w, err.Error(), http.StatusBadRequest)
251
+		ui.options.UI.PrintErr(err)
252
+		return nil, nil
253
+	}
254
+	if configEditor != nil {
255
+		configEditor(&cfg)
260
 	}
256
 	}
261
 	catcher := &errorCatcher{UI: ui.options.UI}
257
 	catcher := &errorCatcher{UI: ui.options.UI}
262
 	options := *ui.options
258
 	options := *ui.options
263
 	options.UI = catcher
259
 	options.UI = catcher
264
-	_, rpt, err := generateRawReport(ui.prof, cmd, v, &options)
260
+	_, rpt, err := generateRawReport(ui.prof, cmd, cfg, &options)
265
 	if err != nil {
261
 	if err != nil {
266
 		http.Error(w, err.Error(), http.StatusBadRequest)
262
 		http.Error(w, err.Error(), http.StatusBadRequest)
267
 		ui.options.UI.PrintErr(err)
263
 		ui.options.UI.PrintErr(err)
271
 }
267
 }
272
 
268
 
273
 // render generates html using the named template based on the contents of data.
269
 // render generates html using the named template based on the contents of data.
274
-func (ui *webInterface) render(w http.ResponseWriter, tmpl string,
270
+func (ui *webInterface) render(w http.ResponseWriter, req *http.Request, tmpl string,
275
 	rpt *report.Report, errList, legend []string, data webArgs) {
271
 	rpt *report.Report, errList, legend []string, data webArgs) {
276
 	file := getFromLegend(legend, "File: ", "unknown")
272
 	file := getFromLegend(legend, "File: ", "unknown")
277
 	profile := getFromLegend(legend, "Type: ", "unknown")
273
 	profile := getFromLegend(legend, "Type: ", "unknown")
281
 	data.SampleTypes = sampleTypes(ui.prof)
277
 	data.SampleTypes = sampleTypes(ui.prof)
282
 	data.Legend = legend
278
 	data.Legend = legend
283
 	data.Help = ui.help
279
 	data.Help = ui.help
280
+	data.Configs = configMenu(ui.settingsFile, *req.URL)
281
+
284
 	html := &bytes.Buffer{}
282
 	html := &bytes.Buffer{}
285
 	if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil {
283
 	if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil {
286
 		http.Error(w, "internal template error", http.StatusInternalServerError)
284
 		http.Error(w, "internal template error", http.StatusInternalServerError)
293
 
291
 
294
 // dot generates a web page containing an svg diagram.
292
 // dot generates a web page containing an svg diagram.
295
 func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
293
 func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
296
-	rpt, errList := ui.makeReport(w, req, []string{"svg"})
294
+	rpt, errList := ui.makeReport(w, req, []string{"svg"}, nil)
297
 	if rpt == nil {
295
 	if rpt == nil {
298
 		return // error already reported
296
 		return // error already reported
299
 	}
297
 	}
320
 		nodes = append(nodes, n.Info.Name)
318
 		nodes = append(nodes, n.Info.Name)
321
 	}
319
 	}
322
 
320
 
323
-	ui.render(w, "graph", rpt, errList, legend, webArgs{
321
+	ui.render(w, req, "graph", rpt, errList, legend, webArgs{
324
 		HTMLBody: template.HTML(string(svg)),
322
 		HTMLBody: template.HTML(string(svg)),
325
 		Nodes:    nodes,
323
 		Nodes:    nodes,
326
 	})
324
 	})
345
 }
343
 }
346
 
344
 
347
 func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
345
 func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
348
-	rpt, errList := ui.makeReport(w, req, []string{"top"}, "nodecount", "500")
346
+	rpt, errList := ui.makeReport(w, req, []string{"top"}, func(cfg *config) {
347
+		cfg.NodeCount = 500
348
+	})
349
 	if rpt == nil {
349
 	if rpt == nil {
350
 		return // error already reported
350
 		return // error already reported
351
 	}
351
 	}
355
 		nodes = append(nodes, item.Name)
355
 		nodes = append(nodes, item.Name)
356
 	}
356
 	}
357
 
357
 
358
-	ui.render(w, "top", rpt, errList, legend, webArgs{
358
+	ui.render(w, req, "top", rpt, errList, legend, webArgs{
359
 		Top:   top,
359
 		Top:   top,
360
 		Nodes: nodes,
360
 		Nodes: nodes,
361
 	})
361
 	})
364
 // disasm generates a web page containing disassembly.
364
 // disasm generates a web page containing disassembly.
365
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
365
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
366
 	args := []string{"disasm", req.URL.Query().Get("f")}
366
 	args := []string{"disasm", req.URL.Query().Get("f")}
367
-	rpt, errList := ui.makeReport(w, req, args)
367
+	rpt, errList := ui.makeReport(w, req, args, nil)
368
 	if rpt == nil {
368
 	if rpt == nil {
369
 		return // error already reported
369
 		return // error already reported
370
 	}
370
 	}
377
 	}
377
 	}
378
 
378
 
379
 	legend := report.ProfileLabels(rpt)
379
 	legend := report.ProfileLabels(rpt)
380
-	ui.render(w, "plaintext", rpt, errList, legend, webArgs{
380
+	ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{
381
 		TextBody: out.String(),
381
 		TextBody: out.String(),
382
 	})
382
 	})
383
 
383
 
387
 // data.
387
 // data.
388
 func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
388
 func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
389
 	args := []string{"weblist", req.URL.Query().Get("f")}
389
 	args := []string{"weblist", req.URL.Query().Get("f")}
390
-	rpt, errList := ui.makeReport(w, req, args)
390
+	rpt, errList := ui.makeReport(w, req, args, nil)
391
 	if rpt == nil {
391
 	if rpt == nil {
392
 		return // error already reported
392
 		return // error already reported
393
 	}
393
 	}
401
 	}
401
 	}
402
 
402
 
403
 	legend := report.ProfileLabels(rpt)
403
 	legend := report.ProfileLabels(rpt)
404
-	ui.render(w, "sourcelisting", rpt, errList, legend, webArgs{
404
+	ui.render(w, req, "sourcelisting", rpt, errList, legend, webArgs{
405
 		HTMLBody: template.HTML(body.String()),
405
 		HTMLBody: template.HTML(body.String()),
406
 	})
406
 	})
407
 }
407
 }
409
 // peek generates a web page listing callers/callers.
409
 // peek generates a web page listing callers/callers.
410
 func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
410
 func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
411
 	args := []string{"peek", req.URL.Query().Get("f")}
411
 	args := []string{"peek", req.URL.Query().Get("f")}
412
-	rpt, errList := ui.makeReport(w, req, args, "lines", "t")
412
+	rpt, errList := ui.makeReport(w, req, args, func(cfg *config) {
413
+		cfg.Granularity = "lines"
414
+	})
413
 	if rpt == nil {
415
 	if rpt == nil {
414
 		return // error already reported
416
 		return // error already reported
415
 	}
417
 	}
422
 	}
424
 	}
423
 
425
 
424
 	legend := report.ProfileLabels(rpt)
426
 	legend := report.ProfileLabels(rpt)
425
-	ui.render(w, "plaintext", rpt, errList, legend, webArgs{
427
+	ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{
426
 		TextBody: out.String(),
428
 		TextBody: out.String(),
427
 	})
429
 	})
428
 }
430
 }
429
 
431
 
432
+// saveConfig saves URL configuration.
433
+func (ui *webInterface) saveConfig(w http.ResponseWriter, req *http.Request) {
434
+	if err := setConfig(ui.settingsFile, *req.URL); err != nil {
435
+		http.Error(w, err.Error(), http.StatusBadRequest)
436
+		ui.options.UI.PrintErr(err)
437
+		return
438
+	}
439
+}
440
+
441
+// deleteConfig deletes a configuration.
442
+func (ui *webInterface) deleteConfig(w http.ResponseWriter, req *http.Request) {
443
+	name := req.URL.Query().Get("config")
444
+	if err := removeConfig(ui.settingsFile, name); err != nil {
445
+		http.Error(w, err.Error(), http.StatusBadRequest)
446
+		ui.options.UI.PrintErr(err)
447
+		return
448
+	}
449
+}
450
+
430
 // getFromLegend returns the suffix of an entry in legend that starts
451
 // getFromLegend returns the suffix of an entry in legend that starts
431
 // with param.  It returns def if no such entry is found.
452
 // with param.  It returns def if no such entry is found.
432
 func getFromLegend(legend []string, param, def string) string {
453
 func getFromLegend(legend []string, param, def string) string {

+ 1
- 1
internal/driver/webui_test.go Vedi File

56
 	// Start server and wait for it to be initialized
56
 	// Start server and wait for it to be initialized
57
 	go serveWebInterface("unused:1234", prof, &plugin.Options{
57
 	go serveWebInterface("unused:1234", prof, &plugin.Options{
58
 		Obj:        fakeObjTool{},
58
 		Obj:        fakeObjTool{},
59
-		UI:         &proftest.TestUI{},
59
+		UI:         &proftest.TestUI{T: t},
60
 		HTTPServer: creator,
60
 		HTTPServer: creator,
61
 	}, false)
61
 	}, false)
62
 	<-serverCreated
62
 	<-serverCreated