Skip to content

Commit d326bfb

Browse files
authored
profiler: Implement Delta Profiles (#842)
This patch implements delta profiles which will allow us to enable allocation, mutex and block profiles in the aggregation and comparison views of our web ui. The internal google doc "RFC: Go Profiling: Delta Profiles" describes this in great detail. To simplify the code, a refactoring was done that attempts to increase code sharing between similar profile types, in particular heap, mutex, block and goroutine profiles (see type profileType). Additionally the new profiler.enabledProfileTypes() method ensures that profiles are collected in a deterministic order. Testing is accomplished by the new pprofutils package which allows converting profiles between protobuf and a simplified text format. Since that package also contains a suitable delta profiling implementation, it's used for the delta profiling itself as well. In this iteration, delta profiles are uploaded in addition to the original profiles using a "delta-" prefix, e.g. "delta-mutex.pprof". This is done to avoid breaking things until the backend has made corresponding changes as well. The plan for the next iteration is to stop uploading the original profiles since they are redundant and a waste of bandwidth and storage. One particular complexity worth noting is that the "delta-heap.pprof" contains 2 profiles alloc_ and inuse_. Only the alloc_ sample types are subject to delta computation, the inuse_ ones are kept as-is since they describe the current state of the heap's live set.
1 parent 8317212 commit d326bfb

File tree

16 files changed

+938
-234
lines changed

16 files changed

+938
-234
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ require (
66
github.com/DataDog/datadog-go v4.4.0+incompatible
77
github.com/DataDog/gostackparse v0.5.0
88
github.com/DataDog/sketches-go v1.0.0
9-
github.com/google/pprof v0.0.0-20210125172800-10e9aeb4a998
9+
github.com/google/pprof v0.0.0-20210423192551-a2663126120b
1010
github.com/tinylib/msgp v1.1.2
1111
)

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
259259
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
260260
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
261261
github.com/google/pprof v0.0.0-20210125172800-10e9aeb4a998/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
262+
github.com/google/pprof v0.0.0-20210423192551-a2663126120b h1:l2YRhr+YLzmSp7KJMswRVk/lO5SwoFIcCLzJsVj+YPc=
263+
github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
262264
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
263265
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
264266
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
@@ -539,6 +541,7 @@ github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgh
539541
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
540542
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
541543
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
544+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
542545
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
543546
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
544547
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# pprofutils
2+
3+
Internal fork of https://github.com/felixge/pprofutils stripped to only include
4+
essential code and tests. It's used for delta profiles as well as testing.
5+
6+
It'd be nice to keep this in sync with upstream, but no worries if not. We just
7+
need the delta profile stuff to work.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2021 Datadog, Inc.
5+
6+
package pprofutils
7+
8+
import (
9+
"errors"
10+
11+
"github.com/google/pprof/profile"
12+
)
13+
14+
// Delta describes how to compute the delta between two profiles and implements
15+
// the conversion.
16+
type Delta struct {
17+
// SampleTypes limits the delta calcultion to the given sample types. Other
18+
// sample types will retain the values of profile b. The defined sample types
19+
// must exist in the profile, otherwise derivation will fail with an error.
20+
// If the slice is empty, all sample types are subject to delta profile
21+
// derivation.
22+
//
23+
// The use case for this for this is to deal with the heap profile which
24+
// contains alloc and inuse sample types, but delta profiling makes no sense
25+
// for the latter.
26+
SampleTypes []ValueType
27+
}
28+
29+
// Convert computes the delta between all values b-a and returns them as a new
30+
// profile. Samples that end up with a delta of 0 are dropped. WARNING: Profile
31+
// a will be mutated by this function. You should pass a copy if that's
32+
// undesirable.
33+
func (d Delta) Convert(a, b *profile.Profile) (*profile.Profile, error) {
34+
ratios := make([]float64, len(a.SampleType))
35+
36+
found := 0
37+
for i, st := range a.SampleType {
38+
// Empty c.SampleTypes means we calculate the delta for every st
39+
if len(d.SampleTypes) == 0 {
40+
ratios[i] = -1
41+
continue
42+
}
43+
44+
// Otherwise we only calcuate the delta for any st that is listed in
45+
// c.SampleTypes. st's not listed in there will default to ratio 0, which
46+
// means we delete them from pa, so only the pb values remain in the final
47+
// profile.
48+
for _, deltaSt := range d.SampleTypes {
49+
if deltaSt.Type == st.Type && deltaSt.Unit == st.Unit {
50+
ratios[i] = -1
51+
found++
52+
}
53+
}
54+
}
55+
if found != len(d.SampleTypes) {
56+
return nil, errors.New("one or more sample type(s) was not found in the profile")
57+
}
58+
59+
a.ScaleN(ratios)
60+
61+
delta, err := profile.Merge([]*profile.Profile{a, b})
62+
if err != nil {
63+
return nil, err
64+
}
65+
return delta, delta.CheckValid()
66+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2021 Datadog, Inc.
5+
6+
package pprofutils
7+
8+
import (
9+
"bytes"
10+
"strings"
11+
"testing"
12+
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestDelta(t *testing.T) {
17+
t.Run("simple", func(t *testing.T) {
18+
var deltaText bytes.Buffer
19+
20+
profA, err := Text{}.Convert(strings.NewReader(strings.TrimSpace(`
21+
main;foo 5
22+
main;foo;bar 3
23+
main;foobar 4
24+
`)))
25+
require.NoError(t, err)
26+
27+
profB, err := Text{}.Convert(strings.NewReader(strings.TrimSpace(`
28+
main;foo 8
29+
main;foo;bar 3
30+
main;foobar 5
31+
`)))
32+
require.NoError(t, err)
33+
34+
delta, err := Delta{}.Convert(profA, profB)
35+
require.NoError(t, err)
36+
37+
require.NoError(t, Protobuf{}.Convert(delta, &deltaText))
38+
require.Equal(t, deltaText.String(), strings.TrimSpace(`
39+
main;foo 3
40+
main;foobar 1
41+
`)+"\n")
42+
})
43+
44+
t.Run("sampleTypes", func(t *testing.T) {
45+
profA, err := Text{}.Convert(strings.NewReader(strings.TrimSpace(`
46+
x/count y/count
47+
main;foo 5 10
48+
main;foo;bar 3 6
49+
main;foo;baz 9 0
50+
main;foobar 4 8
51+
`)))
52+
require.NoError(t, err)
53+
54+
profB, err := Text{}.Convert(strings.NewReader(strings.TrimSpace(`
55+
x/count y/count
56+
main;foo 8 16
57+
main;foo;bar 3 6
58+
main;foo;baz 9 0
59+
main;foobar 5 10
60+
`)))
61+
require.NoError(t, err)
62+
63+
t.Run("happyPath", func(t *testing.T) {
64+
var deltaText bytes.Buffer
65+
66+
deltaConfig := Delta{SampleTypes: []ValueType{{Type: "x", Unit: "count"}}}
67+
delta, err := deltaConfig.Convert(profA, profB)
68+
require.NoError(t, err)
69+
70+
require.NoError(t, Protobuf{SampleTypes: true}.Convert(delta, &deltaText))
71+
require.Equal(t, deltaText.String(), strings.TrimSpace(`
72+
x/count y/count
73+
main;foo 3 16
74+
main;foobar 1 10
75+
main;foo;bar 0 6
76+
`)+"\n")
77+
})
78+
79+
t.Run("unknownSampleType", func(t *testing.T) {
80+
deltaConfig := Delta{SampleTypes: []ValueType{{Type: "foo", Unit: "count"}}}
81+
_, err := deltaConfig.Convert(profA, profB)
82+
require.Equal(t, "one or more sample type(s) was not found in the profile", err.Error())
83+
})
84+
})
85+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2021 Datadog, Inc.
5+
6+
// Package pprofutils is a fork of github.com/felixge/pprofutils, see README.
7+
package pprofutils
8+
9+
// ValueType describes the type and unit of a value.
10+
type ValueType struct {
11+
Type string
12+
Unit string
13+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2021 Datadog, Inc.
5+
6+
package pprofutils
7+
8+
import (
9+
"bufio"
10+
"fmt"
11+
"io"
12+
"sort"
13+
"strings"
14+
15+
"github.com/google/pprof/profile"
16+
)
17+
18+
// Protobuf converts from pprof's protobuf to folded text format.
19+
type Protobuf struct {
20+
// SampleTypes causes the text output to begin with a header line listing
21+
// the sample types found in the profile. This is a custom extension to the
22+
// folded text format.
23+
SampleTypes bool
24+
}
25+
26+
// Convert marshals the given protobuf profile into folded text format.
27+
func (p Protobuf) Convert(protobuf *profile.Profile, text io.Writer) error {
28+
w := bufio.NewWriter(text)
29+
if p.SampleTypes {
30+
var sampleTypes []string
31+
for _, sampleType := range protobuf.SampleType {
32+
sampleTypes = append(sampleTypes, sampleType.Type+"/"+sampleType.Unit)
33+
}
34+
w.WriteString(strings.Join(sampleTypes, " ") + "\n")
35+
}
36+
if err := protobuf.Aggregate(true, true, false, false, false); err != nil {
37+
return err
38+
}
39+
protobuf = protobuf.Compact()
40+
sort.Slice(protobuf.Sample, func(i, j int) bool {
41+
return protobuf.Sample[i].Value[0] > protobuf.Sample[j].Value[0]
42+
})
43+
for _, sample := range protobuf.Sample {
44+
var frames []string
45+
for i := range sample.Location {
46+
loc := sample.Location[len(sample.Location)-i-1]
47+
for j := range loc.Line {
48+
line := loc.Line[len(loc.Line)-j-1]
49+
frames = append(frames, line.Function.Name)
50+
}
51+
}
52+
var values []string
53+
for _, val := range sample.Value {
54+
values = append(values, fmt.Sprintf("%d", val))
55+
if !p.SampleTypes {
56+
break
57+
}
58+
}
59+
fmt.Fprintf(
60+
w,
61+
"%s %s\n",
62+
strings.Join(frames, ";"),
63+
strings.Join(values, " "),
64+
)
65+
}
66+
return w.Flush()
67+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2021 Datadog, Inc.
5+
6+
package pprofutils
7+
8+
import (
9+
"bytes"
10+
"io/ioutil"
11+
"path/filepath"
12+
"strings"
13+
"testing"
14+
15+
"github.com/google/pprof/profile"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestProtobufConvert(t *testing.T) {
20+
t.Run("basic", func(t *testing.T) {
21+
data, err := ioutil.ReadFile(filepath.Join("test-fixtures", "pprof.samples.cpu.001.pb.gz"))
22+
require.NoError(t, err)
23+
24+
proto, err := profile.Parse(bytes.NewReader(data))
25+
require.NoError(t, err)
26+
27+
out := bytes.Buffer{}
28+
require.NoError(t, Protobuf{}.Convert(proto, &out))
29+
want := strings.TrimSpace(`
30+
golang.org/x/sync/errgroup.(*Group).Go.func1;main.run.func2;main.computeSum 19
31+
runtime.mcall;runtime.park_m;runtime.resetForSleep;runtime.resettimer;runtime.modtimer;runtime.wakeNetPoller;runtime.netpollBreak;runtime.write;runtime.write1 7
32+
golang.org/x/sync/errgroup.(*Group).Go.func1;main.run.func2;main.computeSum;runtime.asyncPreempt 5
33+
runtime.mstart;runtime.mstart1;runtime.sysmon;runtime.usleep 3
34+
runtime.mcall;runtime.park_m;runtime.schedule;runtime.findrunnable;runtime.stopm;runtime.notesleep;runtime.semasleep;runtime.pthread_cond_wait 2
35+
runtime.mcall;runtime.gopreempt_m;runtime.goschedImpl;runtime.schedule;runtime.findrunnable;runtime.stopm;runtime.notesleep;runtime.semasleep;runtime.pthread_cond_wait 1
36+
runtime.mcall;runtime.park_m;runtime.schedule;runtime.findrunnable;runtime.checkTimers;runtime.nanotime;runtime.nanotime1 1
37+
`) + "\n"
38+
require.Equal(t, out.String(), want)
39+
})
40+
41+
t.Run("differentLinesPerFunction", func(t *testing.T) {
42+
data, err := ioutil.ReadFile(filepath.Join("test-fixtures", "pprof.lines.pb.gz"))
43+
require.NoError(t, err)
44+
45+
proto, err := profile.Parse(bytes.NewReader(data))
46+
require.NoError(t, err)
47+
48+
out := bytes.Buffer{}
49+
require.NoError(t, Protobuf{}.Convert(proto, &out))
50+
want := strings.TrimSpace(`
51+
main.run.func1;main.threadKind.Run;main.goGo1;main.goHog 85
52+
main.run.func1;main.threadKind.Run;main.goGo2;main.goHog 78
53+
main.run.func1;main.threadKind.Run;main.goGo3;main.goHog 72
54+
main.run.func1;main.threadKind.Run;main.goGo0;main.goHog 72
55+
main.run.func1;main.threadKind.Run;main.goGo0;main.goHog;runtime.asyncPreempt 1
56+
`) + "\n"
57+
require.Equal(t, out.String(), want)
58+
})
59+
}
940 Bytes
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)