Procházet zdrojové kódy

Add support for units for numeric tags (#213)

* Add support for tag units

Updated tests for adding tag units

* updated proto docs to describe tag units

* fixed formatting

* added additional test

* require specific units for graph tag to be recognixed

* updated tests and formatting

* Fixed formatting

* Fixed style error

* modify proto_test to be sure tag units not overwritten

* Clarified label docs

* call legacy profile memory allocations size, not byte

* updated tests to call bytes tags in legacy profile size

* Revert "updated tests to call bytes tags in legacy profile size"

This reverts commit 9289378af2.

* Revert "call legacy profile memory allocations size, not byte"

This reverts commit 6b18973562.

* Updated so units not modified when profile written out

* Fixed formatting errors

* Removed field for inferred numeric label units from profile

* Modified profile String() output

* fixed formatting error

* changed name of function used to identify units for numeric labels

* Refactor String() for profile

* Modified for clarity and address comments

* renamed function for clarity

* Made numeric label units field seperate in profile

* Modified test

* Refactored identifyNumLabelUnits()

* Updated comments

* Fixed formatting error

* Addressed comments

* Fixed style error

* clarify comment

* Refactored to address comments

* addressed comments

* addressed comments

* addressed comments
Margaret Nolan před 7 roky
rodič
revize
c70cbc2c12

+ 5
- 3
doc/developer/profile.proto.md Zobrazit soubor

@@ -128,9 +128,11 @@ size of 6MB.
128 128
 
129 129
 Labels can be string-based or numeric. They are represented by the Label
130 130
 message, with a key identifying the label and either a string or numeric
131
-value. For numeric labels, by convention the key represents the measurement unit
132
-of the numeric value. So for the previous example, the samples would have labels
133
-{“bytes”, 2097152} and {“bytes”, 4194304}.
131
+value. For numeric labels, the measurement unit can be specified in the profile.
132
+If no unit is specified and the key is "request" or "alignment",
133
+then the units are assumed to be "bytes". Otherwise when no unit is specified
134
+the key will be used as the measurement unit of the numeric value. All tags with
135
+the same key should have the same unit.
134 136
 
135 137
 ## Keep and drop expressions
136 138
 

+ 22
- 4
internal/driver/driver.go Zobrazit soubor

@@ -23,6 +23,7 @@ import (
23 23
 	"os"
24 24
 	"path/filepath"
25 25
 	"regexp"
26
+	"strings"
26 27
 
27 28
 	"github.com/google/pprof/internal/plugin"
28 29
 	"github.com/google/pprof/internal/report"
@@ -61,16 +62,19 @@ func PProf(eo *plugin.Options) error {
61 62
 func generateRawReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) (*command, *report.Report, error) {
62 63
 	p = p.Copy() // Prevent modification to the incoming profile.
63 64
 
65
+	// Identify units of numeric tags in profile.
66
+	numLabelUnits := identifyNumLabelUnits(p, o.UI)
67
+
64 68
 	vars = applyCommandOverrides(cmd, vars)
65 69
 
66 70
 	// Delay focus after configuring report to get percentages on all samples.
67 71
 	relative := vars["relative_percentages"].boolValue()
68 72
 	if relative {
69
-		if err := applyFocus(p, vars, o.UI); err != nil {
73
+		if err := applyFocus(p, numLabelUnits, vars, o.UI); err != nil {
70 74
 			return nil, nil, err
71 75
 		}
72 76
 	}
73
-	ropt, err := reportOptions(p, vars)
77
+	ropt, err := reportOptions(p, numLabelUnits, vars)
74 78
 	if err != nil {
75 79
 		return nil, nil, err
76 80
 	}
@@ -89,7 +93,7 @@ func generateRawReport(p *profile.Profile, cmd []string, vars variables, o *plug
89 93
 
90 94
 	rpt := report.New(p, ropt)
91 95
 	if !relative {
92
-		if err := applyFocus(p, vars, o.UI); err != nil {
96
+		if err := applyFocus(p, numLabelUnits, vars, o.UI); err != nil {
93 97
 			return nil, nil, err
94 98
 		}
95 99
 	}
@@ -221,7 +225,7 @@ func aggregate(prof *profile.Profile, v variables) error {
221 225
 	return prof.Aggregate(inlines, function, filename, linenumber, address)
222 226
 }
223 227
 
224
-func reportOptions(p *profile.Profile, vars variables) (*report.Options, error) {
228
+func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars variables) (*report.Options, error) {
225 229
 	si, mean := vars["sample_index"].value, vars["mean"].boolValue()
226 230
 	value, meanDiv, sample, err := sampleFormat(p, si, mean)
227 231
 	if err != nil {
@@ -259,6 +263,7 @@ func reportOptions(p *profile.Profile, vars variables) (*report.Options, error)
259 263
 		EdgeFraction: vars["edgefraction"].floatValue(),
260 264
 
261 265
 		ActiveFilters: filters,
266
+		NumLabelUnits: numLabelUnits,
262 267
 
263 268
 		SampleValue:       value,
264 269
 		SampleMeanDivisor: meanDiv,
@@ -277,6 +282,19 @@ func reportOptions(p *profile.Profile, vars variables) (*report.Options, error)
277 282
 	return ropt, nil
278 283
 }
279 284
 
285
+// identifyNumLabelUnits returns a map of numeric label keys to the units
286
+// associated with those keys.
287
+func identifyNumLabelUnits(p *profile.Profile, ui plugin.UI) map[string]string {
288
+	numLabelUnits, ignoredUnits := p.NumLabelUnits()
289
+
290
+	// Print errors for tags with multiple units associated with
291
+	// a single key.
292
+	for k, units := range ignoredUnits {
293
+		ui.PrintErr(fmt.Sprintf("For tag %s used unit %s, also encountered unit(s) %s", k, numLabelUnits[k], strings.Join(units, ",")))
294
+	}
295
+	return numLabelUnits
296
+}
297
+
280 298
 type sampleValueFunc func([]int64) int64
281 299
 
282 300
 // sampleFormat returns a function to extract values out of a profile.Sample,

+ 11
- 8
internal/driver/driver_focus.go Zobrazit soubor

@@ -28,13 +28,13 @@ 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, v variables, ui plugin.UI) error {
31
+func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variables, ui plugin.UI) error {
32 32
 	focus, err := compileRegexOption("focus", v["focus"].value, nil)
33 33
 	ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
34 34
 	hide, err := compileRegexOption("hide", v["hide"].value, err)
35 35
 	show, err := compileRegexOption("show", v["show"].value, err)
36
-	tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, ui, err)
37
-	tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, ui, err)
36
+	tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
37
+	tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
38 38
 	prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, err)
39 39
 	if err != nil {
40 40
 		return err
@@ -73,7 +73,7 @@ func compileRegexOption(name, value string, err error) (*regexp.Regexp, error) {
73 73
 	return rx, nil
74 74
 }
75 75
 
76
-func compileTagFilter(name, value string, ui plugin.UI, err error) (func(*profile.Sample) bool, error) {
76
+func compileTagFilter(name, value string, numLabelUnits map[string]string, ui plugin.UI, err error) (func(*profile.Sample) bool, error) {
77 77
 	if value == "" || err != nil {
78 78
 		return nil, err
79 79
 	}
@@ -87,18 +87,21 @@ func compileTagFilter(name, value string, ui plugin.UI, err error) (func(*profil
87 87
 
88 88
 	if numFilter := parseTagFilterRange(value); numFilter != nil {
89 89
 		ui.PrintErr(name, ":Interpreted '", value, "' as range, not regexp")
90
-		labelFilter := func(vals []int64, key string) bool {
90
+		labelFilter := func(vals []int64, unit string) bool {
91 91
 			for _, val := range vals {
92
-				if numFilter(val, key) {
92
+				if numFilter(val, unit) {
93 93
 					return true
94 94
 				}
95 95
 			}
96 96
 			return false
97 97
 		}
98
+		numLabelUnit := func(key string) string {
99
+			return numLabelUnits[key]
100
+		}
98 101
 		if wantKey == "" {
99 102
 			return func(s *profile.Sample) bool {
100 103
 				for key, vals := range s.NumLabel {
101
-					if labelFilter(vals, key) {
104
+					if labelFilter(vals, numLabelUnit(key)) {
102 105
 						return true
103 106
 					}
104 107
 				}
@@ -107,7 +110,7 @@ func compileTagFilter(name, value string, ui plugin.UI, err error) (func(*profil
107 110
 		}
108 111
 		return func(s *profile.Sample) bool {
109 112
 			if vals, ok := s.NumLabel[wantKey]; ok {
110
-				return labelFilter(vals, wantKey)
113
+				return labelFilter(vals, numLabelUnit(wantKey))
111 114
 			}
112 115
 			return false
113 116
 		}, nil

+ 312
- 69
internal/driver/driver_test.go Zobrazit soubor

@@ -89,6 +89,7 @@ func TestParse(t *testing.T) {
89 89
 		{"disasm=line[13],addresses,flat", "cpu"},
90 90
 		{"peek=line.*01", "cpu"},
91 91
 		{"weblist=line[13],addresses,flat", "cpu"},
92
+		{"tags,tagfocus=400kb:", "heap_request"},
92 93
 	}
93 94
 
94 95
 	baseVars := pprofVariables
@@ -395,11 +396,17 @@ func (testFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string
395 396
 			{Type: "alloc_objects", Unit: "count"},
396 397
 			{Type: "alloc_space", Unit: "bytes"},
397 398
 		}
399
+	case "heap_request":
400
+		p = heapProfile()
401
+		for _, s := range p.Sample {
402
+			s.NumLabel["request"] = s.NumLabel["bytes"]
403
+		}
398 404
 	case "heap_sizetags":
399 405
 		p = heapProfile()
400 406
 		tags := []int64{2, 4, 8, 16, 32, 64, 128, 256}
401 407
 		for _, s := range p.Sample {
402
-			s.NumLabel["bytes"] = append(s.NumLabel["bytes"], tags...)
408
+			numValues := append(s.NumLabel["bytes"], tags...)
409
+			s.NumLabel["bytes"] = numValues
403 410
 		}
404 411
 	case "heap_tags":
405 412
 		p = heapProfile()
@@ -783,30 +790,22 @@ func heapProfile() *profile.Profile {
783 790
 			{
784 791
 				Location: []*profile.Location{heapL[0], heapL[1], heapL[2]},
785 792
 				Value:    []int64{10, 1024000},
786
-				NumLabel: map[string][]int64{
787
-					"bytes": {102400},
788
-				},
793
+				NumLabel: map[string][]int64{"bytes": {102400}},
789 794
 			},
790 795
 			{
791 796
 				Location: []*profile.Location{heapL[0], heapL[3]},
792 797
 				Value:    []int64{20, 4096000},
793
-				NumLabel: map[string][]int64{
794
-					"bytes": {204800},
795
-				},
798
+				NumLabel: map[string][]int64{"bytes": {204800}},
796 799
 			},
797 800
 			{
798 801
 				Location: []*profile.Location{heapL[1], heapL[4]},
799 802
 				Value:    []int64{40, 65536000},
800
-				NumLabel: map[string][]int64{
801
-					"bytes": {1638400},
802
-				},
803
+				NumLabel: map[string][]int64{"bytes": {1638400}},
803 804
 			},
804 805
 			{
805 806
 				Location: []*profile.Location{heapL[2]},
806 807
 				Value:    []int64{80, 32768000},
807
-				NumLabel: map[string][]int64{
808
-					"bytes": {409600},
809
-				},
808
+				NumLabel: map[string][]int64{"bytes": {409600}},
810 809
 			},
811 810
 		},
812 811
 		DropFrames: ".*operator new.*|malloc",
@@ -989,76 +988,320 @@ func TestAutoComplete(t *testing.T) {
989 988
 
990 989
 func TestTagFilter(t *testing.T) {
991 990
 	var tagFilterTests = []struct {
992
-		name, value string
991
+		desc, value string
993 992
 		tags        map[string][]string
994 993
 		want        bool
995 994
 	}{
996
-		{"test1", "tag2", map[string][]string{"value1": {"tag1", "tag2"}}, true},
997
-		{"test2", "tag3", map[string][]string{"value1": {"tag1", "tag2"}}, false},
998
-		{"test3", "tag1,tag3", map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}}, true},
999
-		{"test4", "t..[12],t..3", map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}}, true},
1000
-		{"test5", "tag2,tag3", map[string][]string{"value1": {"tag1", "tag2"}}, false},
1001
-		{"test6", "key1=tag1,tag2", map[string][]string{"key1": {"tag1", "tag2"}}, true},
1002
-		{"test7", "key1=tag1,tag2", map[string][]string{"key1": {"tag1"}}, true},
1003
-		{"test8", "key1:tag1,tag2", map[string][]string{"key1": {"tag1", "tag2"}}, true},
1004
-		{"test9", "key1:tag1,tag2", map[string][]string{"key1": {"tag2"}}, false},
1005
-		{"test10", "key1:tag1,tag2", map[string][]string{"key1": {"tag1"}}, false},
995
+		{
996
+			"1 key with 1 matching value",
997
+			"tag2",
998
+			map[string][]string{"value1": {"tag1", "tag2"}},
999
+			true,
1000
+		},
1001
+		{
1002
+			"1 key with no matching values",
1003
+			"tag3",
1004
+			map[string][]string{"value1": {"tag1", "tag2"}},
1005
+			false,
1006
+		},
1007
+		{
1008
+			"two keys, each with value matching different one value in list",
1009
+			"tag1,tag3",
1010
+			map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}},
1011
+			true,
1012
+		},
1013
+		{"two keys, all value matching different regex value in list",
1014
+			"t..[12],t..3",
1015
+			map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}},
1016
+			true,
1017
+		},
1018
+		{
1019
+			"one key, not all values in list matched",
1020
+			"tag2,tag3",
1021
+			map[string][]string{"value1": {"tag1", "tag2"}},
1022
+			false,
1023
+		},
1024
+		{
1025
+			"key specified, list of tags where all tags in list matched",
1026
+			"key1=tag1,tag2",
1027
+			map[string][]string{"key1": {"tag1", "tag2"}},
1028
+			true,
1029
+		},
1030
+		{"key specified, list of tag values where not all are matched",
1031
+			"key1=tag1,tag2",
1032
+			map[string][]string{"key1": {"tag1"}},
1033
+			true,
1034
+		},
1035
+		{
1036
+			"key included for regex matching, list of values where all values in list matched",
1037
+			"key1:tag1,tag2",
1038
+			map[string][]string{"key1": {"tag1", "tag2"}},
1039
+			true,
1040
+		},
1041
+		{
1042
+			"key included for regex matching, list of values where not only second value matched",
1043
+			"key1:tag1,tag2",
1044
+			map[string][]string{"key1": {"tag2"}},
1045
+			false,
1046
+		},
1047
+		{
1048
+			"key included for regex matching, list of values where not only first value matched",
1049
+			"key1:tag1,tag2",
1050
+			map[string][]string{"key1": {"tag1"}},
1051
+			false,
1052
+		},
1006 1053
 	}
1007 1054
 	for _, test := range tagFilterTests {
1008
-		filter, err := compileTagFilter(test.name, test.value, &proftest.TestUI{T: t}, nil)
1009
-		if err != nil {
1010
-			t.Errorf("tagFilter %s:%v", test.name, err)
1011
-			continue
1012
-		}
1013
-		s := profile.Sample{
1014
-			Label: test.tags,
1015
-		}
1055
+		t.Run(test.desc, func(*testing.T) {
1056
+			filter, err := compileTagFilter(test.desc, test.value, nil, &proftest.TestUI{T: t}, nil)
1057
+			if err != nil {
1058
+				t.Fatalf("tagFilter %s:%v", test.desc, err)
1059
+			}
1060
+			s := profile.Sample{
1061
+				Label: test.tags,
1062
+			}
1063
+			if got := filter(&s); got != test.want {
1064
+				t.Errorf("tagFilter %s: got %v, want %v", test.desc, got, test.want)
1065
+			}
1066
+		})
1067
+	}
1068
+}
1016 1069
 
1017
-		if got := filter(&s); got != test.want {
1018
-			t.Errorf("tagFilter %s: got %v, want %v", test.name, got, test.want)
1019
-		}
1070
+func TestIdentifyNumLabelUnits(t *testing.T) {
1071
+	var tagFilterTests = []struct {
1072
+		desc               string
1073
+		tagVals            []map[string][]int64
1074
+		tagUnits           []map[string][]string
1075
+		wantUnits          map[string]string
1076
+		allowedRx          string
1077
+		wantIgnoreErrCount int
1078
+	}{
1079
+		{
1080
+			"Multiple keys, different units",
1081
+			[]map[string][]int64{{"key1": {131072}, "key2": {128}}},
1082
+			[]map[string][]string{{"key1": {"bytes"}, "key2": {"kilobytes"}}},
1083
+			map[string]string{"key1": "bytes", "key2": "kilobytes"},
1084
+			"",
1085
+			0,
1086
+		},
1087
+		{
1088
+			"One key with different units in same sample",
1089
+			[]map[string][]int64{{"key1": {8, 8}}},
1090
+			[]map[string][]string{{"key1": {"bytes", "kilobytes"}}},
1091
+			map[string]string{"key1": "bytes"},
1092
+			`(For tag key1 used unit bytes, also encountered unit\(s\) kilobytes)`,
1093
+			1,
1094
+		},
1095
+		{
1096
+			"One key with different units in different samples",
1097
+			[]map[string][]int64{{"key1": {8}}, {"key1": {8}}},
1098
+			[]map[string][]string{{"key1": {"bytes"}}, {"key1": {"kilobytes"}}},
1099
+			map[string]string{"key1": "bytes"},
1100
+			`(For tag key1 used unit bytes, also encountered unit\(s\) kilobytes)`,
1101
+			1,
1102
+		},
1103
+		{
1104
+			"Check units not over-written for keys with default units",
1105
+			[]map[string][]int64{{
1106
+				"alignment": {8},
1107
+				"request":   {8},
1108
+				"bytes":     {8},
1109
+			}},
1110
+			[]map[string][]string{{
1111
+				"alignment": {"seconds"},
1112
+				"request":   {"minutes"},
1113
+				"bytes":     {"hours"},
1114
+			}},
1115
+			map[string]string{
1116
+				"alignment": "seconds",
1117
+				"request":   "minutes",
1118
+				"bytes":     "hours",
1119
+			},
1120
+			"",
1121
+			0,
1122
+		},
1123
+	}
1124
+	for _, test := range tagFilterTests {
1125
+		t.Run(test.desc, func(*testing.T) {
1126
+			p := profile.Profile{Sample: make([]*profile.Sample, len(test.tagVals))}
1127
+			for i, numLabel := range test.tagVals {
1128
+				s := profile.Sample{
1129
+					NumLabel: numLabel,
1130
+					NumUnit:  test.tagUnits[i],
1131
+				}
1132
+				p.Sample[i] = &s
1133
+			}
1134
+			testUI := &proftest.TestUI{T: t, AllowRx: test.allowedRx}
1135
+			units := identifyNumLabelUnits(&p, testUI)
1136
+			for key, wantUnit := range test.wantUnits {
1137
+				unit := units[key]
1138
+				if wantUnit != unit {
1139
+					t.Errorf("for key %s, got unit %s, want unit %s", key, unit, wantUnit)
1140
+				}
1141
+			}
1142
+			if got, want := testUI.NumAllowRxMatches, test.wantIgnoreErrCount; want != got {
1143
+				t.Errorf("got %d errors logged, want %d errors logged", got, want)
1144
+			}
1145
+		})
1020 1146
 	}
1021 1147
 }
1022 1148
 
1023 1149
 func TestNumericTagFilter(t *testing.T) {
1024 1150
 	var tagFilterTests = []struct {
1025
-		name, value string
1026
-		tags        map[string][]int64
1027
-		want        bool
1151
+		desc, value     string
1152
+		tags            map[string][]int64
1153
+		identifiedUnits map[string]string
1154
+		want            bool
1028 1155
 	}{
1029
-		{"test1", "128kb", map[string][]int64{"bytes": {131072}, "kilobytes": {128}}, true},
1030
-		{"test2", "512kb", map[string][]int64{"bytes": {512}, "kilobytes": {128}}, false},
1031
-		{"test3", "10b", map[string][]int64{"bytes": {10, 20}, "kilobytes": {128}}, true},
1032
-		{"test4", ":10b", map[string][]int64{"bytes": {8}}, true},
1033
-		{"test5", ":10kb", map[string][]int64{"bytes": {8}}, true},
1034
-		{"test6", "10b:", map[string][]int64{"kilobytes": {8}}, true},
1035
-		{"test7", "10b:", map[string][]int64{"bytes": {12}}, true},
1036
-		{"test8", "10b:", map[string][]int64{"bytes": {8}}, false},
1037
-		{"test9", "10kb:", map[string][]int64{"bytes": {8}}, false},
1038
-		{"test10", ":10b", map[string][]int64{"kilobytes": {8}}, false},
1039
-		{"test11", ":10b", map[string][]int64{"bytes": {12}}, false},
1040
-		{"test12", "bytes=5b", map[string][]int64{"bytes": {5, 10}}, true},
1041
-		{"test13", "bytes=1024b", map[string][]int64{"kilobytes": {1, 1024}}, false},
1042
-		{"test14", "bytes=1024b", map[string][]int64{"kilobytes": {5}, "bytes": {1024}}, true},
1043
-		{"test15", "bytes=512b:1024b", map[string][]int64{"bytes": {780}}, true},
1044
-		{"test16", "bytes=1kb:2kb", map[string][]int64{"bytes": {4096}}, false},
1045
-		{"test17", "bytes=512b:1024b", map[string][]int64{"bytes": {256}}, false},
1156
+		{
1157
+			"Match when unit conversion required",
1158
+			"128kb",
1159
+			map[string][]int64{"key1": {131072}, "key2": {128}},
1160
+			map[string]string{"key1": "bytes", "key2": "kilobytes"},
1161
+			true,
1162
+		},
1163
+		{
1164
+			"Match only when values equal after unit conversion",
1165
+			"512kb",
1166
+			map[string][]int64{"key1": {512}, "key2": {128}},
1167
+			map[string]string{"key1": "bytes", "key2": "kilobytes"},
1168
+			false,
1169
+		},
1170
+		{
1171
+			"Match when values and units initially equal",
1172
+			"10bytes",
1173
+			map[string][]int64{"key1": {10}, "key2": {128}},
1174
+			map[string]string{"key1": "bytes", "key2": "kilobytes"},
1175
+			true,
1176
+		},
1177
+		{
1178
+			"Match range without lower bound, no unit conversion required",
1179
+			":10bytes",
1180
+			map[string][]int64{"key1": {8}},
1181
+			map[string]string{"key1": "bytes"},
1182
+			true,
1183
+		},
1184
+		{
1185
+			"Match range without lower bound, unit conversion required",
1186
+			":10kb",
1187
+			map[string][]int64{"key1": {8}},
1188
+			map[string]string{"key1": "bytes"},
1189
+			true,
1190
+		},
1191
+		{
1192
+			"Match range without upper bound, unit conversion required",
1193
+			"10b:",
1194
+			map[string][]int64{"key1": {8}},
1195
+			map[string]string{"key1": "kilobytes"},
1196
+			true,
1197
+		},
1198
+		{
1199
+			"Match range without upper bound, no unit conversion required",
1200
+			"10b:",
1201
+			map[string][]int64{"key1": {12}},
1202
+			map[string]string{"key1": "bytes"},
1203
+			true,
1204
+		},
1205
+		{
1206
+			"Don't match range without upper bound, no unit conversion required",
1207
+			"10b:",
1208
+			map[string][]int64{"key1": {8}},
1209
+			map[string]string{"key1": "bytes"},
1210
+			false,
1211
+		},
1212
+		{
1213
+			"Multiple keys with different units, don't match range without upper bound",
1214
+			"10kb:",
1215
+			map[string][]int64{"key1": {8}},
1216
+			map[string]string{"key1": "bytes", "key2": "kilobytes"},
1217
+			false,
1218
+		},
1219
+		{
1220
+			"Match range without upper bound, unit conversion required",
1221
+			"10b:",
1222
+			map[string][]int64{"key1": {8}},
1223
+			map[string]string{"key1": "kilobytes"},
1224
+			true,
1225
+		},
1226
+		{
1227
+			"Don't match range without lower bound, no unit conversion required",
1228
+			":10b",
1229
+			map[string][]int64{"key1": {12}},
1230
+			map[string]string{"key1": "bytes"},
1231
+			false,
1232
+		},
1233
+		{
1234
+			"Match specific key, key present, one of two values match",
1235
+			"bytes=5b",
1236
+			map[string][]int64{"bytes": {10, 5}},
1237
+			map[string]string{"bytes": "bytes"},
1238
+			true,
1239
+		},
1240
+		{
1241
+			"Match specific key, key present and value matches",
1242
+			"bytes=1024b",
1243
+			map[string][]int64{"bytes": {1024}},
1244
+			map[string]string{"bytes": "kilobytes"},
1245
+			false,
1246
+		},
1247
+		{
1248
+			"Match specific key, matching key present and value matches, also non-matching key",
1249
+			"bytes=1024b",
1250
+			map[string][]int64{"bytes": {1024}, "key2": {5}},
1251
+			map[string]string{"bytes": "bytes", "key2": "bytes"},
1252
+			true,
1253
+		},
1254
+		{
1255
+			"Match specific key and range of values, value matches",
1256
+			"bytes=512b:1024b",
1257
+			map[string][]int64{"bytes": {780}},
1258
+			map[string]string{"bytes": "bytes"},
1259
+			true,
1260
+		},
1261
+		{
1262
+			"Match specific key and range of values, value too large",
1263
+			"key1=1kb:2kb",
1264
+			map[string][]int64{"key1": {4096}},
1265
+			map[string]string{"key1": "bytes"},
1266
+			false,
1267
+		},
1268
+		{
1269
+			"Match specific key and range of values, value too small",
1270
+			"key1=1kb:2kb",
1271
+			map[string][]int64{"key1": {256}},
1272
+			map[string]string{"key1": "bytes"},
1273
+			false,
1274
+		},
1275
+		{
1276
+			"Match specific key and value, unit conversion required",
1277
+			"bytes=1024b",
1278
+			map[string][]int64{"bytes": {1}},
1279
+			map[string]string{"bytes": "kilobytes"},
1280
+			true,
1281
+		},
1282
+		{
1283
+			"Match specific key and value, key does not appear",
1284
+			"key2=256bytes",
1285
+			map[string][]int64{"key1": {256}},
1286
+			map[string]string{"key1": "bytes"},
1287
+			false,
1288
+		},
1046 1289
 	}
1047 1290
 	for _, test := range tagFilterTests {
1048
-		expectedErrMsg := fmt.Sprint([]string{test.name, ":Interpreted '", test.value, "' as range, not regexp"})
1049
-		filter, err := compileTagFilter(test.name, test.value, &proftest.TestUI{T: t,
1050
-			IgnoreRx: expectedErrMsg}, nil)
1051
-		if err != nil {
1052
-			t.Errorf("tagFilter %s:%v", test.name, err)
1053
-			continue
1054
-		}
1055
-		s := profile.Sample{
1056
-			NumLabel: test.tags,
1057
-		}
1058
-
1059
-		if got := filter(&s); got != test.want {
1060
-			t.Errorf("tagFilter %s: got %v, want %v", test.name, got, test.want)
1061
-		}
1291
+		t.Run(test.desc, func(*testing.T) {
1292
+			wantErrMsg := strings.Join([]string{"(", test.desc, ":Interpreted '", test.value[strings.Index(test.value, "=")+1:], "' as range, not regexp", ")"}, "")
1293
+			filter, err := compileTagFilter(test.desc, test.value, test.identifiedUnits, &proftest.TestUI{T: t,
1294
+				AllowRx: wantErrMsg}, nil)
1295
+			if err != nil {
1296
+				t.Fatalf("%v", err)
1297
+			}
1298
+			s := profile.Sample{
1299
+				NumLabel: test.tags,
1300
+			}
1301
+			if got := filter(&s); got != test.want {
1302
+				t.Fatalf("got %v, want %v", got, test.want)
1303
+			}
1304
+		})
1062 1305
 	}
1063 1306
 }
1064 1307
 

+ 1
- 1
internal/driver/fetch_test.go Zobrazit soubor

@@ -406,7 +406,7 @@ func TestHttpsInsecure(t *testing.T) {
406 406
 	}
407 407
 	o := &plugin.Options{
408 408
 		Obj: &binutils.Binutils{},
409
-		UI:  &proftest.TestUI{T: t, IgnoreRx: "Saved profile in"},
409
+		UI:  &proftest.TestUI{T: t, AllowRx: "Saved profile in"},
410 410
 	}
411 411
 	o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI}
412 412
 	p, err := fetchProfiles(s, o)

+ 2
- 1
internal/driver/interactive.go Zobrazit soubor

@@ -123,7 +123,8 @@ var generateReportWrapper = generateReport // For testing purposes.
123 123
 // greetings prints a brief welcome and some overall profile
124 124
 // information before accepting interactive commands.
125 125
 func greetings(p *profile.Profile, ui plugin.UI) {
126
-	ropt, err := reportOptions(p, pprofVariables)
126
+	numLabelUnits := identifyNumLabelUnits(p, ui)
127
+	ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
127 128
 	if err == nil {
128 129
 		ui.Print(strings.Join(report.ProfileLabels(report.New(p, ropt)), "\n"))
129 130
 	}

+ 8
- 0
internal/driver/testdata/pprof.heap_request.tags.focus Zobrazit soubor

@@ -0,0 +1,8 @@
1
+ bytes: Total 93.8MB
2
+        62.5MB (66.67%): 1.56MB
3
+        31.2MB (33.33%): 400kB
4
+
5
+ request: Total 93.8MB
6
+          62.5MB (66.67%): 1.56MB
7
+          31.2MB (33.33%): 400kB
8
+

+ 6
- 6
internal/driver/testdata/pprof.heap_tags.traces Zobrazit soubor

@@ -3,8 +3,8 @@ comment
3 3
 Type: inuse_space
4 4
 -----------+-------------------------------------------------------
5 5
       key1:  tag
6
-     bytes:  102400
7
-   request:  102400
6
+     bytes:  100kB
7
+   request:  100kB
8 8
     1000kB   line1000
9 9
              line2001
10 10
              line2000
@@ -12,20 +12,20 @@ Type: inuse_space
12 12
              line3001
13 13
              line3000
14 14
 -----------+-------------------------------------------------------
15
-     bytes:  204800
15
+     bytes:  200kB
16 16
     3.91MB   line1000
17 17
              line3001
18 18
              line3000
19 19
 -----------+-------------------------------------------------------
20 20
       key1:  tag
21
-     bytes:  1638400
22
-   request:  1638400
21
+     bytes:  1.56MB
22
+   request:  1.56MB
23 23
    62.50MB   line2001
24 24
              line2000
25 25
              line3002
26 26
              line3000
27 27
 -----------+-------------------------------------------------------
28
-     bytes:  409600
28
+     bytes:  400kB
29 29
    31.25MB   line3002
30 30
              line3001
31 31
              line3000

+ 14
- 8
internal/graph/graph.go Zobrazit soubor

@@ -329,7 +329,7 @@ func newGraph(prof *profile.Profile, o *Options) (*Graph, map[uint64]Nodes) {
329 329
 				// Add cum weight to all nodes in stack, avoiding double counting.
330 330
 				if _, ok := seenNode[n]; !ok {
331 331
 					seenNode[n] = true
332
-					n.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, false)
332
+					n.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, false)
333 333
 				}
334 334
 				// Update edge weights for all edges in stack, avoiding double counting.
335 335
 				if _, ok := seenEdge[nodePair{n, parent}]; !ok && parent != nil && n != parent {
@@ -342,7 +342,7 @@ func newGraph(prof *profile.Profile, o *Options) (*Graph, map[uint64]Nodes) {
342 342
 		}
343 343
 		if parent != nil && !residual {
344 344
 			// Add flat weight to leaf node.
345
-			parent.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, true)
345
+			parent.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, true)
346 346
 		}
347 347
 	}
348 348
 
@@ -401,7 +401,7 @@ func newTree(prof *profile.Profile, o *Options) (g *Graph) {
401 401
 				if n == nil {
402 402
 					continue
403 403
 				}
404
-				n.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, false)
404
+				n.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, false)
405 405
 				if parent != nil {
406 406
 					parent.AddToEdgeDiv(n, dw, w, false, lidx != len(lines)-1)
407 407
 				}
@@ -409,7 +409,7 @@ func newTree(prof *profile.Profile, o *Options) (g *Graph) {
409 409
 			}
410 410
 		}
411 411
 		if parent != nil {
412
-			parent.addSample(dw, w, labels, sample.NumLabel, o.FormatTag, true)
412
+			parent.addSample(dw, w, labels, sample.NumLabel, sample.NumUnit, o.FormatTag, true)
413 413
 		}
414 414
 	}
415 415
 
@@ -602,7 +602,7 @@ func (ns Nodes) Sum() (flat int64, cum int64) {
602 602
 	return
603 603
 }
604 604
 
605
-func (n *Node) addSample(dw, w int64, labels string, numLabel map[string][]int64, format func(int64, string) string, flat bool) {
605
+func (n *Node) addSample(dw, w int64, labels string, numLabel map[string][]int64, numUnit map[string][]string, format func(int64, string) string, flat bool) {
606 606
 	// Update sample value
607 607
 	if flat {
608 608
 		n.FlatDiv += dw
@@ -633,9 +633,15 @@ func (n *Node) addSample(dw, w int64, labels string, numLabel map[string][]int64
633 633
 	if format == nil {
634 634
 		format = defaultLabelFormat
635 635
 	}
636
-	for key, nvals := range numLabel {
637
-		for _, v := range nvals {
638
-			t := numericTags.findOrAddTag(format(v, key), key, v)
636
+	for k, nvals := range numLabel {
637
+		units := numUnit[k]
638
+		for i, v := range nvals {
639
+			var t *Tag
640
+			if len(units) > 0 {
641
+				t = numericTags.findOrAddTag(format(v, units[i]), units[i], v)
642
+			} else {
643
+				t = numericTags.findOrAddTag(format(v, k), k, v)
644
+			}
639 645
 			if flat {
640 646
 				t.FlatDiv += dw
641 647
 				t.Flat += w

+ 11
- 7
internal/proftest/proftest.go Zobrazit soubor

@@ -72,11 +72,14 @@ func EncodeJSON(x interface{}) []byte {
72 72
 }
73 73
 
74 74
 // TestUI implements the plugin.UI interface, triggering test failures
75
-// if more than Ignore errors not matching IgnoreRx are printed.
75
+// if more than Ignore errors not matching AllowRx are printed.
76
+// Also tracks the number of times the error matches AllowRx in
77
+// NumAllowRxMatches.
76 78
 type TestUI struct {
77
-	T        *testing.T
78
-	Ignore   int
79
-	IgnoreRx string
79
+	T                 *testing.T
80
+	Ignore            int
81
+	AllowRx           string
82
+	NumAllowRxMatches int
80 83
 }
81 84
 
82 85
 // ReadLine returns no input, as no input is expected during testing.
@@ -91,11 +94,12 @@ func (ui *TestUI) Print(args ...interface{}) {
91 94
 // PrintErr messages may trigger an error failure. A fixed number of
92 95
 // error messages are permitted when appropriate.
93 96
 func (ui *TestUI) PrintErr(args ...interface{}) {
94
-	if ui.IgnoreRx != "" {
95
-		if matched, err := regexp.MatchString(ui.IgnoreRx, fmt.Sprint(args)); matched || err != nil {
97
+	if ui.AllowRx != "" {
98
+		if matched, err := regexp.MatchString(ui.AllowRx, fmt.Sprint(args...)); matched || err != nil {
96 99
 			if err != nil {
97
-				ui.T.Errorf("failed to match against regex %q: %v", ui.IgnoreRx, err)
100
+				ui.T.Errorf("failed to match against regex %q: %v", ui.AllowRx, err)
98 101
 			}
102
+			ui.NumAllowRxMatches++
99 103
 			return
100 104
 		}
101 105
 	}

+ 25
- 6
internal/report/report.go Zobrazit soubor

@@ -65,6 +65,7 @@ type Options struct {
65 65
 	Title               string
66 66
 	ProfileLabels       []string
67 67
 	ActiveFilters       []string
68
+	NumLabelUnits       map[string]string
68 69
 
69 70
 	NodeCount    int
70 71
 	NodeFraction float64
@@ -241,15 +242,27 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
241 242
 	for _, f := range prof.Function {
242 243
 		f.Filename = trimPath(f.Filename)
243 244
 	}
244
-	// Remove numeric tags not recognized by pprof.
245
+	// Removes all numeric tags except for the bytes tag prior
246
+	// to making graph.
247
+	// TODO: modify to select first numeric tag if no bytes tag
245 248
 	for _, s := range prof.Sample {
246 249
 		numLabels := make(map[string][]int64, len(s.NumLabel))
247
-		for k, v := range s.NumLabel {
250
+		numUnits := make(map[string][]string, len(s.NumLabel))
251
+		for k, vs := range s.NumLabel {
248 252
 			if k == "bytes" {
249
-				numLabels[k] = append(numLabels[k], v...)
253
+				unit := o.NumLabelUnits[k]
254
+				numValues := make([]int64, len(vs))
255
+				numUnit := make([]string, len(vs))
256
+				for i, v := range vs {
257
+					numValues[i] = v
258
+					numUnit[i] = unit
259
+				}
260
+				numLabels[k] = append(numLabels[k], numValues...)
261
+				numUnits[k] = append(numUnits[k], numUnit...)
250 262
 			}
251 263
 		}
252 264
 		s.NumLabel = numLabels
265
+		s.NumUnit = numUnits
253 266
 	}
254 267
 
255 268
 	formatTag := func(v int64, key string) string {
@@ -651,8 +664,9 @@ func printTags(w io.Writer, rpt *Report) error {
651 664
 			}
652 665
 		}
653 666
 		for key, vals := range s.NumLabel {
667
+			unit := o.NumLabelUnits[key]
654 668
 			for _, nval := range vals {
655
-				val := formatTag(nval, key)
669
+				val := formatTag(nval, unit)
656 670
 				valueMap, ok := tagMap[key]
657 671
 				if !ok {
658 672
 					valueMap = make(map[string]int64)
@@ -807,8 +821,13 @@ func printTraces(w io.Writer, rpt *Report) error {
807 821
 
808 822
 		// Print any numeric labels for the sample
809 823
 		var numLabels []string
810
-		for k, v := range sample.NumLabel {
811
-			numLabels = append(numLabels, fmt.Sprintf("%10s:  %s\n", k, strings.Trim(fmt.Sprintf("%d", v), "[]")))
824
+		for key, vals := range sample.NumLabel {
825
+			unit := o.NumLabelUnits[key]
826
+			numValues := make([]string, len(vals))
827
+			for i, vv := range vals {
828
+				numValues[i] = measurement.Label(vv, unit)
829
+			}
830
+			numLabels = append(numLabels, fmt.Sprintf("%10s:  %s\n", key, strings.Join(numValues, " ")))
812 831
 		}
813 832
 		sort.Strings(numLabels)
814 833
 		fmt.Fprint(w, strings.Join(numLabels, ""))

+ 37
- 3
profile/encode.go Zobrazit soubor

@@ -59,12 +59,19 @@ func (p *Profile) preEncode() {
59 59
 		}
60 60
 		sort.Strings(numKeys)
61 61
 		for _, k := range numKeys {
62
+			keyX := addString(strings, k)
62 63
 			vs := s.NumLabel[k]
63
-			for _, v := range vs {
64
+			units := s.NumUnit[k]
65
+			for i, v := range vs {
66
+				var unitX int64
67
+				if len(units) != 0 {
68
+					unitX = addString(strings, units[i])
69
+				}
64 70
 				s.labelX = append(s.labelX,
65 71
 					label{
66
-						keyX: addString(strings, k),
67
-						numX: v,
72
+						keyX:  keyX,
73
+						numX:  v,
74
+						unitX: unitX,
68 75
 					},
69 76
 				)
70 77
 			}
@@ -289,6 +296,7 @@ func (p *Profile) postDecode() error {
289 296
 	for _, s := range p.Sample {
290 297
 		labels := make(map[string][]string, len(s.labelX))
291 298
 		numLabels := make(map[string][]int64, len(s.labelX))
299
+		numUnits := make(map[string][]string, len(s.labelX))
292 300
 		for _, l := range s.labelX {
293 301
 			var key, value string
294 302
 			key, err = getString(p.stringTable, &l.keyX, err)
@@ -296,6 +304,14 @@ func (p *Profile) postDecode() error {
296 304
 				value, err = getString(p.stringTable, &l.strX, err)
297 305
 				labels[key] = append(labels[key], value)
298 306
 			} else if l.numX != 0 {
307
+				numValues := numLabels[key]
308
+				units := numUnits[key]
309
+				if l.unitX != 0 {
310
+					var unit string
311
+					unit, err = getString(p.stringTable, &l.unitX, err)
312
+					units = padStringArray(units, len(numValues))
313
+					numUnits[key] = append(units, unit)
314
+				}
299 315
 				numLabels[key] = append(numLabels[key], l.numX)
300 316
 			}
301 317
 		}
@@ -304,6 +320,12 @@ func (p *Profile) postDecode() error {
304 320
 		}
305 321
 		if len(numLabels) > 0 {
306 322
 			s.NumLabel = numLabels
323
+			for key, units := range numUnits {
324
+				if len(units) > 0 {
325
+					numUnits[key] = padStringArray(units, len(numLabels[key]))
326
+				}
327
+			}
328
+			s.NumUnit = numUnits
307 329
 		}
308 330
 		s.Location = make([]*Location, len(s.locationIDX))
309 331
 		for i, lid := range s.locationIDX {
@@ -340,6 +362,15 @@ func (p *Profile) postDecode() error {
340 362
 	return err
341 363
 }
342 364
 
365
+// padStringArray pads arr with enough empty strings to make arr
366
+// length l when arr's length is less than l.
367
+func padStringArray(arr []string, l int) []string {
368
+	if l <= len(arr) {
369
+		return arr
370
+	}
371
+	return append(arr, make([]string, l-len(arr))...)
372
+}
373
+
343 374
 func (p *ValueType) decoder() []decoder {
344 375
 	return valueTypeDecoder
345 376
 }
@@ -392,6 +423,7 @@ func (p label) encode(b *buffer) {
392 423
 	encodeInt64Opt(b, 1, p.keyX)
393 424
 	encodeInt64Opt(b, 2, p.strX)
394 425
 	encodeInt64Opt(b, 3, p.numX)
426
+	encodeInt64Opt(b, 4, p.unitX)
395 427
 }
396 428
 
397 429
 var labelDecoder = []decoder{
@@ -402,6 +434,8 @@ var labelDecoder = []decoder{
402 434
 	func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).strX) },
403 435
 	// optional int64 num = 3
404 436
 	func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).numX) },
437
+	// optional int64 num = 4
438
+	func(b *buffer, m message) error { return decodeInt64(b, &m.(*label).unitX) },
405 439
 }
406 440
 
407 441
 func (p *Mapping) decoder() []decoder {

+ 6
- 1
profile/merge.go Zobrazit soubor

@@ -155,6 +155,7 @@ func (pm *profileMerger) mapSample(src *Sample) *Sample {
155 155
 		Value:    make([]int64, len(src.Value)),
156 156
 		Label:    make(map[string][]string, len(src.Label)),
157 157
 		NumLabel: make(map[string][]int64, len(src.NumLabel)),
158
+		NumUnit:  make(map[string][]string, len(src.NumLabel)),
158 159
 	}
159 160
 	for i, l := range src.Location {
160 161
 		s.Location[i] = pm.mapLocation(l)
@@ -165,9 +166,13 @@ func (pm *profileMerger) mapSample(src *Sample) *Sample {
165 166
 		s.Label[k] = vv
166 167
 	}
167 168
 	for k, v := range src.NumLabel {
169
+		u := src.NumUnit[k]
168 170
 		vv := make([]int64, len(v))
171
+		uu := make([]string, len(u))
169 172
 		copy(vv, v)
173
+		copy(uu, u)
170 174
 		s.NumLabel[k] = vv
175
+		s.NumUnit[k] = uu
171 176
 	}
172 177
 	// Check memoization table. Must be done on the remapped location to
173 178
 	// account for the remapped mapping. Add current values to the
@@ -200,7 +205,7 @@ func (sample *Sample) key() sampleKey {
200 205
 
201 206
 	numlabels := make([]string, 0, len(sample.NumLabel))
202 207
 	for k, v := range sample.NumLabel {
203
-		numlabels = append(numlabels, fmt.Sprintf("%q%x", k, v))
208
+		numlabels = append(numlabels, fmt.Sprintf("%q%x%x", k, v, sample.NumUnit[k]))
204 209
 	}
205 210
 	sort.Strings(numlabels)
206 211
 

+ 184
- 68
profile/profile.go Zobrazit soubor

@@ -74,6 +74,7 @@ type Sample struct {
74 74
 	Value    []int64
75 75
 	Label    map[string][]string
76 76
 	NumLabel map[string][]int64
77
+	NumUnit  map[string][]string
77 78
 
78 79
 	locationIDX []uint64
79 80
 	labelX      []label
@@ -85,6 +86,8 @@ type label struct {
85 86
 	// Exactly one of the two following values must be set
86 87
 	strX int64
87 88
 	numX int64 // Integer value for this label
89
+	// can be set if numX has value
90
+	unitX int64
88 91
 }
89 92
 
90 93
 // Mapping corresponds to Profile.Mapping
@@ -435,6 +438,74 @@ func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address
435 438
 	return p.CheckValid()
436 439
 }
437 440
 
441
+// NumLabelUnits returns a map of numeric label keys to the units
442
+// associated with those keys and a map of those keys to any units
443
+// that were encountered but not used.
444
+// Unit for a given key is the first encountered unit for that key. If multiple
445
+// units are encountered for values paired with a particular key, then the first
446
+// unit encountered is used and all other units are returned in sorted order
447
+// in map of ignored units.
448
+// If no units are encountered for a particular key, the unit is then inferred
449
+// based on the key.
450
+func (p *Profile) NumLabelUnits() (map[string]string, map[string][]string) {
451
+	numLabelUnits := map[string]string{}
452
+	ignoredUnits := map[string]map[string]bool{}
453
+
454
+	// Determine units based on numeric tags for each sample.
455
+	for _, s := range p.Sample {
456
+		for k, vs := range s.NumLabel {
457
+			units := s.NumUnit[k]
458
+			if len(units) != len(vs) {
459
+				if _, ok := numLabelUnits[k]; !ok {
460
+					numLabelUnits[k] = ""
461
+				} else {
462
+					ignoredUnits[k] = map[string]bool{"": true}
463
+				}
464
+				continue
465
+			}
466
+			for _, unit := range units {
467
+				if wantUnit, ok := numLabelUnits[k]; !ok {
468
+					numLabelUnits[k] = unit
469
+				} else if wantUnit != unit {
470
+					if v, ok := ignoredUnits[k]; ok {
471
+						v[unit] = true
472
+					} else {
473
+						ignoredUnits[k] = map[string]bool{unit: true}
474
+					}
475
+				}
476
+			}
477
+		}
478
+	}
479
+
480
+	// Infer units for keys without any units associated with
481
+	// numeric tag values.
482
+	for key, unit := range numLabelUnits {
483
+		if unit == "" {
484
+			switch key {
485
+			case "alignment", "request":
486
+				numLabelUnits[key] = "bytes"
487
+			default:
488
+				numLabelUnits[key] = key
489
+			}
490
+		}
491
+	}
492
+
493
+	// Copy ignored units into more readable format
494
+	unitsIgnored := make(map[string][]string, len(ignoredUnits))
495
+	for key, values := range ignoredUnits {
496
+		units := make([]string, len(values))
497
+		i := 0
498
+		for unit := range values {
499
+			units[i] = unit
500
+			i++
501
+		}
502
+		sort.Strings(units)
503
+		unitsIgnored[key] = units
504
+	}
505
+
506
+	return numLabelUnits, unitsIgnored
507
+}
508
+
438 509
 // String dumps a text representation of a profile. Intended mainly
439 510
 // for debugging purposes.
440 511
 func (p *Profile) String() string {
@@ -464,87 +535,132 @@ func (p *Profile) String() string {
464 535
 	}
465 536
 	ss = append(ss, strings.TrimSpace(sh1))
466 537
 	for _, s := range p.Sample {
467
-		var sv string
468
-		for _, v := range s.Value {
469
-			sv = fmt.Sprintf("%s %10d", sv, v)
470
-		}
471
-		sv = sv + ": "
472
-		for _, l := range s.Location {
473
-			sv = sv + fmt.Sprintf("%d ", l.ID)
474
-		}
475
-		ss = append(ss, sv)
476
-		const labelHeader = "                "
477
-		if len(s.Label) > 0 {
478
-			ls := []string{}
479
-			for k, v := range s.Label {
480
-				ls = append(ls, fmt.Sprintf("%s:%v", k, v))
481
-			}
482
-			sort.Strings(ls)
483
-			ss = append(ss, labelHeader+strings.Join(ls, " "))
484
-		}
485
-		if len(s.NumLabel) > 0 {
486
-			ls := []string{}
487
-			for k, v := range s.NumLabel {
488
-				ls = append(ls, fmt.Sprintf("%s:%v", k, v))
489
-			}
490
-			sort.Strings(ls)
491
-			ss = append(ss, labelHeader+strings.Join(ls, " "))
492
-		}
538
+		ss = append(ss, s.string())
493 539
 	}
494 540
 
495 541
 	ss = append(ss, "Locations")
496 542
 	for _, l := range p.Location {
497
-		locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
498
-		if m := l.Mapping; m != nil {
499
-			locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
500
-		}
501
-		if len(l.Line) == 0 {
502
-			ss = append(ss, locStr)
503
-		}
504
-		for li := range l.Line {
505
-			lnStr := "??"
506
-			if fn := l.Line[li].Function; fn != nil {
507
-				lnStr = fmt.Sprintf("%s %s:%d s=%d",
508
-					fn.Name,
509
-					fn.Filename,
510
-					l.Line[li].Line,
511
-					fn.StartLine)
512
-				if fn.Name != fn.SystemName {
513
-					lnStr = lnStr + "(" + fn.SystemName + ")"
514
-				}
515
-			}
516
-			ss = append(ss, locStr+lnStr)
517
-			// Do not print location details past the first line
518
-			locStr = "             "
519
-		}
543
+		ss = append(ss, l.string())
520 544
 	}
521 545
 
522 546
 	ss = append(ss, "Mappings")
523 547
 	for _, m := range p.Mapping {
524
-		bits := ""
525
-		if m.HasFunctions {
526
-			bits = bits + "[FN]"
527
-		}
528
-		if m.HasFilenames {
529
-			bits = bits + "[FL]"
530
-		}
531
-		if m.HasLineNumbers {
532
-			bits = bits + "[LN]"
533
-		}
534
-		if m.HasInlineFrames {
535
-			bits = bits + "[IN]"
536
-		}
537
-		ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
538
-			m.ID,
539
-			m.Start, m.Limit, m.Offset,
540
-			m.File,
541
-			m.BuildID,
542
-			bits))
548
+		ss = append(ss, m.string())
543 549
 	}
544 550
 
545 551
 	return strings.Join(ss, "\n") + "\n"
546 552
 }
547 553
 
554
+// string dumps a text representation of a mapping. Intended mainly
555
+// for debugging purposes.
556
+func (m *Mapping) string() string {
557
+	bits := ""
558
+	if m.HasFunctions {
559
+		bits = bits + "[FN]"
560
+	}
561
+	if m.HasFilenames {
562
+		bits = bits + "[FL]"
563
+	}
564
+	if m.HasLineNumbers {
565
+		bits = bits + "[LN]"
566
+	}
567
+	if m.HasInlineFrames {
568
+		bits = bits + "[IN]"
569
+	}
570
+	return fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
571
+		m.ID,
572
+		m.Start, m.Limit, m.Offset,
573
+		m.File,
574
+		m.BuildID,
575
+		bits)
576
+}
577
+
578
+// string dumps a text representation of a location. Intended mainly
579
+// for debugging purposes.
580
+func (l *Location) string() string {
581
+	ss := []string{}
582
+	locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
583
+	if m := l.Mapping; m != nil {
584
+		locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
585
+	}
586
+	if len(l.Line) == 0 {
587
+		ss = append(ss, locStr)
588
+	}
589
+	for li := range l.Line {
590
+		lnStr := "??"
591
+		if fn := l.Line[li].Function; fn != nil {
592
+			lnStr = fmt.Sprintf("%s %s:%d s=%d",
593
+				fn.Name,
594
+				fn.Filename,
595
+				l.Line[li].Line,
596
+				fn.StartLine)
597
+			if fn.Name != fn.SystemName {
598
+				lnStr = lnStr + "(" + fn.SystemName + ")"
599
+			}
600
+		}
601
+		ss = append(ss, locStr+lnStr)
602
+		// Do not print location details past the first line
603
+		locStr = "             "
604
+	}
605
+	return strings.Join(ss, "\n")
606
+}
607
+
608
+// string dumps a text representation of a sample. Intended mainly
609
+// for debugging purposes.
610
+func (s *Sample) string() string {
611
+	ss := []string{}
612
+	var sv string
613
+	for _, v := range s.Value {
614
+		sv = fmt.Sprintf("%s %10d", sv, v)
615
+	}
616
+	sv = sv + ": "
617
+	for _, l := range s.Location {
618
+		sv = sv + fmt.Sprintf("%d ", l.ID)
619
+	}
620
+	ss = append(ss, sv)
621
+	const labelHeader = "                "
622
+	if len(s.Label) > 0 {
623
+		ss = append(ss, labelHeader+labelsToString(s.Label))
624
+	}
625
+	if len(s.NumLabel) > 0 {
626
+		ss = append(ss, labelHeader+numLabelsToString(s.NumLabel, s.NumUnit))
627
+	}
628
+	return strings.Join(ss, "\n")
629
+}
630
+
631
+// labelsToString returns a string representation of a
632
+// map representing labels.
633
+func labelsToString(labels map[string][]string) string {
634
+	ls := []string{}
635
+	for k, v := range labels {
636
+		ls = append(ls, fmt.Sprintf("%s:%v", k, v))
637
+	}
638
+	sort.Strings(ls)
639
+	return strings.Join(ls, " ")
640
+}
641
+
642
+// numLablesToString returns a string representation of a map
643
+// representing numeric labels.
644
+func numLabelsToString(numLabels map[string][]int64, numUnits map[string][]string) string {
645
+	ls := []string{}
646
+	for k, v := range numLabels {
647
+		units := numUnits[k]
648
+		var labelString string
649
+		if len(units) == len(v) {
650
+			values := make([]string, len(v))
651
+			for i, vv := range v {
652
+				values[i] = fmt.Sprintf("%d %s", vv, units[i])
653
+			}
654
+			labelString = fmt.Sprintf("%s:%v", k, values)
655
+		} else {
656
+			labelString = fmt.Sprintf("%s:%v", k, v)
657
+		}
658
+		ls = append(ls, labelString)
659
+	}
660
+	sort.Strings(ls)
661
+	return strings.Join(ls, " ")
662
+}
663
+
548 664
 // Scale multiplies all sample values in a profile by a constant.
549 665
 func (p *Profile) Scale(ratio float64) {
550 666
 	if ratio == 1 {

+ 238
- 0
profile/profile_test.go Zobrazit soubor

@@ -19,6 +19,7 @@ import (
19 19
 	"fmt"
20 20
 	"io/ioutil"
21 21
 	"path/filepath"
22
+	"reflect"
22 23
 	"regexp"
23 24
 	"strings"
24 25
 	"sync"
@@ -350,6 +351,70 @@ var testProfile3 = &Profile{
350 351
 	Mapping:  cpuM,
351 352
 }
352 353
 
354
+var testProfile4 = &Profile{
355
+	PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
356
+	Period:        1,
357
+	DurationNanos: 10e9,
358
+	SampleType: []*ValueType{
359
+		{Type: "samples", Unit: "count"},
360
+	},
361
+	Sample: []*Sample{
362
+		{
363
+			Location: []*Location{cpuL[0]},
364
+			Value:    []int64{1000},
365
+			NumLabel: map[string][]int64{
366
+				"key1": {10},
367
+				"key2": {30},
368
+			},
369
+			NumUnit: map[string][]string{
370
+				"key1": {"bytes"},
371
+				"key2": {"bytes"},
372
+			},
373
+		},
374
+	},
375
+	Location: cpuL,
376
+	Function: cpuF,
377
+	Mapping:  cpuM,
378
+}
379
+
380
+var testProfile5 = &Profile{
381
+	PeriodType:    &ValueType{Type: "cpu", Unit: "milliseconds"},
382
+	Period:        1,
383
+	DurationNanos: 10e9,
384
+	SampleType: []*ValueType{
385
+		{Type: "samples", Unit: "count"},
386
+	},
387
+	Sample: []*Sample{
388
+		{
389
+			Location: []*Location{cpuL[0]},
390
+			Value:    []int64{1000},
391
+			NumLabel: map[string][]int64{
392
+				"key1": {10},
393
+				"key2": {30},
394
+			},
395
+			NumUnit: map[string][]string{
396
+				"key1": {"bytes"},
397
+				"key2": {"bytes"},
398
+			},
399
+		},
400
+		{
401
+			Location: []*Location{cpuL[0]},
402
+			Value:    []int64{1000},
403
+			NumLabel: map[string][]int64{
404
+				"key1": {10},
405
+				"key2": {30},
406
+			},
407
+			NumUnit: map[string][]string{
408
+				"key1": {"kilobytes"},
409
+				"key2": {"kilobytes"},
410
+			},
411
+		},
412
+	},
413
+	Location: cpuL,
414
+	Function: cpuF,
415
+	Mapping:  cpuM,
416
+}
417
+
353 418
 var aggTests = map[string]aggTest{
354 419
 	"precise":         {true, true, true, true, 5},
355 420
 	"fileline":        {false, true, true, true, 4},
@@ -506,6 +571,63 @@ func TestMergeAll(t *testing.T) {
506 571
 	}
507 572
 }
508 573
 
574
+func TestNumLabelMerge(t *testing.T) {
575
+	for _, tc := range []struct {
576
+		name          string
577
+		profs         []*Profile
578
+		wantNumLabels []map[string][]int64
579
+		wantNumUnits  []map[string][]string
580
+	}{
581
+		{
582
+			name:  "different tag units not merged",
583
+			profs: []*Profile{testProfile4.Copy(), testProfile5.Copy()},
584
+			wantNumLabels: []map[string][]int64{
585
+				{
586
+					"key1": {10},
587
+					"key2": {30},
588
+				},
589
+				{
590
+					"key1": {10},
591
+					"key2": {30},
592
+				},
593
+			},
594
+			wantNumUnits: []map[string][]string{
595
+				{
596
+					"key1": {"bytes"},
597
+					"key2": {"bytes"},
598
+				},
599
+				{
600
+					"key1": {"kilobytes"},
601
+					"key2": {"kilobytes"},
602
+				},
603
+			},
604
+		},
605
+	} {
606
+		t.Run(tc.name, func(t *testing.T) {
607
+			prof, err := Merge(tc.profs)
608
+			if err != nil {
609
+				t.Errorf("merge error: %v", err)
610
+			}
611
+
612
+			if want, got := len(tc.wantNumLabels), len(prof.Sample); want != got {
613
+				t.Fatalf("got %d samples, want %d samples", got, want)
614
+			}
615
+			for i, wantLabels := range tc.wantNumLabels {
616
+				numLabels := prof.Sample[i].NumLabel
617
+				if !reflect.DeepEqual(wantLabels, numLabels) {
618
+					t.Errorf("got numeric labels %v, want %v", numLabels, wantLabels)
619
+				}
620
+
621
+				wantUnits := tc.wantNumUnits[i]
622
+				numUnits := prof.Sample[i].NumUnit
623
+				if !reflect.DeepEqual(wantUnits, numUnits) {
624
+					t.Errorf("got numeric labels %v, want %v", numUnits, wantUnits)
625
+				}
626
+			}
627
+		})
628
+	}
629
+}
630
+
509 631
 func TestNormalizeBySameProfile(t *testing.T) {
510 632
 	pb := testProfile1.Copy()
511 633
 	p := testProfile1.Copy()
@@ -689,6 +811,122 @@ func locationHash(s *Sample) string {
689 811
 	return tb
690 812
 }
691 813
 
814
+func TestInferUnits(t *testing.T) {
815
+	var tagFilterTests = []struct {
816
+		name             string
817
+		tagVals          []map[string][]int64
818
+		tagUnits         []map[string][]string
819
+		wantUnits        map[string]string
820
+		wantIgnoredUnits map[string][]string
821
+	}{
822
+		{
823
+			"One sample, multiple keys, different specified units",
824
+			[]map[string][]int64{{"key1": {131072}, "key2": {128}}},
825
+			[]map[string][]string{{"key1": {"bytes"}, "key2": {"kilobytes"}}},
826
+			map[string]string{"key1": "bytes", "key2": "kilobytes"},
827
+			map[string][]string{},
828
+		},
829
+		{
830
+			"One sample, one key with one value, unit specified",
831
+			[]map[string][]int64{{"key1": {8}}},
832
+			[]map[string][]string{{"key1": {"bytes"}}},
833
+			map[string]string{"key1": "bytes"},
834
+			map[string][]string{},
835
+		},
836
+		{
837
+			"Key bytes, unit not specified",
838
+			[]map[string][]int64{{"bytes": {8}}},
839
+			[]map[string][]string{nil},
840
+			map[string]string{"bytes": "bytes"},
841
+			map[string][]string{},
842
+		},
843
+		{
844
+			"One sample, one key with one value, unit not specified",
845
+			[]map[string][]int64{{"kilobytes": {8}}},
846
+			[]map[string][]string{nil},
847
+			map[string]string{"kilobytes": "kilobytes"},
848
+			map[string][]string{},
849
+		},
850
+		{
851
+			"Key request, unit not specified",
852
+			[]map[string][]int64{{"request": {8}}},
853
+			[]map[string][]string{nil},
854
+			map[string]string{"request": "bytes"},
855
+			map[string][]string{},
856
+		},
857
+		{
858
+			"Key alignment, unit not specified",
859
+			[]map[string][]int64{{"alignment": {8}}},
860
+			[]map[string][]string{nil},
861
+			map[string]string{"alignment": "bytes"},
862
+			map[string][]string{},
863
+		},
864
+		{
865
+			"One sample, one key with multiple values and different units",
866
+			[]map[string][]int64{{"key1": {8, 8}}},
867
+			[]map[string][]string{{"key1": {"bytes", "kilobytes"}}},
868
+			map[string]string{"key1": "bytes"},
869
+			map[string][]string{"key1": {"kilobytes"}},
870
+		},
871
+		{
872
+			"Two samples, one key, different units specified",
873
+			[]map[string][]int64{{"key1": {8}}, {"key1": {8}}},
874
+			[]map[string][]string{{"key1": {"bytes"}}, {"key1": {"kilobytes"}}},
875
+			map[string]string{"key1": "bytes"},
876
+			map[string][]string{"key1": {"kilobytes"}},
877
+		},
878
+		{
879
+			"Keys alignment, request, and bytes have units specified",
880
+			[]map[string][]int64{{
881
+				"alignment": {8},
882
+				"request":   {8},
883
+				"bytes":     {8},
884
+			}},
885
+			[]map[string][]string{{
886
+				"alignment": {"seconds"},
887
+				"request":   {"minutes"},
888
+				"bytes":     {"hours"},
889
+			}},
890
+			map[string]string{
891
+				"alignment": "seconds",
892
+				"request":   "minutes",
893
+				"bytes":     "hours",
894
+			},
895
+			map[string][]string{},
896
+		},
897
+	}
898
+	for _, test := range tagFilterTests {
899
+		p := &Profile{Sample: make([]*Sample, len(test.tagVals))}
900
+		for i, numLabel := range test.tagVals {
901
+			s := Sample{
902
+				NumLabel: numLabel,
903
+				NumUnit:  test.tagUnits[i],
904
+			}
905
+			p.Sample[i] = &s
906
+		}
907
+		units, ignoredUnits := p.NumLabelUnits()
908
+		for key, wantUnit := range test.wantUnits {
909
+			unit := units[key]
910
+			if wantUnit != unit {
911
+				t.Errorf("%s: for key %s, got unit %s, want unit %s", test.name, key, unit, wantUnit)
912
+			}
913
+		}
914
+		for key, ignored := range ignoredUnits {
915
+			wantIgnored := test.wantIgnoredUnits[key]
916
+			if len(wantIgnored) != len(ignored) {
917
+				t.Errorf("%s: for key %s, got ignored units %v, got ignored units %v", test.name, key, ignored, wantIgnored)
918
+				continue
919
+			}
920
+			for i, want := range wantIgnored {
921
+				if got := ignored[i]; want != got {
922
+					t.Errorf("%s: for key %s, got ignored units %v, want ignored units %v", test.name, key, ignored, wantIgnored)
923
+					break
924
+				}
925
+			}
926
+		}
927
+	}
928
+}
929
+
692 930
 func TestSetMain(t *testing.T) {
693 931
 	testProfile1.massageMappings()
694 932
 	if testProfile1.Mapping[0].File != mainBinary {

+ 9
- 2
profile/proto_test.go Zobrazit soubor

@@ -113,8 +113,15 @@ var all = &Profile{
113 113
 				"key2": {"value2"},
114 114
 			},
115 115
 			NumLabel: map[string][]int64{
116
-				"key1": {1, 2},
117
-				"key2": {3, 4},
116
+				"key1":      {1, 2},
117
+				"key2":      {3, 4},
118
+				"bytes":     {3, 4},
119
+				"requests":  {1, 1, 3, 4, 5},
120
+				"alignment": {3, 4},
121
+			},
122
+			NumUnit: map[string][]string{
123
+				"requests":  {"", "", "seconds", "", "s"},
124
+				"alignment": {"kilobytes", "kilobytes"},
118 125
 			},
119 126
 		},
120 127
 	},

+ 9
- 0
proto/profile.proto Zobrazit soubor

@@ -109,6 +109,15 @@ message Label {
109 109
   // At most one of the following must be present
110 110
   int64 str = 2;   // Index into string table
111 111
   int64 num = 3;
112
+
113
+  // Should only be present when num is present.
114
+  // Specifies the units of num.
115
+  // Use arbitrary string (for example, "requests") as a custom count unit.
116
+  // If no unit is specified, consumer may apply heuristic to deduce the unit.
117
+  // Consumers may also  interpret units like "bytes" and "kilobytes" as memory
118
+  // units and units like "seconds" and "nanoseconds" as time units,
119
+  // and apply appropriate unit conversions to these.
120
+  int64 num_unit = 4;  // Index into string table
112 121
 }
113 122
 
114 123
 message Mapping {