Bladeren bron

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 jaren geleden
bovenliggende
commit
427632fa3b
No account linked to committer's email address

+ 34
- 0
doc/README.md Bestand weergeven

@@ -387,3 +387,37 @@ the symbolization handler.
387 387
 
388 388
 * **-symbolize=demangle=templates:** Demangle, and trim function parameters, but
389 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 Bestand weergeven

@@ -69,8 +69,9 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
69 69
 	flagHTTP := flag.String("http", "", "Present interactive web UI at the specified http host:port")
70 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 76
 	flagCommands := make(map[string]*bool)
76 77
 	flagParamCommands := make(map[string]*string)
@@ -107,8 +108,8 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
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 113
 		return nil, nil, err
113 114
 	}
114 115
 
@@ -124,7 +125,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
124 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 129
 	si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
129 130
 	si = sampleIndex(flagMeanDelay, si, "delay", "-mean_delay", o.UI)
130 131
 	si = sampleIndex(flagContentions, si, "contentions", "-contentions", o.UI)
@@ -132,10 +133,10 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
132 133
 	si = sampleIndex(flagInUseObjects, si, "inuse_objects", "-inuse_objects", o.UI)
133 134
 	si = sampleIndex(flagAllocSpace, si, "alloc_space", "-alloc_space", o.UI)
134 135
 	si = sampleIndex(flagAllocObjects, si, "alloc_objects", "-alloc_objects", o.UI)
135
-	pprofVariables.set("sample_index", si)
136
+	cfg.SampleIndex = si
136 137
 
137 138
 	if *flagMeanDelay {
138
-		pprofVariables.set("mean", "true")
139
+		cfg.Mean = true
139 140
 	}
140 141
 
141 142
 	source := &source{
@@ -154,7 +155,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
154 155
 		return nil, nil, err
155 156
 	}
156 157
 
157
-	normalize := pprofVariables["normalize"].boolValue()
158
+	normalize := cfg.Normalize
158 159
 	if normalize && len(source.Base) == 0 {
159 160
 		return nil, nil, errors.New("must have base profile to normalize by")
160 161
 	}
@@ -163,6 +164,8 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
163 164
 	if bu, ok := o.Obj.(*binutils.Binutils); ok {
164 165
 		bu.SetTools(*flagTools)
165 166
 	}
167
+
168
+	setCurrentConfig(cfg)
166 169
 	return source, cmd, nil
167 170
 }
168 171
 
@@ -194,66 +197,72 @@ func dropEmpty(list []*string) []string {
194 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 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 268
 // isBuildID determines if the profile may contain a build ID, by

+ 81
- 199
internal/driver/commands.go Bestand weergeven

@@ -22,7 +22,6 @@ import (
22 22
 	"os/exec"
23 23
 	"runtime"
24 24
 	"sort"
25
-	"strconv"
26 25
 	"strings"
27 26
 	"time"
28 27
 
@@ -70,9 +69,7 @@ func AddCommand(cmd string, format int, post PostProcessor, desc, usage string)
70 69
 // SetVariableDefault sets the default value for a pprof
71 70
 // variable. This enables extensions to set their own defaults.
72 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 75
 // PostProcessor is a function that applies post-processing to the report output
@@ -124,133 +121,132 @@ var pprofCommands = commands{
124 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 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 129
 	// Comparisons.
134
-	"drop_negative": &variable{boolKind, "f", "", helpText(
130
+	"drop_negative": helpText(
135 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 134
 	// Graph handling options.
139
-	"call_tree": &variable{boolKind, "f", "", helpText(
135
+	"call_tree": helpText(
140 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 139
 	// Display options.
144
-	"relative_percentages": &variable{boolKind, "f", "", helpText(
140
+	"relative_percentages": helpText(
145 141
 		"Show percentages relative to focused subgraph",
146 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 145
 		"Measurement units to display",
150 146
 		"Scale the sample values to this unit.",
151 147
 		"For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
152 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 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 157
 	// Filtering options
162
-	"nodecount": &variable{intKind, "-1", "", helpText(
158
+	"nodecount": helpText(
163 159
 		"Max number of nodes to show",
164 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 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 168
 		"Restricts to samples going through a node matching regexp",
173 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 172
 		"Skips paths going through any nodes matching regexp",
177 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 176
 		"Drops any functions below the matched frame.",
181 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 180
 		"Skips nodes matching regexp",
185 181
 		"Discard nodes that match this location.",
186 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 185
 		"Only show nodes matching regexp",
190 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 189
 		"Drops functions above the highest matched frame.",
194 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 193
 		"Restricts to samples with tags in range or matched by regexp",
198 194
 		"Use name=value syntax to limit the matching to a specific tag.",
199 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 198
 		"Discard samples with tags in range or matched by regexp",
203 199
 		"Use name=value syntax to limit the matching to a specific tag.",
204 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 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 206
 		"Skip tags matching this regexp",
211
-		"Discard tags that match this regexp")},
207
+		"Discard tags that match this regexp"),
212 208
 	// Heap profile options
213
-	"divide_by": &variable{floatKind, "1", "", helpText(
209
+	"divide_by": helpText(
214 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 213
 		"Average sample value over first value (count)",
218 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 217
 		"Sample value to report (0-based index or name)",
222 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 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 227
 	// Output granularity
232
-	"functions": &variable{boolKind, "t", "granularity", helpText(
228
+	"functions": helpText(
233 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 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 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 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 244
 func helpText(s ...string) string {
249 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 250
 func usage(commandLine bool) string {
255 251
 	var prefix string
256 252
 	if commandLine {
@@ -278,34 +274,27 @@ func usage(commandLine bool) string {
278 274
 	help = help + strings.Join(commands, "\n") + "\n\n" +
279 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 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 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 291
 		radioStrings = append(radioStrings, strings.Join(s, "\n"))
306 292
 	}
293
+	sort.Strings(variables)
307 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 300
 func reportHelp(c string, cum, redirect bool) string {
@@ -448,105 +437,8 @@ func invokeVisualizer(suffix string, visualizers []string) PostProcessor {
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 442
 func stringToBool(s string) (bool, error) {
551 443
 	switch strings.ToLower(s) {
552 444
 	case "true", "t", "yes", "y", "1", "":
@@ -557,13 +449,3 @@ func stringToBool(s string) (bool, error) {
557 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 Bestand weergeven

@@ -0,0 +1,367 @@
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 Bestand weergeven

@@ -50,7 +50,7 @@ func PProf(eo *plugin.Options) error {
50 50
 	}
51 51
 
52 52
 	if cmd != nil {
53
-		return generateReport(p, cmd, pprofVariables, o)
53
+		return generateReport(p, cmd, currentConfig(), o)
54 54
 	}
55 55
 
56 56
 	if src.HTTPHostport != "" {
@@ -59,7 +59,7 @@ func PProf(eo *plugin.Options) error {
59 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 63
 	p = p.Copy() // Prevent modification to the incoming profile.
64 64
 
65 65
 	// Identify units of numeric tags in profile.
@@ -71,16 +71,16 @@ func generateRawReport(p *profile.Profile, cmd []string, vars variables, o *plug
71 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 76
 	// Delay focus after configuring report to get percentages on all samples.
77
-	relative := vars["relative_percentages"].boolValue()
77
+	relative := cfg.RelativePercentages
78 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 80
 			return nil, nil, err
81 81
 		}
82 82
 	}
83
-	ropt, err := reportOptions(p, numLabelUnits, vars)
83
+	ropt, err := reportOptions(p, numLabelUnits, cfg)
84 84
 	if err != nil {
85 85
 		return nil, nil, err
86 86
 	}
@@ -95,19 +95,19 @@ func generateRawReport(p *profile.Profile, cmd []string, vars variables, o *plug
95 95
 
96 96
 	rpt := report.New(p, ropt)
97 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 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 103
 		return nil, nil, err
104 104
 	}
105 105
 
106 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 111
 	if err != nil {
112 112
 		return err
113 113
 	}
@@ -129,7 +129,7 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
129 129
 	}
130 130
 
131 131
 	// If no output is specified, use default visualizer.
132
-	output := vars["output"].value
132
+	output := cfg.Output
133 133
 	if output == "" {
134 134
 		if c.visualizer != nil {
135 135
 			return c.visualizer(src, os.Stdout, o.UI)
@@ -151,7 +151,7 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
151 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 155
 	// Some report types override the trim flag to false below. This is to make
156 156
 	// sure the default heuristics of excluding insignificant nodes and edges
157 157
 	// from the call graph do not apply. One example where it is important is
@@ -160,55 +160,55 @@ func applyCommandOverrides(cmd string, outputFormat int, v variables) variables
160 160
 	// data is selected. So, with trimming enabled, the report could end up
161 161
 	// showing no data if the specified function is "uninteresting" as far as the
162 162
 	// trimming is concerned.
163
-	trim := v["trim"].boolValue()
163
+	trim := cfg.Trim
164 164
 
165 165
 	switch cmd {
166 166
 	case "disasm", "weblist":
167 167
 		trim = false
168
-		v.set("addresses", "t")
168
+		cfg.Granularity = "addresses"
169 169
 		// Force the 'noinlines' mode so that source locations for a given address
170 170
 		// collapse and there is only one for the given address. Without this
171 171
 		// cumulative metrics would be double-counted when annotating the assembly.
172 172
 		// This is because the merge is done by address and in case of an inlined
173 173
 		// stack each of the inlined entries is a separate callgraph node.
174
-		v.set("noinlines", "t")
174
+		cfg.NoInlines = true
175 175
 	case "peek":
176 176
 		trim = false
177 177
 	case "list":
178 178
 		trim = false
179
-		v.set("lines", "t")
179
+		cfg.Granularity = "lines"
180 180
 		// Do not force 'noinlines' to be false so that specifying
181 181
 		// "-list foo -noinlines" is supported and works as expected.
182 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 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 192
 	switch outputFormat {
193 193
 	case report.Proto, report.Raw, report.Callgrind:
194 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 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 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 212
 		if inlines {
213 213
 			return nil
214 214
 		}
@@ -216,15 +216,15 @@ func aggregate(prof *profile.Profile, v variables) error {
216 216
 		filename = true
217 217
 		linenumber = true
218 218
 		address = true
219
-	case v["lines"].boolValue():
219
+	case "lines":
220 220
 		function = true
221 221
 		filename = true
222 222
 		linenumber = true
223
-	case v["files"].boolValue():
223
+	case "files":
224 224
 		filename = true
225
-	case v["functions"].boolValue():
225
+	case "functions":
226 226
 		function = true
227
-	case v["filefunctions"].boolValue():
227
+	case "filefunctions":
228 228
 		function = true
229 229
 		filename = true
230 230
 	default:
@@ -233,8 +233,8 @@ func aggregate(prof *profile.Profile, v variables) error {
233 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 238
 	value, meanDiv, sample, err := sampleFormat(p, si, mean)
239 239
 	if err != nil {
240 240
 		return nil, err
@@ -245,29 +245,37 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
245 245
 		stype = "mean_" + stype
246 246
 	}
247 247
 
248
-	if vars["divide_by"].floatValue() == 0 {
248
+	if cfg.DivideBy == 0 {
249 249
 		return nil, fmt.Errorf("zero divisor specified")
250 250
 	}
251 251
 
252 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 254
 		if v != "" {
256 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 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 280
 		ActiveFilters: filters,
273 281
 		NumLabelUnits: numLabelUnits,
@@ -277,12 +285,12 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
277 285
 		SampleType:        stype,
278 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 296
 	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {

+ 11
- 11
internal/driver/driver_focus.go Bestand weergeven

@@ -28,15 +28,15 @@ import (
28 28
 var tagFilterRangeRx = regexp.MustCompile("([+-]?[[:digit:]]+)([[:alpha:]]+)?")
29 29
 
30 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 40
 	if err != nil {
41 41
 		return err
42 42
 	}
@@ -54,8 +54,8 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
54 54
 	warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
55 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 59
 	tns, tnh := prof.FilterTagsByName(tagshow, taghide)
60 60
 	warnNoMatches(tagshow == nil || tns, "TagShow", ui)
61 61
 	warnNoMatches(tagignore == nil || tnh, "TagHide", ui)

+ 25
- 9
internal/driver/driver_test.go Bestand weergeven

@@ -102,12 +102,12 @@ func TestParse(t *testing.T) {
102 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 107
 	for _, tc := range testcase {
108 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 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,8 +141,8 @@ func TestParse(t *testing.T) {
141 141
 			if err := PProf(o1); err != nil {
142 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 147
 			// Read the profile from the encoded protobuf
148 148
 			outputTempFile, err := ioutil.TempFile("", "profile_output")
@@ -1492,6 +1492,23 @@ func TestNumericTagFilter(t *testing.T) {
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 1512
 type testSymbolzMergeFetcher struct{}
1496 1513
 
1497 1514
 func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
@@ -1513,9 +1530,8 @@ func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Pro
1513 1530
 }
1514 1531
 
1515 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 1536
 	f := baseFlags()
1521 1537
 	f.args = []string{

+ 7
- 9
internal/driver/fetch_test.go Bestand weergeven

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

+ 5
- 2
internal/driver/flamegraph.go Bestand weergeven

@@ -38,7 +38,10 @@ type treeNode struct {
38 38
 func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
39 39
 	// Force the call tree so that the graph is a tree.
40 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 45
 	if rpt == nil {
43 46
 		return // error already reported
44 47
 	}
@@ -96,7 +99,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
96 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 103
 		FlameGraph: template.JS(b),
101 104
 		Nodes:      nodeArr,
102 105
 	})

+ 62
- 111
internal/driver/interactive.go Bestand weergeven

@@ -34,17 +34,14 @@ var tailDigitsRE = regexp.MustCompile("[0-9]+$")
34 34
 func interactive(p *profile.Profile, o *plugin.Options) error {
35 35
 	// Enter command processing loop.
36 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 40
 	// Do not wait for the visualizer to complete, to allow multiple
41 41
 	// graphs to be visualized simultaneously.
42 42
 	interactiveMode = true
43 43
 	shortcuts := profileShortcuts(p)
44 44
 
45
-	// Get all groups in pprofVariables to allow for clearer error messages.
46
-	groups := groupOptions(pprofVariables)
47
-
48 45
 	greetings(p, o.UI)
49 46
 	for {
50 47
 		input, err := o.UI.ReadLine("(pprof) ")
@@ -69,9 +66,9 @@ func interactive(p *profile.Profile, o *plugin.Options) error {
69 66
 					}
70 67
 					value = strings.TrimSpace(value)
71 68
 				}
72
-				if v := pprofVariables[name]; v != nil {
69
+				if isConfigurable(name) {
73 70
 					// All non-bool options require inputs
74
-					if v.kind != boolKind && value == "" {
71
+					if len(s) == 1 && !isBoolConfig(name) {
75 72
 						o.UI.PrintErr(fmt.Errorf("please specify a value, e.g. %s=<val>", name))
76 73
 						continue
77 74
 					}
@@ -82,23 +79,17 @@ func interactive(p *profile.Profile, o *plugin.Options) error {
82 79
 							o.UI.PrintErr(err)
83 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 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 89
 						o.UI.PrintErr(err)
89 90
 					}
90 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 95
 			tokens := strings.Fields(input)
@@ -117,9 +108,9 @@ func interactive(p *profile.Profile, o *plugin.Options) error {
117 108
 				continue
118 109
 			}
119 110
 
120
-			args, vars, err := parseCommandLine(tokens)
111
+			args, cfg, err := parseCommandLine(tokens)
121 112
 			if err == nil {
122
-				err = generateReportWrapper(p, args, vars, o)
113
+				err = generateReportWrapper(p, args, cfg, o)
123 114
 			}
124 115
 
125 116
 			if err != nil {
@@ -129,30 +120,13 @@ func interactive(p *profile.Profile, o *plugin.Options) error {
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 123
 var generateReportWrapper = generateReport // For testing purposes.
150 124
 
151 125
 // greetings prints a brief welcome and some overall profile
152 126
 // information before accepting interactive commands.
153 127
 func greetings(p *profile.Profile, ui plugin.UI) {
154 128
 	numLabelUnits := identifyNumLabelUnits(p, ui)
155
-	ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
129
+	ropt, err := reportOptions(p, numLabelUnits, currentConfig())
156 130
 	if err == nil {
157 131
 		rpt := report.New(p, ropt)
158 132
 		ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
@@ -205,27 +179,16 @@ func sampleTypes(p *profile.Profile) []string {
205 179
 
206 180
 func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
207 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 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 187
 		switch {
188
+		case len(f.choices) > 0:
189
+			values := append([]string{}, f.choices...)
190
+			sort.Strings(values)
191
+			comment = "[" + strings.Join(values, " | ") + "]"
229 192
 		case n == "sample_index":
230 193
 			st := sampleTypes(p)
231 194
 			if v == "" {
@@ -247,18 +210,13 @@ func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
247 210
 		}
248 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 213
 	sort.Strings(args)
256 214
 	ui.Print(strings.Join(args, "\n"))
257 215
 }
258 216
 
259 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 220
 	cmd, args := input[:1], input[1:]
263 221
 	name := cmd[0]
264 222
 
@@ -272,28 +230,32 @@ func parseCommandLine(input []string) ([]string, variables, error) {
272 230
 		}
273 231
 	}
274 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 243
 	if c.hasParam {
282 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 247
 		cmd = append(cmd, args[0])
286 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 254
 	var focus, ignore string
293 255
 	for i := 0; i < len(args); i++ {
294 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 259
 			continue
298 260
 		}
299 261
 		switch t[0] {
@@ -302,14 +264,14 @@ func parseCommandLine(input []string) ([]string, variables, error) {
302 264
 			if outputFile == "" {
303 265
 				i++
304 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 269
 				outputFile = args[i]
308 270
 			}
309
-			vcopy.set("output", outputFile)
271
+			vcopy.Output = outputFile
310 272
 		case '-':
311 273
 			if t == "--cum" || t == "-cum" {
312
-				vcopy.set("cum", "t")
274
+				vcopy.Sort = "cum"
313 275
 				continue
314 276
 			}
315 277
 			ignore = catRegex(ignore, t[1:])
@@ -319,30 +281,27 @@ func parseCommandLine(input []string) ([]string, variables, error) {
319 281
 	}
320 282
 
321 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 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 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 305
 func catRegex(a, b string) string {
347 306
 	if a != "" && b != "" {
348 307
 		return a + "|" + b
@@ -370,8 +329,8 @@ func commandHelp(args string, ui plugin.UI) {
370 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 334
 		return
376 335
 	}
377 336
 
@@ -381,18 +340,17 @@ func commandHelp(args string, ui plugin.UI) {
381 340
 // newCompleter creates an autocompletion function for a set of commands.
382 341
 func newCompleter(fns []string) func(string) string {
383 342
 	return func(line string) string {
384
-		v := pprofVariables
385 343
 		switch tokens := strings.Fields(line); len(tokens) {
386 344
 		case 0:
387 345
 			// Nothing to complete
388 346
 		case 1:
389 347
 			// Single token -- complete command name
390
-			if match := matchVariableOrCommand(v, tokens[0]); match != "" {
348
+			if match := matchVariableOrCommand(tokens[0]); match != "" {
391 349
 				return match
392 350
 			}
393 351
 		case 2:
394 352
 			if tokens[0] == "help" {
395
-				if match := matchVariableOrCommand(v, tokens[1]); match != "" {
353
+				if match := matchVariableOrCommand(tokens[1]); match != "" {
396 354
 					return tokens[0] + " " + match
397 355
 				}
398 356
 				return line
@@ -416,26 +374,19 @@ func newCompleter(fns []string) func(string) string {
416 374
 }
417 375
 
418 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 378
 	token = strings.ToLower(token)
421
-	found := ""
379
+	var matches []string
422 380
 	for cmd := range pprofCommands {
423 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 392
 // functionCompleter replaces provided substring with a function

+ 46
- 72
internal/driver/interactive_test.go Bestand weergeven

@@ -37,8 +37,8 @@ func TestShell(t *testing.T) {
37 37
 	savedCommands, pprofCommands = pprofCommands, testCommands
38 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 43
 	shortcuts1, scScript1 := makeShortcuts(interleave(script, 2), 1)
44 44
 	shortcuts2, scScript2 := makeShortcuts(interleave(script, 1), 2)
@@ -55,9 +55,9 @@ func TestShell(t *testing.T) {
55 55
 		{"Random interleave of independent scripts 2", interleave(script, 1), pprofShortcuts, "", 0, false},
56 56
 		{"Random interleave of independent scripts with shortcuts 1", scScript1, shortcuts1, "", 0, false},
57 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 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 61
 		{"No float value provided for the option", []string{"divide_by"}, pprofShortcuts, `please specify a value, e.g. divide_by=<val>`, 1, false},
62 62
 		{"Helpful input format reminder", []string{"sample_index 0"}, pprofShortcuts, `did you mean: sample_index=0`, 1, false},
63 63
 		{"Verify propagation of IO errors", []string{"**error**"}, pprofShortcuts, "", 0, true},
@@ -66,7 +66,7 @@ func TestShell(t *testing.T) {
66 66
 	o := setDefaults(&plugin.Options{HTTPTransport: transport.New(nil)})
67 67
 	for _, tc := range testcases {
68 68
 		t.Run(tc.name, func(t *testing.T) {
69
-			pprofVariables = testVariables(savedVariables)
69
+			setCurrentConfig(savedConfig)
70 70
 			pprofShortcuts = tc.shortcuts
71 71
 			ui := &proftest.TestUI{
72 72
 				T:       t,
@@ -93,38 +93,16 @@ var testCommands = commands{
93 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 96
 // script contains sequences of commands to be executed for testing. Commands
116 97
 // are split by semicolon and interleaved randomly, so they must be
117 98
 // independent from each other.
118 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 108
 func makeShortcuts(input []string, seed int) (shortcuts, []string) {
@@ -153,7 +131,7 @@ func makeShortcuts(input []string, seed int) (shortcuts, []string) {
153 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 135
 	if len(cmd) != 2 {
158 136
 		return fmt.Errorf("expected len(cmd)==2, got %v", cmd)
159 137
 	}
@@ -168,12 +146,12 @@ func checkValue(p *profile.Profile, cmd []string, vars variables, o *plugin.Opti
168 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 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 155
 		return fmt.Errorf("Variable %s, want %s, got %s", name, value, got)
178 156
 	}
179 157
 	return nil
@@ -208,63 +186,59 @@ func TestInteractiveCommands(t *testing.T) {
208 186
 		{
209 187
 			"top 10 --cum focus1 -ignore focus2",
210 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 197
 			"top10 --cum focus1 -ignore focus2",
220 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 207
 			"dot",
230 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 215
 			"tags   -ignore1 -ignore2 focus1 >out",
238 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 226
 			"weblist  find -test",
249 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 236
 			"callgrind   fun -ignore  >out",
261 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,7 +248,7 @@ func TestInteractiveCommands(t *testing.T) {
274 248
 	}
275 249
 
276 250
 	for _, tc := range testcases {
277
-		cmd, vars, err := parseCommandLine(strings.Fields(tc.input))
251
+		cmd, cfg, err := parseCommandLine(strings.Fields(tc.input))
278 252
 		if tc.want == nil && err != nil {
279 253
 			// Error expected
280 254
 			continue
@@ -289,10 +263,10 @@ func TestInteractiveCommands(t *testing.T) {
289 263
 		if c == nil {
290 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 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 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 Bestand weergeven

@@ -0,0 +1,153 @@
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 Bestand weergeven

@@ -0,0 +1,247 @@
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 Bestand weergeven

@@ -166,6 +166,73 @@ a {
166 166
   color: gray;
167 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 237
 #content {
171 238
   overflow-y: scroll;
@@ -284,6 +351,24 @@ table tr td {
284 351
     </div>
285 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 372
   <div>
288 373
     <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
289 374
   </div>
@@ -296,6 +381,31 @@ table tr td {
296 381
   </div>
297 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 409
 <div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
300 410
 {{end}}
301 411
 
@@ -585,6 +695,131 @@ function initMenus() {
585 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 823
 function viewer(baseUrl, nodes) {
589 824
   'use strict';
590 825
 
@@ -877,6 +1112,7 @@ function viewer(baseUrl, nodes) {
877 1112
   }
878 1113
 
879 1114
   addAction('details', handleDetails);
1115
+  initConfigManager();
880 1116
 
881 1117
   search.addEventListener('input', handleSearch);
882 1118
   search.addEventListener('keydown', handleKey);

+ 82
- 61
internal/driver/webui.go Bestand weergeven

@@ -35,22 +35,28 @@ import (
35 35
 
36 36
 // webInterface holds the state needed for serving a browser based interface.
37 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 50
 	templates := template.New("templategroup")
46 51
 	addTemplates(templates)
47 52
 	report.AddSourceTemplates(templates)
48 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 62
 // maxEntries is the maximum number of entries to print for text interfaces.
@@ -80,6 +86,7 @@ type webArgs struct {
80 86
 	TextBody    string
81 87
 	Top         []report.TextItem
82 88
 	FlameGraph  template.JS
89
+	Configs     []configMenuEntry
83 90
 }
84 91
 
85 92
 func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error {
@@ -88,16 +95,20 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, d
88 95
 		return err
89 96
 	}
90 97
 	interactiveMode = true
91
-	ui := makeWebInterface(p, o)
98
+	ui, err := makeWebInterface(p, o)
99
+	if err != nil {
100
+		return err
101
+	}
92 102
 	for n, c := range pprofCommands {
93 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 108
 	ui.help["details"] = "Show information about the profile and this view"
99 109
 	ui.help["graph"] = "Display profile as a directed graph"
100 110
 	ui.help["reset"] = "Show the entire profile"
111
+	ui.help["save_config"] = "Save current settings"
101 112
 
102 113
 	server := o.HTTPServer
103 114
 	if server == nil {
@@ -108,12 +119,14 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, d
108 119
 		Host:     host,
109 120
 		Port:     port,
110 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,21 +219,9 @@ func isLocalhost(host string) bool {
206 219
 
207 220
 func openBrowser(url string, o *plugin.Options) {
208 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 226
 	// Give server a little time to get ready.
226 227
 	time.Sleep(time.Millisecond * 500)
@@ -240,28 +241,23 @@ func openBrowser(url string, o *plugin.Options) {
240 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 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 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 257
 	catcher := &errorCatcher{UI: ui.options.UI}
262 258
 	options := *ui.options
263 259
 	options.UI = catcher
264
-	_, rpt, err := generateRawReport(ui.prof, cmd, v, &options)
260
+	_, rpt, err := generateRawReport(ui.prof, cmd, cfg, &options)
265 261
 	if err != nil {
266 262
 		http.Error(w, err.Error(), http.StatusBadRequest)
267 263
 		ui.options.UI.PrintErr(err)
@@ -271,7 +267,7 @@ func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
271 267
 }
272 268
 
273 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 271
 	rpt *report.Report, errList, legend []string, data webArgs) {
276 272
 	file := getFromLegend(legend, "File: ", "unknown")
277 273
 	profile := getFromLegend(legend, "Type: ", "unknown")
@@ -281,6 +277,8 @@ func (ui *webInterface) render(w http.ResponseWriter, tmpl string,
281 277
 	data.SampleTypes = sampleTypes(ui.prof)
282 278
 	data.Legend = legend
283 279
 	data.Help = ui.help
280
+	data.Configs = configMenu(ui.settingsFile, *req.URL)
281
+
284 282
 	html := &bytes.Buffer{}
285 283
 	if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil {
286 284
 		http.Error(w, "internal template error", http.StatusInternalServerError)
@@ -293,7 +291,7 @@ func (ui *webInterface) render(w http.ResponseWriter, tmpl string,
293 291
 
294 292
 // dot generates a web page containing an svg diagram.
295 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 295
 	if rpt == nil {
298 296
 		return // error already reported
299 297
 	}
@@ -320,7 +318,7 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
320 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 322
 		HTMLBody: template.HTML(string(svg)),
325 323
 		Nodes:    nodes,
326 324
 	})
@@ -345,7 +343,9 @@ func dotToSvg(dot []byte) ([]byte, error) {
345 343
 }
346 344
 
347 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 349
 	if rpt == nil {
350 350
 		return // error already reported
351 351
 	}
@@ -355,7 +355,7 @@ func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
355 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 359
 		Top:   top,
360 360
 		Nodes: nodes,
361 361
 	})
@@ -364,7 +364,7 @@ func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
364 364
 // disasm generates a web page containing disassembly.
365 365
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
366 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 368
 	if rpt == nil {
369 369
 		return // error already reported
370 370
 	}
@@ -377,7 +377,7 @@ func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
377 377
 	}
378 378
 
379 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 381
 		TextBody: out.String(),
382 382
 	})
383 383
 
@@ -387,7 +387,7 @@ func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
387 387
 // data.
388 388
 func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
389 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 391
 	if rpt == nil {
392 392
 		return // error already reported
393 393
 	}
@@ -401,7 +401,7 @@ func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
401 401
 	}
402 402
 
403 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 405
 		HTMLBody: template.HTML(body.String()),
406 406
 	})
407 407
 }
@@ -409,7 +409,9 @@ func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
409 409
 // peek generates a web page listing callers/callers.
410 410
 func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
411 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 415
 	if rpt == nil {
414 416
 		return // error already reported
415 417
 	}
@@ -422,11 +424,30 @@ func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
422 424
 	}
423 425
 
424 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 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 451
 // getFromLegend returns the suffix of an entry in legend that starts
431 452
 // with param.  It returns def if no such entry is found.
432 453
 func getFromLegend(legend []string, param, def string) string {

+ 1
- 1
internal/driver/webui_test.go Bestand weergeven

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