Repository: google/pprof (go)

Issue:
Profile.Copy throws away all NumLabels after it hits NumUnit==""
### What version of pprof are you using?

`02619b876842e0d0afb5e5580d3a374dad740edb`

### What operating system and processor architecture are you using?

```
$ uname -a
Linux markhansen.syd.corp.google.com 5.10.40-1rodete2-amd64 #1 SMP Debian 5.10.40-1rodete2 (2021-06-22) x86_64 GNU/Linux
```

### What did you do?
See https://github.com/google/pprof/pull/649/commits/4c12914e03def8579623408e741bb6f5879e0f92#diff-bbbb7a39a412ce191a6c45df8a9ccaba7ac1ca16b71b80c4d9ae4a14fa075a15, in particular this sample, with a numeric label `allocations` with a number value `1024` and no units (`""`):

```
		{
			Location: []*profile.Location{cpuL[2], cpuL[0]},
			Value:    []int64{10, 10},
			Label: map[string][]string{
				"key1": {"tag3"},
				"key2": {"tag2"},
			},
			NumLabel: map[string][]int64{
				"allocations": []int64{1024},
			},
			NumUnit: map[string][]string{
				"allocations": []string{""},
			},
		},
```

This should be printed as `allocations:1024 foo foo_caller main: 10`, but instead, (see the passing test), is printed without the numbers:

```
				// TODO: bug, this line should say allocations:1024
				"allocations: foo foo_caller main: 10",
```

The blame seems to be somewhere in `Profile.preEncode` or `Profile.postDecode` (called when I call `Profile.Copy`), which seems to skip any numeric values after it hits an empty string unit.

Pull request:
Fix tagroot to properly format unitless numeric tags.
It's possible for the unit array to be empty for a unitless numeric tag.
In this case the tag value should be formatted properly but before this
change it was output empty.

Fixes #651. From the bug description it doesn't look like exactly this
issue but it does get fixed.

The added tests fail before the change and pass after.

Patch:
diff --git a/internal/driver/tagroot.go b/internal/driver/tagroot.go
index c2cdfa4..c43d599 100644
--- a/internal/driver/tagroot.go
+++ b/internal/driver/tagroot.go
@@ -113,12 +113,17 @@ func formatLabelValues(s *profile.Sample, k string, outputUnit string) []string
 	values = append(values, s.Label[k]...)
 	numLabels := s.NumLabel[k]
 	numUnits := s.NumUnit[k]
-	if len(numLabels) != len(numUnits) {
+	if len(numLabels) != len(numUnits) && len(numUnits) != 0 {
 		return values
 	}
 	for i, numLabel := range numLabels {
-		unit := numUnits[i]
-		values = append(values, measurement.ScaledLabel(numLabel, unit, outputUnit))
+		var value string
+		if len(numUnits) != 0 {
+			value = measurement.ScaledLabel(numLabel, numUnits[i], outputUnit)
+		} else {
+			value = measurement.ScaledLabel(numLabel, "", "")
+		}
+		values = append(values, value)
 	}
 	return values
 }


Test patch:
diff --git a/internal/driver/tagroot_test.go b/internal/driver/tagroot_test.go
index d70a901..16d2f8a 100644
--- a/internal/driver/tagroot_test.go
+++ b/internal/driver/tagroot_test.go
@@ -15,6 +15,7 @@ var cpuF = []*profile.Function{
 	{ID: 1, Name: "main", SystemName: "main", Filename: "main.c"},
 	{ID: 2, Name: "foo", SystemName: "foo", Filename: "foo.c"},
 	{ID: 3, Name: "foo_caller", SystemName: "foo_caller", Filename: "foo.c"},
+	{ID: 4, Name: "bar", SystemName: "bar", Filename: "bar.c"},
 }
 
 var cpuM = []*profile.Mapping{
@@ -38,26 +39,6 @@ var cpuM = []*profile.Mapping{
 		HasLineNumbers:  true,
 		HasInlineFrames: true,
 	},
-	{
-		ID:              3,
-		Start:           0x4000,
-		Limit:           0x5000,
-		File:            "/lib/lib2_c.so.6",
-		HasFunctions:    true,
-		HasFilenames:    true,
-		HasLineNumbers:  true,
-		HasInlineFrames: true,
-	},
-	{
-		ID:              4,
-		Start:           0x5000,
-		Limit:           0x9000,
-		File:            "/lib/lib.so_6 (deleted)",
-		HasFunctions:    true,
-		HasFilenames:    true,
-		HasLineNumbers:  true,
-		HasInlineFrames: true,
-	},
 }
 
 var cpuL = []*profile.Location{
@@ -103,6 +84,14 @@ var cpuL = []*profile.Location{
 			{Function: cpuF[2], Line: 3},
 		},
 	},
+	{
+		ID:      3003,
+		Mapping: cpuM[0],
+		Address: 0x3003,
+		Line: []profile.Line{
+			{Function: cpuF[3], Line: 1},
+		},
+	},
 }
 
 var testProfile1 = &profile.Profile{
@@ -173,6 +162,13 @@ var testProfile1 = &profile.Profile{
 				"allocations": {"byte", "kilobyte"},
 			},
 		},
+		{
+			Location: []*profile.Location{cpuL[5], cpuL[0]},
+			Value:    []int64{200, 200},
+			NumLabel: map[string][]int64{
+				"allocations": {1024},
+			},
+		},
 	},
 	Location: cpuL,
 	Function: cpuF,
@@ -199,6 +195,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"main(main.c);foo(foo.c);foo_caller(foo.c) 10",
 				"main(main.c);foo_caller(foo.c) 10000",
 				"main(main.c);foo_caller(foo.c) 1",
+				"main(main.c);bar(bar.c) 200",
 			},
 		},
 		{
@@ -211,6 +208,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"(key404);main(main.c);foo(foo.c);foo_caller(foo.c);(key404) 10",
 				"(key404);main(main.c);foo_caller(foo.c);(key404) 10000",
 				"(key404);main(main.c);foo_caller(foo.c);(key404) 1",
+				"(key404);main(main.c);bar(bar.c);(key404) 200",
 			},
 		},
 		{
@@ -223,6 +221,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"tag3(key1);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
 				"tag4(key1);main(main.c);foo_caller(foo.c) 10000",
 				"tag4(key1);main(main.c);foo_caller(foo.c) 1",
+				"(key1);main(main.c);bar(bar.c) 200",
 			},
 		},
 		{
@@ -235,6 +234,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"tag2(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
 				"tag1(key2);main(main.c);foo_caller(foo.c) 10000",
 				"tag1,tag5(key2);main(main.c);foo_caller(foo.c) 1",
+				"(key2);main(main.c);bar(bar.c) 200",
 			},
 		},
 		{
@@ -247,6 +247,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"main(main.c);foo(foo.c);foo_caller(foo.c);tag3(key1) 10",
 				"main(main.c);foo_caller(foo.c);tag4(key1) 10000",
 				"main(main.c);foo_caller(foo.c);tag4(key1) 1",
+				"main(main.c);bar(bar.c);(key1) 200",
 			},
 		},
 		{
@@ -259,6 +260,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"main(main.c);foo(foo.c);foo_caller(foo.c);(key3) 10",
 				"main(main.c);foo_caller(foo.c);(key3) 10000",
 				"main(main.c);foo_caller(foo.c);(key3) 1",
+				"main(main.c);bar(bar.c);(key3) 200",
 			},
 		},
 		{
@@ -271,6 +273,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"tag3(key1);tag2(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
 				"tag4(key1);tag1(key2);main(main.c);foo_caller(foo.c) 10000",
 				"tag4(key1);tag1,tag5(key2);main(main.c);foo_caller(foo.c) 1",
+				"(key1);(key2);main(main.c);bar(bar.c) 200",
 			},
 		},
 		{
@@ -283,6 +286,7 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 				"main(main.c);foo(foo.c);foo_caller(foo.c);tag3(key1);tag2(key2) 10",
 				"main(main.c);foo_caller(foo.c);tag4(key1);tag1(key2) 10000",
 				"main(main.c);foo_caller(foo.c);tag4(key1);tag1,tag5(key2) 1",
+				"main(main.c);bar(bar.c);(key1);(key2) 200",
 			},
 		},
 		{
@@ -292,10 +296,10 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 			wantSampleFuncs: []string{
 				"main(main.c);(allocations) 1000",
 				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
-				// TODO: bug #651, this line should say 1024(allocations)
-				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 10",
+				"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
 				"main(main.c);foo_caller(foo.c);1024B,2048B(allocations) 10000",
 				"main(main.c);foo_caller(foo.c);1024B,1024B(allocations) 1",
+				"main(main.c);bar(bar.c);1024(allocations) 200",
 			},
 		},
 		{
@@ -305,10 +309,10 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 			wantSampleFuncs: []string{
 				"(allocations);main(main.c) 1000",
 				"(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
-				// TODO: bug #651, this line should say 1024(allocations)
-				"(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
+				"1024(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
 				"1024B,2048B(allocations);main(main.c);foo_caller(foo.c) 10000",
 				"1024B,1024B(allocations);main(main.c);foo_caller(foo.c) 1",
+				"1024(allocations);main(main.c);bar(bar.c) 200",
 			},
 		},
 		{
@@ -319,10 +323,23 @@ func TestAddLabelNodesMatchBooleans(t *testing.T) {
 			wantSampleFuncs: []string{
 				"main(main.c);(allocations) 1000",
 				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
-				// TODO: bug, this line should say 1024(allocations)
-				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 10",
+				"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
 				"main(main.c);foo_caller(foo.c);1kB,2kB(allocations) 10000",
 				"main(main.c);foo_caller(foo.c);1kB,1kB(allocations) 1",
+				"main(main.c);bar(bar.c);1024(allocations) 200",
+			},
+		},
+		{
+			name:    "Numeric units with no units are handled properly by tagleaf",
+			tagleaf: []string{"allocations"},
+			leafm:   true,
+			wantSampleFuncs: []string{
+				"main(main.c);(allocations) 1000",
+				"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
+				"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
+				"main(main.c);foo_caller(foo.c);1024B,2048B(allocations) 10000",
+				"main(main.c);foo_caller(foo.c);1024B,1024B(allocations) 1",
+				"main(main.c);bar(bar.c);1024(allocations) 200",
 			},
 		},
 	} {