Kaynağa Gözat

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

Margaret Nolan 6 yıl önce
ebeveyn
işleme
c283c2ce6a

+ 38
- 6
doc/README.md Dosyayı Görüntüle

@@ -216,6 +216,44 @@ search for them in a directory pointed to by the environment variable
216 216
 * **-weblist= _regex_:** Generates a source/assembly combined annotated listing for
217 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 257
 # Fetching profiles
220 258
 
221 259
 pprof can read profiles from a file or directly from a URL over http. Its native
@@ -237,11 +275,6 @@ them. This is useful to combine profiles from multiple processes of a
237 275
 distributed job. The profiles may be from different programs but must be
238 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 278
 ## Symbolization
246 279
 
247 280
 pprof can add symbol information to a profile that was collected only with
@@ -286,4 +319,3 @@ the symbolization handler.
286 319
 
287 320
 * **-symbolize=demangle=templates:** Demangle, and trim function parameters, but
288 321
   not template parameters.
289
-

+ 39
- 10
internal/driver/cli.go Dosyayı Görüntüle

@@ -15,6 +15,7 @@
15 15
 package driver
16 16
 
17 17
 import (
18
+	"errors"
18 19
 	"fmt"
19 20
 	"os"
20 21
 	"strings"
@@ -28,6 +29,7 @@ type source struct {
28 29
 	ExecName  string
29 30
 	BuildID   string
30 31
 	Base      []string
32
+	DiffBase  bool
31 33
 	Normalize bool
32 34
 
33 35
 	Seconds      int
@@ -43,7 +45,8 @@ type source struct {
43 45
 func parseFlags(o *plugin.Options) (*source, []string, error) {
44 46
 	flag := o.Flagset
45 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 50
 	// Source options.
48 51
 	flagSymbolize := flag.String("symbolize", "", "Options for profile symbolization")
49 52
 	flagBuildID := flag.String("buildid", "", "Override build id for first mapping")
@@ -85,7 +88,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
85 88
 			usageMsgVars)
86 89
 	})
87 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 94
 	var execName string
@@ -112,7 +115,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
112 115
 		return nil, nil, err
113 116
 	}
114 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 121
 	si := pprofVariables["sample_index"].value
@@ -140,15 +143,13 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
140 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 150
 	normalize := pprofVariables["normalize"].boolValue()
150 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 154
 	source.Normalize = normalize
154 155
 
@@ -158,6 +159,34 @@ func parseFlags(o *plugin.Options) (*source, []string, error) {
158 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 190
 // installFlags creates command line flags for pprof variables.
162 191
 func installFlags(flag plugin.FlagSet) flagsInstalled {
163 192
 	f := flagsInstalled{
@@ -240,7 +269,7 @@ func outputFormat(bcmd map[string]*bool, acmd map[string]*string) (cmd []string,
240 269
 	for n, b := range bcmd {
241 270
 		if *b {
242 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 274
 			cmd = []string{n}
246 275
 		}
@@ -248,7 +277,7 @@ func outputFormat(bcmd map[string]*bool, acmd map[string]*string) (cmd []string,
248 277
 	for n, s := range acmd {
249 278
 		if *s != "" {
250 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 282
 			cmd = []string{n, *s}
254 283
 		}

+ 7
- 2
internal/driver/driver_test.go Dosyayı Görüntüle

@@ -288,7 +288,7 @@ type testFlags struct {
288 288
 	floats      map[string]float64
289 289
 	strings     map[string]string
290 290
 	args        []string
291
-	stringLists map[string][]*string
291
+	stringLists map[string][]string
292 292
 }
293 293
 
294 294
 func (testFlags) ExtraUsage() string { return "" }
@@ -355,7 +355,12 @@ func (f testFlags) StringVar(p *string, s, d, c string) {
355 355
 
356 356
 func (f testFlags) StringList(s, d, c string) *[]*string {
357 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 365
 	return &[]*string{}
361 366
 }

+ 3
- 0
internal/driver/fetch.go Dosyayı Görüntüle

@@ -63,6 +63,9 @@ func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
63 63
 	}
64 64
 
65 65
 	if pbase != nil {
66
+		if s.DiffBase {
67
+			pbase.SetLabel("pprof::base", []string{"true"})
68
+		}
66 69
 		if s.Normalize {
67 70
 			err := p.Normalize(pbase)
68 71
 			if err != nil {

+ 206
- 34
internal/driver/fetch_test.go Dosyayı Görüntüle

@@ -210,13 +210,20 @@ func TestFetchWithBase(t *testing.T) {
210 210
 	baseVars := pprofVariables
211 211
 	defer func() { pprofVariables = baseVars }()
212 212
 
213
+	type WantSample struct {
214
+		values []int64
215
+		labels map[string][]string
216
+	}
217
+
213 218
 	const path = "testdata/"
214 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 229
 	testcases := []testcase{
@@ -224,58 +231,216 @@ func TestFetchWithBase(t *testing.T) {
224 231
 			"not normalized base is same as source",
225 232
 			[]string{path + "cppbench.contention"},
226 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 244
 			false,
228
-			[][]int64{},
245
+			nil,
246
+			"",
229 247
 		},
230 248
 		{
231 249
 			"not normalized single source, multiple base (all profiles same)",
232 250
 			[]string{path + "cppbench.contention"},
233 251
 			[]string{path + "cppbench.contention", path + "cppbench.contention"},
252
+			nil,
234 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 283
 			"not normalized, different base and source",
239 284
 			[]string{path + "cppbench.contention"},
240 285
 			[]string{path + "cppbench.small.contention"},
286
+			nil,
241 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 317
 			"normalized base is same as source",
246 318
 			[]string{path + "cppbench.contention"},
247 319
 			[]string{path + "cppbench.contention"},
320
+			nil,
248 321
 			true,
249
-			[][]int64{},
322
+			nil,
323
+			"",
250 324
 		},
251 325
 		{
252 326
 			"normalized single source, multiple base (all profiles same)",
253 327
 			[]string{path + "cppbench.contention"},
254 328
 			[]string{path + "cppbench.contention", path + "cppbench.contention"},
329
+			nil,
255 330
 			true,
256
-			[][]int64{},
331
+			nil,
332
+			"",
257 333
 		},
258 334
 		{
259 335
 			"normalized different base and source",
260 336
 			[]string{path + "cppbench.contention"},
261 337
 			[]string{path + "cppbench.small.contention"},
338
+			nil,
262 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 437
 	for _, tc := range testcases {
268 438
 		t.Run(tc.desc, func(t *testing.T) {
269 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 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 445
 				bools: map[string]bool{
281 446
 					"normalize": tc.normalize,
@@ -289,30 +454,37 @@ func TestFetchWithBase(t *testing.T) {
289 454
 			})
290 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 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 472
 			p, err := fetchProfiles(src, o)
297
-			pprofVariables = baseVars
473
+
298 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 Dosyayı Görüntüle

@@ -264,6 +264,10 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
264 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 271
 	formatTag := func(v int64, key string) string {
268 272
 		return measurement.ScaledLabel(v, key, o.OutputUnit)
269 273
 	}
@@ -1212,10 +1216,11 @@ func NewDefault(prof *profile.Profile, options Options) *Report {
1212 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 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 1224
 	for _, sample := range prof.Sample {
1220 1225
 		var d, v int64
1221 1226
 		v = value(sample.Value)
@@ -1225,13 +1230,21 @@ func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) i
1225 1230
 		if v < 0 {
1226 1231
 			v = -v
1227 1232
 		}
1228
-		ret += v
1233
+		total += v
1229 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 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 1250
 // Report contains the data and associated routines to extract a

+ 118
- 0
internal/report/report_test.go Dosyayı Görüntüle

@@ -287,3 +287,121 @@ func TestLegendActiveFilters(t *testing.T) {
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 Dosyayı Görüntüle

@@ -674,6 +674,36 @@ func numLabelsToString(numLabels map[string][]int64, numUnits map[string][]strin
674 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 707
 // Scale multiplies all sample values in a profile by a constant.
678 708
 func (p *Profile) Scale(ratio float64) {
679 709
 	if ratio == 1 {

+ 266
- 1
profile/profile_test.go Dosyayı Görüntüle

@@ -741,7 +741,7 @@ func TestNumLabelMerge(t *testing.T) {
741 741
 		wantNumUnits  []map[string][]string
742 742
 	}{
743 743
 		{
744
-			name:  "different tag units not merged",
744
+			name:  "different label units not merged",
745 745
 			profs: []*Profile{testProfile4.Copy(), testProfile5.Copy()},
746 746
 			wantNumLabels: []map[string][]int64{
747 747
 				{
@@ -912,6 +912,271 @@ func locationHash(s *Sample) string {
912 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 1180
 func TestNumLabelUnits(t *testing.T) {
916 1181
 	var tagFilterTests = []struct {
917 1182
 		desc             string