Browse Source

add -diff flag for better profile comparision (#369)

Margaret Nolan 6 years ago
parent
commit
c283c2ce6a

+ 38
- 6
doc/README.md View File

216
 * **-weblist= _regex_:** Generates a source/assembly combined annotated listing for
216
 * **-weblist= _regex_:** Generates a source/assembly combined annotated listing for
217
   functions matching *regex*, and starts a web browser to display it.
217
   functions matching *regex*, and starts a web browser to display it.
218
 
218
 
219
+## Comparing profiles
220
+
221
+pprof can subtract one profile from another, provided the profiles are of
222
+compatible types (i.e. two heap profiles). pprof has two options which can be
223
+used to specify the filename or URL for a profile to be subtracted from the
224
+source profile:
225
+
226
+* **-diff_base= _profile_:** useful for comparing two profiles. Percentages in
227
+the output are relative to the total of samples in the diff base profile.
228
+
229
+* **-base= _profile_:** useful for subtracting a cumulative profile, like a
230
+[golang block profile](https://golang.org/doc/diagnostics.html#profiling),
231
+from another cumulative profile collected from the same program at a later time.
232
+When comparing cumulative profiles collected on the same program, percentages in
233
+the output are relative to the difference between the total for the source
234
+profile and the total for the base profile.
235
+
236
+The **-normalize** flag can be used when a base profile is specified with either 
237
+the `-diff_base` or the `-base` option. This flag scales the source profile so
238
+that the total of samples in the source profile is equal to the total of samples
239
+in the base profile prior to subtracting the base profile from the source
240
+profile. Useful for determining the relative differences between profiles, for
241
+example, which profile has a larger percentage of CPU time used in a particular
242
+function.
243
+
244
+When using the **-diff_base** option, some report entries may have negative
245
+values. If the merged profile is output as a protocol buffer, all samples in the
246
+diff base profile will have a label with the key "pprof::base" and a value of
247
+"true". If pprof is then used to look at the merged profile, it will behave as
248
+if separate source and base profiles were passed in.
249
+
250
+When using the **-base** option to subtract one cumulative profile from another
251
+collected on the same program at a later time, percentages will be relative to
252
+the difference between the total for the source profile and the total for
253
+the base profile, and all values will be positive. In the general case, some 
254
+report entries may have negative values and percentages will be relative to the
255
+total of the absolute value of all samples when aggregated at the address level.
256
+
219
 # Fetching profiles
257
 # Fetching profiles
220
 
258
 
221
 pprof can read profiles from a file or directly from a URL over http. Its native
259
 pprof can read profiles from a file or directly from a URL over http. Its native
237
 distributed job. The profiles may be from different programs but must be
275
 distributed job. The profiles may be from different programs but must be
238
 compatible (for example, CPU profiles cannot be combined with heap profiles).
276
 compatible (for example, CPU profiles cannot be combined with heap profiles).
239
 
277
 
240
-pprof can subtract a profile from another in order to compare them. For that,
241
-use the **-base= _profile_** option, where *profile* is the filename or URL for the
242
-profile to be subtracted. This may result on some report entries having negative
243
-values.
244
-
245
 ## Symbolization
278
 ## Symbolization
246
 
279
 
247
 pprof can add symbol information to a profile that was collected only with
280
 pprof can add symbol information to a profile that was collected only with
286
 
319
 
287
 * **-symbolize=demangle=templates:** Demangle, and trim function parameters, but
320
 * **-symbolize=demangle=templates:** Demangle, and trim function parameters, but
288
   not template parameters.
321
   not template parameters.
289
-

+ 39
- 10
internal/driver/cli.go View File

15
 package driver
15
 package driver
16
 
16
 
17
 import (
17
 import (
18
+	"errors"
18
 	"fmt"
19
 	"fmt"
19
 	"os"
20
 	"os"
20
 	"strings"
21
 	"strings"
28
 	ExecName  string
29
 	ExecName  string
29
 	BuildID   string
30
 	BuildID   string
30
 	Base      []string
31
 	Base      []string
32
+	DiffBase  bool
31
 	Normalize bool
33
 	Normalize bool
32
 
34
 
33
 	Seconds      int
35
 	Seconds      int
43
 func parseFlags(o *plugin.Options) (*source, []string, error) {
45
 func parseFlags(o *plugin.Options) (*source, []string, error) {
44
 	flag := o.Flagset
46
 	flag := o.Flagset
45
 	// Comparisons.
47
 	// Comparisons.
46
-	flagBase := flag.StringList("base", "", "Source for base profile for comparison")
48
+	flagBase := flag.StringList("base", "", "Source for base profile for profile subtraction")
49
+	flagDiffBase := flag.StringList("diff_base", "", "Source for diff base profile for comparison")
47
 	// Source options.
50
 	// Source options.
48
 	flagSymbolize := flag.String("symbolize", "", "Options for profile symbolization")
51
 	flagSymbolize := flag.String("symbolize", "", "Options for profile symbolization")
49
 	flagBuildID := flag.String("buildid", "", "Override build id for first mapping")
52
 	flagBuildID := flag.String("buildid", "", "Override build id for first mapping")
85
 			usageMsgVars)
88
 			usageMsgVars)
86
 	})
89
 	})
87
 	if len(args) == 0 {
90
 	if len(args) == 0 {
88
-		return nil, nil, fmt.Errorf("no profile source specified")
91
+		return nil, nil, errors.New("no profile source specified")
89
 	}
92
 	}
90
 
93
 
91
 	var execName string
94
 	var execName string
112
 		return nil, nil, err
115
 		return nil, nil, err
113
 	}
116
 	}
114
 	if cmd != nil && *flagHTTP != "" {
117
 	if cmd != nil && *flagHTTP != "" {
115
-		return nil, nil, fmt.Errorf("-http is not compatible with an output format on the command line")
118
+		return nil, nil, errors.New("-http is not compatible with an output format on the command line")
116
 	}
119
 	}
117
 
120
 
118
 	si := pprofVariables["sample_index"].value
121
 	si := pprofVariables["sample_index"].value
140
 		Comment:      *flagAddComment,
143
 		Comment:      *flagAddComment,
141
 	}
144
 	}
142
 
145
 
143
-	for _, s := range *flagBase {
144
-		if *s != "" {
145
-			source.Base = append(source.Base, *s)
146
-		}
146
+	if err := source.addBaseProfiles(*flagBase, *flagDiffBase); err != nil {
147
+		return nil, nil, err
147
 	}
148
 	}
148
 
149
 
149
 	normalize := pprofVariables["normalize"].boolValue()
150
 	normalize := pprofVariables["normalize"].boolValue()
150
 	if normalize && len(source.Base) == 0 {
151
 	if normalize && len(source.Base) == 0 {
151
-		return nil, nil, fmt.Errorf("Must have base profile to normalize by")
152
+		return nil, nil, errors.New("must have base profile to normalize by")
152
 	}
153
 	}
153
 	source.Normalize = normalize
154
 	source.Normalize = normalize
154
 
155
 
158
 	return source, cmd, nil
159
 	return source, cmd, nil
159
 }
160
 }
160
 
161
 
162
+// addBaseProfiles adds the list of base profiles or diff base profiles to
163
+// the source. This function will return an error if both base and diff base
164
+// profiles are specified.
165
+func (source *source) addBaseProfiles(flagBase, flagDiffBase []*string) error {
166
+	base, diffBase := dropEmpty(flagBase), dropEmpty(flagDiffBase)
167
+	if len(base) > 0 && len(diffBase) > 0 {
168
+		return errors.New("-base and -diff_base flags cannot both be specified")
169
+	}
170
+
171
+	source.Base = base
172
+	if len(diffBase) > 0 {
173
+		source.Base, source.DiffBase = diffBase, true
174
+	}
175
+	return nil
176
+}
177
+
178
+// dropEmpty list takes a slice of string pointers, and outputs a slice of
179
+// non-empty strings associated with the flag.
180
+func dropEmpty(list []*string) []string {
181
+	var l []string
182
+	for _, s := range list {
183
+		if *s != "" {
184
+			l = append(l, *s)
185
+		}
186
+	}
187
+	return l
188
+}
189
+
161
 // installFlags creates command line flags for pprof variables.
190
 // installFlags creates command line flags for pprof variables.
162
 func installFlags(flag plugin.FlagSet) flagsInstalled {
191
 func installFlags(flag plugin.FlagSet) flagsInstalled {
163
 	f := flagsInstalled{
192
 	f := flagsInstalled{
240
 	for n, b := range bcmd {
269
 	for n, b := range bcmd {
241
 		if *b {
270
 		if *b {
242
 			if cmd != nil {
271
 			if cmd != nil {
243
-				return nil, fmt.Errorf("must set at most one output format")
272
+				return nil, errors.New("must set at most one output format")
244
 			}
273
 			}
245
 			cmd = []string{n}
274
 			cmd = []string{n}
246
 		}
275
 		}
248
 	for n, s := range acmd {
277
 	for n, s := range acmd {
249
 		if *s != "" {
278
 		if *s != "" {
250
 			if cmd != nil {
279
 			if cmd != nil {
251
-				return nil, fmt.Errorf("must set at most one output format")
280
+				return nil, errors.New("must set at most one output format")
252
 			}
281
 			}
253
 			cmd = []string{n, *s}
282
 			cmd = []string{n, *s}
254
 		}
283
 		}

+ 7
- 2
internal/driver/driver_test.go View File

288
 	floats      map[string]float64
288
 	floats      map[string]float64
289
 	strings     map[string]string
289
 	strings     map[string]string
290
 	args        []string
290
 	args        []string
291
-	stringLists map[string][]*string
291
+	stringLists map[string][]string
292
 }
292
 }
293
 
293
 
294
 func (testFlags) ExtraUsage() string { return "" }
294
 func (testFlags) ExtraUsage() string { return "" }
355
 
355
 
356
 func (f testFlags) StringList(s, d, c string) *[]*string {
356
 func (f testFlags) StringList(s, d, c string) *[]*string {
357
 	if t, ok := f.stringLists[s]; ok {
357
 	if t, ok := f.stringLists[s]; ok {
358
-		return &t
358
+		// convert slice of strings to slice of string pointers before returning.
359
+		tp := make([]*string, len(t))
360
+		for i, v := range t {
361
+			tp[i] = &v
362
+		}
363
+		return &tp
359
 	}
364
 	}
360
 	return &[]*string{}
365
 	return &[]*string{}
361
 }
366
 }

+ 3
- 0
internal/driver/fetch.go View File

63
 	}
63
 	}
64
 
64
 
65
 	if pbase != nil {
65
 	if pbase != nil {
66
+		if s.DiffBase {
67
+			pbase.SetLabel("pprof::base", []string{"true"})
68
+		}
66
 		if s.Normalize {
69
 		if s.Normalize {
67
 			err := p.Normalize(pbase)
70
 			err := p.Normalize(pbase)
68
 			if err != nil {
71
 			if err != nil {

+ 206
- 34
internal/driver/fetch_test.go View File

210
 	baseVars := pprofVariables
210
 	baseVars := pprofVariables
211
 	defer func() { pprofVariables = baseVars }()
211
 	defer func() { pprofVariables = baseVars }()
212
 
212
 
213
+	type WantSample struct {
214
+		values []int64
215
+		labels map[string][]string
216
+	}
217
+
213
 	const path = "testdata/"
218
 	const path = "testdata/"
214
 	type testcase struct {
219
 	type testcase struct {
215
-		desc            string
216
-		sources         []string
217
-		bases           []string
218
-		normalize       bool
219
-		expectedSamples [][]int64
220
+		desc         string
221
+		sources      []string
222
+		bases        []string
223
+		diffBases    []string
224
+		normalize    bool
225
+		wantSamples  []WantSample
226
+		wantErrorMsg string
220
 	}
227
 	}
221
 
228
 
222
 	testcases := []testcase{
229
 	testcases := []testcase{
224
 			"not normalized base is same as source",
231
 			"not normalized base is same as source",
225
 			[]string{path + "cppbench.contention"},
232
 			[]string{path + "cppbench.contention"},
226
 			[]string{path + "cppbench.contention"},
233
 			[]string{path + "cppbench.contention"},
234
+			nil,
235
+			false,
236
+			nil,
237
+			"",
238
+		},
239
+		{
240
+			"not normalized base is same as source",
241
+			[]string{path + "cppbench.contention"},
242
+			[]string{path + "cppbench.contention"},
243
+			nil,
227
 			false,
244
 			false,
228
-			[][]int64{},
245
+			nil,
246
+			"",
229
 		},
247
 		},
230
 		{
248
 		{
231
 			"not normalized single source, multiple base (all profiles same)",
249
 			"not normalized single source, multiple base (all profiles same)",
232
 			[]string{path + "cppbench.contention"},
250
 			[]string{path + "cppbench.contention"},
233
 			[]string{path + "cppbench.contention", path + "cppbench.contention"},
251
 			[]string{path + "cppbench.contention", path + "cppbench.contention"},
252
+			nil,
234
 			false,
253
 			false,
235
-			[][]int64{{-2700, -608881724}, {-100, -23992}, {-200, -179943}, {-100, -17778444}, {-100, -75976}, {-300, -63568134}},
254
+			[]WantSample{
255
+				{
256
+					values: []int64{-2700, -608881724},
257
+					labels: map[string][]string{},
258
+				},
259
+				{
260
+					values: []int64{-100, -23992},
261
+					labels: map[string][]string{},
262
+				},
263
+				{
264
+					values: []int64{-200, -179943},
265
+					labels: map[string][]string{},
266
+				},
267
+				{
268
+					values: []int64{-100, -17778444},
269
+					labels: map[string][]string{},
270
+				},
271
+				{
272
+					values: []int64{-100, -75976},
273
+					labels: map[string][]string{},
274
+				},
275
+				{
276
+					values: []int64{-300, -63568134},
277
+					labels: map[string][]string{},
278
+				},
279
+			},
280
+			"",
236
 		},
281
 		},
237
 		{
282
 		{
238
 			"not normalized, different base and source",
283
 			"not normalized, different base and source",
239
 			[]string{path + "cppbench.contention"},
284
 			[]string{path + "cppbench.contention"},
240
 			[]string{path + "cppbench.small.contention"},
285
 			[]string{path + "cppbench.small.contention"},
286
+			nil,
241
 			false,
287
 			false,
242
-			[][]int64{{1700, 608878600}, {100, 23992}, {200, 179943}, {100, 17778444}, {100, 75976}, {300, 63568134}},
288
+			[]WantSample{
289
+				{
290
+					values: []int64{1700, 608878600},
291
+					labels: map[string][]string{},
292
+				},
293
+				{
294
+					values: []int64{100, 23992},
295
+					labels: map[string][]string{},
296
+				},
297
+				{
298
+					values: []int64{200, 179943},
299
+					labels: map[string][]string{},
300
+				},
301
+				{
302
+					values: []int64{100, 17778444},
303
+					labels: map[string][]string{},
304
+				},
305
+				{
306
+					values: []int64{100, 75976},
307
+					labels: map[string][]string{},
308
+				},
309
+				{
310
+					values: []int64{300, 63568134},
311
+					labels: map[string][]string{},
312
+				},
313
+			},
314
+			"",
243
 		},
315
 		},
244
 		{
316
 		{
245
 			"normalized base is same as source",
317
 			"normalized base is same as source",
246
 			[]string{path + "cppbench.contention"},
318
 			[]string{path + "cppbench.contention"},
247
 			[]string{path + "cppbench.contention"},
319
 			[]string{path + "cppbench.contention"},
320
+			nil,
248
 			true,
321
 			true,
249
-			[][]int64{},
322
+			nil,
323
+			"",
250
 		},
324
 		},
251
 		{
325
 		{
252
 			"normalized single source, multiple base (all profiles same)",
326
 			"normalized single source, multiple base (all profiles same)",
253
 			[]string{path + "cppbench.contention"},
327
 			[]string{path + "cppbench.contention"},
254
 			[]string{path + "cppbench.contention", path + "cppbench.contention"},
328
 			[]string{path + "cppbench.contention", path + "cppbench.contention"},
329
+			nil,
255
 			true,
330
 			true,
256
-			[][]int64{},
331
+			nil,
332
+			"",
257
 		},
333
 		},
258
 		{
334
 		{
259
 			"normalized different base and source",
335
 			"normalized different base and source",
260
 			[]string{path + "cppbench.contention"},
336
 			[]string{path + "cppbench.contention"},
261
 			[]string{path + "cppbench.small.contention"},
337
 			[]string{path + "cppbench.small.contention"},
338
+			nil,
262
 			true,
339
 			true,
263
-			[][]int64{{-229, -370}, {28, 0}, {57, 0}, {28, 80}, {28, 0}, {85, 287}},
340
+			[]WantSample{
341
+				{
342
+					values: []int64{-229, -370},
343
+					labels: map[string][]string{},
344
+				},
345
+				{
346
+					values: []int64{28, 0},
347
+					labels: map[string][]string{},
348
+				},
349
+				{
350
+					values: []int64{57, 0},
351
+					labels: map[string][]string{},
352
+				},
353
+				{
354
+					values: []int64{28, 80},
355
+					labels: map[string][]string{},
356
+				},
357
+				{
358
+					values: []int64{28, 0},
359
+					labels: map[string][]string{},
360
+				},
361
+				{
362
+					values: []int64{85, 287},
363
+					labels: map[string][]string{},
364
+				},
365
+			},
366
+			"",
367
+		},
368
+		{
369
+			"not normalized diff base is same as source",
370
+			[]string{path + "cppbench.contention"},
371
+			nil,
372
+			[]string{path + "cppbench.contention"},
373
+			false,
374
+			[]WantSample{
375
+				{
376
+					values: []int64{2700, 608881724},
377
+					labels: map[string][]string{},
378
+				},
379
+				{
380
+					values: []int64{100, 23992},
381
+					labels: map[string][]string{},
382
+				},
383
+				{
384
+					values: []int64{200, 179943},
385
+					labels: map[string][]string{},
386
+				},
387
+				{
388
+					values: []int64{100, 17778444},
389
+					labels: map[string][]string{},
390
+				},
391
+				{
392
+					values: []int64{100, 75976},
393
+					labels: map[string][]string{},
394
+				},
395
+				{
396
+					values: []int64{300, 63568134},
397
+					labels: map[string][]string{},
398
+				},
399
+				{
400
+					values: []int64{-2700, -608881724},
401
+					labels: map[string][]string{"pprof::base": {"true"}},
402
+				},
403
+				{
404
+					values: []int64{-100, -23992},
405
+					labels: map[string][]string{"pprof::base": {"true"}},
406
+				},
407
+				{
408
+					values: []int64{-200, -179943},
409
+					labels: map[string][]string{"pprof::base": {"true"}},
410
+				},
411
+				{
412
+					values: []int64{-100, -17778444},
413
+					labels: map[string][]string{"pprof::base": {"true"}},
414
+				},
415
+				{
416
+					values: []int64{-100, -75976},
417
+					labels: map[string][]string{"pprof::base": {"true"}},
418
+				},
419
+				{
420
+					values: []int64{-300, -63568134},
421
+					labels: map[string][]string{"pprof::base": {"true"}},
422
+				},
423
+			},
424
+			"",
425
+		},
426
+		{
427
+			"diff_base and base both specified",
428
+			[]string{path + "cppbench.contention"},
429
+			[]string{path + "cppbench.contention"},
430
+			[]string{path + "cppbench.contention"},
431
+			false,
432
+			nil,
433
+			"-base and -diff_base flags cannot both be specified",
264
 		},
434
 		},
265
 	}
435
 	}
266
 
436
 
267
 	for _, tc := range testcases {
437
 	for _, tc := range testcases {
268
 		t.Run(tc.desc, func(t *testing.T) {
438
 		t.Run(tc.desc, func(t *testing.T) {
269
 			pprofVariables = baseVars.makeCopy()
439
 			pprofVariables = baseVars.makeCopy()
270
-
271
-			base := make([]*string, len(tc.bases))
272
-			for i, s := range tc.bases {
273
-				base[i] = &s
274
-			}
275
-
276
 			f := testFlags{
440
 			f := testFlags{
277
-				stringLists: map[string][]*string{
278
-					"base": base,
441
+				stringLists: map[string][]string{
442
+					"base":      tc.bases,
443
+					"diff_base": tc.diffBases,
279
 				},
444
 				},
280
 				bools: map[string]bool{
445
 				bools: map[string]bool{
281
 					"normalize": tc.normalize,
446
 					"normalize": tc.normalize,
289
 			})
454
 			})
290
 			src, _, err := parseFlags(o)
455
 			src, _, err := parseFlags(o)
291
 
456
 
457
+			if tc.wantErrorMsg != "" {
458
+				if err == nil {
459
+					t.Fatalf("got nil, want error %q", tc.wantErrorMsg)
460
+				}
461
+
462
+				if gotErrMsg := err.Error(); gotErrMsg != tc.wantErrorMsg {
463
+					t.Fatalf("got error %q, want error %q", gotErrMsg, tc.wantErrorMsg)
464
+				}
465
+				return
466
+			}
467
+
292
 			if err != nil {
468
 			if err != nil {
293
-				t.Fatalf("%s: %v", tc.desc, err)
469
+				t.Fatalf("got error %q, want no error", err)
294
 			}
470
 			}
295
 
471
 
296
 			p, err := fetchProfiles(src, o)
472
 			p, err := fetchProfiles(src, o)
297
-			pprofVariables = baseVars
473
+
298
 			if err != nil {
474
 			if err != nil {
299
-				t.Fatal(err)
475
+				t.Fatalf("got error %q, want no error", err)
300
 			}
476
 			}
301
 
477
 
302
-			if want, got := len(tc.expectedSamples), len(p.Sample); want != got {
303
-				t.Fatalf("want %d samples got %d", want, got)
478
+			if got, want := len(p.Sample), len(tc.wantSamples); got != want {
479
+				t.Fatalf("got %d samples want %d", got, want)
304
 			}
480
 			}
305
 
481
 
306
-			if len(p.Sample) > 0 {
307
-				for i, sample := range p.Sample {
308
-					if want, got := len(tc.expectedSamples[i]), len(sample.Value); want != got {
309
-						t.Errorf("want %d values for sample %d, got %d", want, i, got)
310
-					}
311
-					for j, value := range sample.Value {
312
-						if want, got := tc.expectedSamples[i][j], value; want != got {
313
-							t.Errorf("want value of %d for value %d of sample %d, got %d", want, j, i, got)
314
-						}
315
-					}
482
+			for i, sample := range p.Sample {
483
+				if !reflect.DeepEqual(tc.wantSamples[i].values, sample.Value) {
484
+					t.Errorf("for sample %d got values %v, want %v", i, sample.Value, tc.wantSamples[i])
485
+				}
486
+				if !reflect.DeepEqual(tc.wantSamples[i].labels, sample.Label) {
487
+					t.Errorf("for sample %d got labels %v, want %v", i, sample.Label, tc.wantSamples[i].labels)
316
 				}
488
 				}
317
 			}
489
 			}
318
 		})
490
 		})

+ 19
- 6
internal/report/report.go View File

264
 		s.NumUnit = numUnits
264
 		s.NumUnit = numUnits
265
 	}
265
 	}
266
 
266
 
267
+	// Remove label marking samples from the base profiles, so it does not appear
268
+	// as a nodelet in the graph view.
269
+	prof.RemoveLabel("pprof::base")
270
+
267
 	formatTag := func(v int64, key string) string {
271
 	formatTag := func(v int64, key string) string {
268
 		return measurement.ScaledLabel(v, key, o.OutputUnit)
272
 		return measurement.ScaledLabel(v, key, o.OutputUnit)
269
 	}
273
 	}
1212
 	return New(prof, o)
1216
 	return New(prof, o)
1213
 }
1217
 }
1214
 
1218
 
1215
-// computeTotal computes the sum of all sample values. This will be
1216
-// used to compute percentages.
1219
+// computeTotal computes the sum of the absolute value of all sample values.
1220
+// If any samples have the label "pprof::base" with value "true", then the total
1221
+// will only include samples with that label.
1217
 func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 {
1222
 func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 {
1218
-	var div, ret int64
1223
+	var div, total, diffDiv, diffTotal int64
1219
 	for _, sample := range prof.Sample {
1224
 	for _, sample := range prof.Sample {
1220
 		var d, v int64
1225
 		var d, v int64
1221
 		v = value(sample.Value)
1226
 		v = value(sample.Value)
1225
 		if v < 0 {
1230
 		if v < 0 {
1226
 			v = -v
1231
 			v = -v
1227
 		}
1232
 		}
1228
-		ret += v
1233
+		total += v
1229
 		div += d
1234
 		div += d
1235
+		if sample.HasLabel("pprof::base", "true") {
1236
+			diffTotal += v
1237
+			diffDiv += d
1238
+		}
1239
+	}
1240
+	if diffTotal > 0 {
1241
+		total = diffTotal
1242
+		div = diffDiv
1230
 	}
1243
 	}
1231
 	if div != 0 {
1244
 	if div != 0 {
1232
-		return ret / div
1245
+		return total / div
1233
 	}
1246
 	}
1234
-	return ret
1247
+	return total
1235
 }
1248
 }
1236
 
1249
 
1237
 // Report contains the data and associated routines to extract a
1250
 // Report contains the data and associated routines to extract a

+ 118
- 0
internal/report/report_test.go View File

287
 		}
287
 		}
288
 	}
288
 	}
289
 }
289
 }
290
+
291
+func TestComputeTotal(t *testing.T) {
292
+	p1 := testProfile.Copy()
293
+	p1.Sample = []*profile.Sample{
294
+		{
295
+			Location: []*profile.Location{testL[0]},
296
+			Value:    []int64{1, 1},
297
+		},
298
+		{
299
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
300
+			Value:    []int64{1, 10},
301
+		},
302
+		{
303
+			Location: []*profile.Location{testL[4], testL[2], testL[0]},
304
+			Value:    []int64{1, 100},
305
+		},
306
+	}
307
+
308
+	p2 := testProfile.Copy()
309
+	p2.Sample = []*profile.Sample{
310
+		{
311
+			Location: []*profile.Location{testL[0]},
312
+			Value:    []int64{1, 1},
313
+		},
314
+		{
315
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
316
+			Value:    []int64{1, -10},
317
+		},
318
+		{
319
+			Location: []*profile.Location{testL[4], testL[2], testL[0]},
320
+			Value:    []int64{1, 100},
321
+		},
322
+	}
323
+
324
+	p3 := testProfile.Copy()
325
+	p3.Sample = []*profile.Sample{
326
+		{
327
+			Location: []*profile.Location{testL[0]},
328
+			Value:    []int64{10000, 1},
329
+		},
330
+		{
331
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
332
+			Value:    []int64{-10, 3},
333
+			Label:    map[string][]string{"pprof::base": {"true"}},
334
+		},
335
+		{
336
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
337
+			Value:    []int64{1000, -10},
338
+		},
339
+		{
340
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
341
+			Value:    []int64{-9000, 3},
342
+			Label:    map[string][]string{"pprof::base": {"true"}},
343
+		},
344
+		{
345
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
346
+			Value:    []int64{-1, 3},
347
+			Label:    map[string][]string{"pprof::base": {"true"}},
348
+		},
349
+		{
350
+			Location: []*profile.Location{testL[4], testL[2], testL[0]},
351
+			Value:    []int64{100, 100},
352
+		},
353
+		{
354
+			Location: []*profile.Location{testL[2], testL[1], testL[0]},
355
+			Value:    []int64{100, 3},
356
+			Label:    map[string][]string{"pprof::base": {"true"}},
357
+		},
358
+	}
359
+
360
+	testcases := []struct {
361
+		desc           string
362
+		prof           *profile.Profile
363
+		value, meanDiv func(v []int64) int64
364
+		wantTotal      int64
365
+	}{
366
+		{
367
+			desc: "no diff base, all positive values, index 1",
368
+			prof: p1,
369
+			value: func(v []int64) int64 {
370
+				return v[0]
371
+			},
372
+			wantTotal: 3,
373
+		},
374
+		{
375
+			desc: "no diff base, all positive values, index 2",
376
+			prof: p1,
377
+			value: func(v []int64) int64 {
378
+				return v[1]
379
+			},
380
+			wantTotal: 111,
381
+		},
382
+		{
383
+			desc: "no diff base, some negative values",
384
+			prof: p2,
385
+			value: func(v []int64) int64 {
386
+				return v[1]
387
+			},
388
+			wantTotal: 111,
389
+		},
390
+		{
391
+			desc: "diff base, some negative values",
392
+			prof: p3,
393
+			value: func(v []int64) int64 {
394
+				return v[0]
395
+			},
396
+			wantTotal: 9111,
397
+		},
398
+	}
399
+
400
+	for _, tc := range testcases {
401
+		t.Run(tc.desc, func(t *testing.T) {
402
+			if gotTotal := computeTotal(tc.prof, tc.value, tc.meanDiv); gotTotal != tc.wantTotal {
403
+				t.Errorf("got total %d, want %v", gotTotal, tc.wantTotal)
404
+			}
405
+		})
406
+	}
407
+}

+ 30
- 0
profile/profile.go View File

674
 	return strings.Join(ls, " ")
674
 	return strings.Join(ls, " ")
675
 }
675
 }
676
 
676
 
677
+// SetLabel sets the specified key to the specified value for all samples in the
678
+// profile.
679
+func (p *Profile) SetLabel(key string, value []string) {
680
+	for _, sample := range p.Sample {
681
+		if sample.Label == nil {
682
+			sample.Label = map[string][]string{key: value}
683
+		} else {
684
+			sample.Label[key] = value
685
+		}
686
+	}
687
+}
688
+
689
+// RemoveLabel removes all labels associated with the specified key for all
690
+// samples in the profile.
691
+func (p *Profile) RemoveLabel(key string) {
692
+	for _, sample := range p.Sample {
693
+		delete(sample.Label, key)
694
+	}
695
+}
696
+
697
+// HasLabel returns true if a sample has a label with indicated key and value.
698
+func (s *Sample) HasLabel(key, value string) bool {
699
+	for _, v := range s.Label[key] {
700
+		if v == value {
701
+			return true
702
+		}
703
+	}
704
+	return false
705
+}
706
+
677
 // Scale multiplies all sample values in a profile by a constant.
707
 // Scale multiplies all sample values in a profile by a constant.
678
 func (p *Profile) Scale(ratio float64) {
708
 func (p *Profile) Scale(ratio float64) {
679
 	if ratio == 1 {
709
 	if ratio == 1 {

+ 266
- 1
profile/profile_test.go View File

741
 		wantNumUnits  []map[string][]string
741
 		wantNumUnits  []map[string][]string
742
 	}{
742
 	}{
743
 		{
743
 		{
744
-			name:  "different tag units not merged",
744
+			name:  "different label units not merged",
745
 			profs: []*Profile{testProfile4.Copy(), testProfile5.Copy()},
745
 			profs: []*Profile{testProfile4.Copy(), testProfile5.Copy()},
746
 			wantNumLabels: []map[string][]int64{
746
 			wantNumLabels: []map[string][]int64{
747
 				{
747
 				{
912
 	return tb
912
 	return tb
913
 }
913
 }
914
 
914
 
915
+func TestHasLabel(t *testing.T) {
916
+	var testcases = []struct {
917
+		desc         string
918
+		labels       map[string][]string
919
+		key          string
920
+		value        string
921
+		wantHasLabel bool
922
+	}{
923
+		{
924
+			desc:         "empty label does not have label",
925
+			labels:       map[string][]string{},
926
+			key:          "key",
927
+			value:        "value",
928
+			wantHasLabel: false,
929
+		},
930
+		{
931
+			desc:         "label with one key and value has label",
932
+			labels:       map[string][]string{"key": {"value"}},
933
+			key:          "key",
934
+			value:        "value",
935
+			wantHasLabel: true,
936
+		},
937
+		{
938
+			desc:         "label with one key and value does not have label",
939
+			labels:       map[string][]string{"key": {"value"}},
940
+			key:          "key1",
941
+			value:        "value1",
942
+			wantHasLabel: false,
943
+		},
944
+		{
945
+			desc: "label with many keys and values has label",
946
+			labels: map[string][]string{
947
+				"key1": {"value2", "value1"},
948
+				"key2": {"value1", "value2", "value2"},
949
+				"key3": {"value1", "value2", "value2"},
950
+			},
951
+			key:          "key1",
952
+			value:        "value1",
953
+			wantHasLabel: true,
954
+		},
955
+		{
956
+			desc: "label with many keys and values does not have label",
957
+			labels: map[string][]string{
958
+				"key1": {"value2", "value1"},
959
+				"key2": {"value1", "value2", "value2"},
960
+				"key3": {"value1", "value2", "value2"},
961
+			},
962
+			key:          "key5",
963
+			value:        "value5",
964
+			wantHasLabel: false,
965
+		},
966
+	}
967
+
968
+	for _, tc := range testcases {
969
+		t.Run(tc.desc, func(t *testing.T) {
970
+			sample := &Sample{
971
+				Label: tc.labels,
972
+			}
973
+			if gotHasLabel := sample.HasLabel(tc.key, tc.value); gotHasLabel != tc.wantHasLabel {
974
+				t.Errorf("sample.HasLabel(%q, %q) got %v, want %v", tc.key, tc.value, gotHasLabel, tc.wantHasLabel)
975
+			}
976
+		})
977
+	}
978
+}
979
+
980
+func TestRemove(t *testing.T) {
981
+	var testcases = []struct {
982
+		desc       string
983
+		samples    []*Sample
984
+		removeKey  string
985
+		wantLabels []map[string][]string
986
+	}{
987
+		{
988
+			desc: "some samples have label already",
989
+			samples: []*Sample{
990
+				{
991
+					Location: []*Location{cpuL[0]},
992
+					Value:    []int64{1000},
993
+				},
994
+				{
995
+					Location: []*Location{cpuL[0]},
996
+					Value:    []int64{1000},
997
+					Label: map[string][]string{
998
+						"key1": {"value1", "value2", "value3"},
999
+						"key2": {"value1"},
1000
+					},
1001
+				},
1002
+				{
1003
+					Location: []*Location{cpuL[0]},
1004
+					Value:    []int64{1000},
1005
+					Label: map[string][]string{
1006
+						"key1": {"value2"},
1007
+					},
1008
+				},
1009
+			},
1010
+			removeKey: "key1",
1011
+			wantLabels: []map[string][]string{
1012
+				{},
1013
+				{"key2": {"value1"}},
1014
+				{},
1015
+			},
1016
+		},
1017
+	}
1018
+
1019
+	for _, tc := range testcases {
1020
+		t.Run(tc.desc, func(t *testing.T) {
1021
+			profile := testProfile1.Copy()
1022
+			profile.Sample = tc.samples
1023
+			profile.RemoveLabel(tc.removeKey)
1024
+			if got, want := len(profile.Sample), len(tc.wantLabels); got != want {
1025
+				t.Fatalf("got %v samples, want %v samples", got, want)
1026
+			}
1027
+			for i, sample := range profile.Sample {
1028
+				wantLabels := tc.wantLabels[i]
1029
+				if got, want := len(sample.Label), len(wantLabels); got != want {
1030
+					t.Errorf("got %v label keys for sample %v, want %v", got, i, want)
1031
+					continue
1032
+				}
1033
+				for wantKey, wantValues := range wantLabels {
1034
+					if gotValues, ok := sample.Label[wantKey]; ok {
1035
+						if !reflect.DeepEqual(gotValues, wantValues) {
1036
+							t.Errorf("for key %s, got values %v, want values %v", wantKey, gotValues, wantValues)
1037
+						}
1038
+					} else {
1039
+						t.Errorf("for key %s got no values, want %v", wantKey, wantValues)
1040
+					}
1041
+				}
1042
+			}
1043
+		})
1044
+	}
1045
+}
1046
+
1047
+func TestSetLabel(t *testing.T) {
1048
+	var testcases = []struct {
1049
+		desc       string
1050
+		samples    []*Sample
1051
+		setKey     string
1052
+		setVal     []string
1053
+		wantLabels []map[string][]string
1054
+	}{
1055
+		{
1056
+			desc: "some samples have label already",
1057
+			samples: []*Sample{
1058
+				{
1059
+					Location: []*Location{cpuL[0]},
1060
+					Value:    []int64{1000},
1061
+				},
1062
+				{
1063
+					Location: []*Location{cpuL[0]},
1064
+					Value:    []int64{1000},
1065
+					Label: map[string][]string{
1066
+						"key1": {"value1", "value2", "value3"},
1067
+						"key2": {"value1"},
1068
+					},
1069
+				},
1070
+				{
1071
+					Location: []*Location{cpuL[0]},
1072
+					Value:    []int64{1000},
1073
+					Label: map[string][]string{
1074
+						"key1": {"value2"},
1075
+					},
1076
+				},
1077
+			},
1078
+			setKey: "key1",
1079
+			setVal: []string{"value1"},
1080
+			wantLabels: []map[string][]string{
1081
+				{"key1": {"value1"}},
1082
+				{"key1": {"value1"}, "key2": {"value1"}},
1083
+				{"key1": {"value1"}},
1084
+			},
1085
+		},
1086
+		{
1087
+			desc: "no samples have labels",
1088
+			samples: []*Sample{
1089
+				{
1090
+					Location: []*Location{cpuL[0]},
1091
+					Value:    []int64{1000},
1092
+				},
1093
+			},
1094
+			setKey: "key1",
1095
+			setVal: []string{"value1"},
1096
+			wantLabels: []map[string][]string{
1097
+				{"key1": {"value1"}},
1098
+			},
1099
+		},
1100
+		{
1101
+			desc: "all samples have some labels, but not key being added",
1102
+			samples: []*Sample{
1103
+				{
1104
+					Location: []*Location{cpuL[0]},
1105
+					Value:    []int64{1000},
1106
+					Label: map[string][]string{
1107
+						"key2": {"value2"},
1108
+					},
1109
+				},
1110
+				{
1111
+					Location: []*Location{cpuL[0]},
1112
+					Value:    []int64{1000},
1113
+					Label: map[string][]string{
1114
+						"key3": {"value3"},
1115
+					},
1116
+				},
1117
+			},
1118
+			setKey: "key1",
1119
+			setVal: []string{"value1"},
1120
+			wantLabels: []map[string][]string{
1121
+				{"key1": {"value1"}, "key2": {"value2"}},
1122
+				{"key1": {"value1"}, "key3": {"value3"}},
1123
+			},
1124
+		},
1125
+		{
1126
+			desc: "all samples have key being added",
1127
+			samples: []*Sample{
1128
+				{
1129
+					Location: []*Location{cpuL[0]},
1130
+					Value:    []int64{1000},
1131
+					Label: map[string][]string{
1132
+						"key1": {"value1"},
1133
+					},
1134
+				},
1135
+				{
1136
+					Location: []*Location{cpuL[0]},
1137
+					Value:    []int64{1000},
1138
+					Label: map[string][]string{
1139
+						"key1": {"value1"},
1140
+					},
1141
+				},
1142
+			},
1143
+			setKey: "key1",
1144
+			setVal: []string{"value1"},
1145
+			wantLabels: []map[string][]string{
1146
+				{"key1": {"value1"}},
1147
+				{"key1": {"value1"}},
1148
+			},
1149
+		},
1150
+	}
1151
+
1152
+	for _, tc := range testcases {
1153
+		t.Run(tc.desc, func(t *testing.T) {
1154
+			profile := testProfile1.Copy()
1155
+			profile.Sample = tc.samples
1156
+			profile.SetLabel(tc.setKey, tc.setVal)
1157
+			if got, want := len(profile.Sample), len(tc.wantLabels); got != want {
1158
+				t.Fatalf("got %v samples, want %v samples", got, want)
1159
+			}
1160
+			for i, sample := range profile.Sample {
1161
+				wantLabels := tc.wantLabels[i]
1162
+				if got, want := len(sample.Label), len(wantLabels); got != want {
1163
+					t.Errorf("got %v label keys for sample %v, want %v", got, i, want)
1164
+					continue
1165
+				}
1166
+				for wantKey, wantValues := range wantLabels {
1167
+					if gotValues, ok := sample.Label[wantKey]; ok {
1168
+						if !reflect.DeepEqual(gotValues, wantValues) {
1169
+							t.Errorf("for key %s, got values %v, want values %v", wantKey, gotValues, wantValues)
1170
+						}
1171
+					} else {
1172
+						t.Errorf("for key %s got no values, want %v", wantKey, wantValues)
1173
+					}
1174
+				}
1175
+			}
1176
+		})
1177
+	}
1178
+}
1179
+
915
 func TestNumLabelUnits(t *testing.T) {
1180
 func TestNumLabelUnits(t *testing.T) {
916
 	var tagFilterTests = []struct {
1181
 	var tagFilterTests = []struct {
917
 		desc             string
1182
 		desc             string