profile: avoid unused label map allocations and clear labelX in postDecode (#1064) For every sample with labels, `postDecode` previously allocated all three label maps (`Label`, `NumLabel`, and `NumUnit`) sized to `len(s.labelX)`, assigned an empty `NumUnit` map even when numeric labels carried no units, and never cleared the raw `s.labelX` slice after populating the maps. Count string, numeric, and unit entries first so only non-empty maps are allocated with appropriate capacity hints, leave `s.NumUnit` nil when no units are present, and clear `s.labelX` consistently with the other `*X` fields in `postDecode`. ``` name old time/op new time/op delta ParseLabels/num_label-64 35.5ms ± 4% 32.8ms ± 5% -7.54% (p=0.009 n=6+6) ParseLabels/num_label_with_unit-64 41.9ms ± 2% 39.7ms ± 5% -5.13% (p=0.009 n=5+6) ParseLabels/string_label-64 35.5ms ± 4% 34.4ms ± 4% ~ (p=0.132 n=6+6) ParseLabels/many_labels-64 80.7ms ± 2% 62.3ms ± 3% -22.86% (p=0.002 n=6+6) name old inuse-alloc/op new inuse-alloc/op delta ParseLabels/num_label-64 20.1MB ± 0% 18.4MB ± 0% -8.14% (p=0.000 n=5+6) ParseLabels/num_label_with_unit-64 27.6MB ± 0% 26.9MB ± 0% -2.37% (p=0.008 n=5+5) ParseLabels/string_label-64 19.3MB ± 0% 18.6MB ± 0% -3.39% (p=0.002 n=6+6) ParseLabels/many_labels-64 70.9MB ± 0% 28.2MB ± 0% -60.16% (p=0.002 n=6+6) name old alloc/op new alloc/op delta ParseLabels/num_label-64 33.9MB ± 0% 31.9MB ± 0% -5.79% (p=0.002 n=6+6) ParseLabels/num_label_with_unit-64 41.4MB ± 0% 40.4MB ± 0% -2.37% (p=0.002 n=6+6) ParseLabels/string_label-64 34.1MB ± 0% 32.1MB ± 0% -5.76% (p=0.002 n=6+6) ParseLabels/many_labels-64 94.5MB ± 0% 62.3MB ± 0% -34.06% (p=0.004 n=5+6) name old allocs/op new allocs/op delta ParseLabels/num_label-64 297k ± 0% 257k ± 0% -13.74% (p=0.002 n=6+6) ParseLabels/num_label_with_unit-64 338k ± 0% 318k ± 0% -6.04% (p=0.002 n=6+6) ParseLabels/string_label-64 297k ± 0% 257k ± 0% -13.74% (p=0.002 n=6+6) ParseLabels/many_labels-64 706k ± 0% 543k ± 0% -23.15% (p=0.026 n=5+6) ```
diff --git a/profile/encode.go b/profile/encode.go index 8ce9d3c..3af74d4 100644 --- a/profile/encode.go +++ b/profile/encode.go
@@ -321,9 +321,29 @@ for _, s := range p.Sample { if len(s.labelX) > 0 { - labels := make(map[string][]string, len(s.labelX)) - numLabels := make(map[string][]int64, len(s.labelX)) - numUnits := make(map[string][]string, len(s.labelX)) + var numStr, numNum, numUnit int + for _, l := range s.labelX { + if l.strX != 0 { + numStr++ + } else if l.numX != 0 || l.unitX != 0 { + numNum++ + if l.unitX != 0 { + numUnit++ + } + } + } + var labels map[string][]string + if numStr > 0 { + labels = make(map[string][]string, numStr) + } + var numLabels map[string][]int64 + if numNum > 0 { + numLabels = make(map[string][]int64, numNum) + } + var numUnits map[string][]string + if numUnit > 0 { + numUnits = make(map[string][]string, numUnit) + } for _, l := range s.labelX { var key, value string key, err = getString(p.stringTable, &l.keyX, err) @@ -332,10 +352,10 @@ labels[key] = append(labels[key], value) } else if l.numX != 0 || l.unitX != 0 { numValues := numLabels[key] - units := numUnits[key] if l.unitX != 0 { var unit string unit, err = getString(p.stringTable, &l.unitX, err) + units := numUnits[key] units = padStringArray(units, len(numValues)) numUnits[key] = append(units, unit) } @@ -347,14 +367,17 @@ } if len(numLabels) > 0 { s.NumLabel = numLabels - for key, units := range numUnits { - if len(units) > 0 { - numUnits[key] = padStringArray(units, len(numLabels[key])) + if len(numUnits) > 0 { + for key, units := range numUnits { + if len(units) > 0 { + numUnits[key] = padStringArray(units, len(numLabels[key])) + } } + s.NumUnit = numUnits } - s.NumUnit = numUnits } } + s.labelX = nil s.Location = locBuffer[:len(s.locationIDX)] locBuffer = locBuffer[len(s.locationIDX):]
diff --git a/profile/profile_test.go b/profile/profile_test.go index cbe7e0b..3089747 100644 --- a/profile/profile_test.go +++ b/profile/profile_test.go
@@ -23,6 +23,7 @@ "os" "path/filepath" "reflect" + "runtime" "strings" "sync" "testing" @@ -1956,6 +1957,84 @@ } } +func BenchmarkParseLabels(b *testing.B) { + base, err := ParseData(proftest.LargeProfile(b)) + if err != nil { + b.Fatal(err) + } + for _, tc := range []struct { + name string + mutate func(*Profile) + }{ + { + name: "num_label", + mutate: func(p *Profile) { + for _, s := range p.Sample { + s.NumLabel = map[string][]int64{"bytes": {512}} + } + }, + }, + { + name: "num_label_with_unit", + mutate: func(p *Profile) { + p.SetNumLabel("bytes", []int64{512}, []string{"bytes"}) + }, + }, + { + name: "string_label", + mutate: func(p *Profile) { + p.SetLabel("key", []string{"value"}) + }, + }, + { + name: "many_labels", + mutate: func(p *Profile) { + for i := 0; i < 8; i++ { + p.SetLabel(fmt.Sprintf("key%d", i), []string{"value"}) + } + for _, s := range p.Sample { + s.NumLabel = map[string][]int64{"bytes": {512}} + } + }, + }, + } { + b.Run(tc.name, func(b *testing.B) { + p := base.Copy() + tc.mutate(p) + var buf bytes.Buffer + if err := p.WriteUncompressed(&buf); err != nil { + b.Fatal(err) + } + raw := buf.Bytes() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := ParseData(raw); err != nil { + b.Fatal(err) + } + } + b.StopTimer() + + runtime.GC() + runtime.GC() + var m1, m2 runtime.MemStats + runtime.ReadMemStats(&m1) + parsed, err := ParseData(raw) + if err != nil { + b.Fatal(err) + } + runtime.GC() + runtime.GC() + runtime.ReadMemStats(&m2) + runtime.KeepAlive(parsed) + if m2.HeapAlloc > m1.HeapAlloc { + b.ReportMetric(float64(m2.HeapAlloc-m1.HeapAlloc), "inuse-B/op") + } + }) + } +} + func BenchmarkWrite(b *testing.B) { p, err := Parse(bytes.NewBuffer(proftest.LargeProfile(b))) if err != nil {
diff --git a/profile/proto_test.go b/profile/proto_test.go index e030bb2..3051eec 100644 --- a/profile/proto_test.go +++ b/profile/proto_test.go
@@ -152,6 +152,13 @@ "size": {"bytes"}, }, }, + { + Location: []*Location{testL[0]}, + Value: []int64{50, 60}, + NumLabel: map[string][]int64{ + "bytes": {512}, + }, + }, }, Function: testF, Mapping: testM, @@ -168,6 +175,12 @@ t.Fatal(err) } + for i, s := range all2.Sample { + if s.labelX != nil { + t.Errorf("sample %d: labelX not cleared after decode: %v", i, s.labelX) + } + } + js1 := proftest.EncodeJSON(&all) js2 := proftest.EncodeJSON(&all2) if string(js1) != string(js2) {