Skip to content

Commit b18fe83

Browse files
authored
feat: client side pprof merging for cumulative types (#24)
* feat: implement clientside merging of cumulative profiles * go mod tidy * rename field * rename field * debug tracing * DebugStatsCallback * remove unused field * DebugStatsCallback * DebugStatsCallback * DebugStatsCallback * remvoe debug code * reduce allocations by not doing compaction after removing negative values * export mergers * unexport mergers * parse pprof from bytes
1 parent 4c09702 commit b18fe83

File tree

6 files changed

+183
-9
lines changed

6 files changed

+183
-9
lines changed

.github/workflows/go.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
go: ['1.15', '1.16', '1.17', '1.18']
15+
go: ['1.16', '1.17', '1.18']
1616
steps:
1717
- name: Checkout
1818
uses: actions/checkout@v2

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/pyroscope-io/client
22

33
go 1.17
4+
5+
require github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26

go.sum

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
2+
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
3+
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
4+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
5+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
6+
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
7+
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package cumulativepprof
2+
3+
import (
4+
"fmt"
5+
pprofile "github.com/google/pprof/profile"
6+
"github.com/pyroscope-io/client/upstream"
7+
)
8+
9+
type Merger struct {
10+
SampleTypes []string
11+
MergeRatios []float64
12+
SampleTypeConfig map[string]*upstream.SampleType
13+
Name string
14+
15+
prev *pprofile.Profile
16+
}
17+
18+
type Mergers struct {
19+
Heap *Merger
20+
Block *Merger
21+
Mutex *Merger
22+
}
23+
24+
func NewMergers() *Mergers {
25+
return &Mergers{
26+
Block: &Merger{
27+
SampleTypes: []string{"contentions", "delay"},
28+
MergeRatios: []float64{-1, -1},
29+
SampleTypeConfig: map[string]*upstream.SampleType{
30+
"contentions": {
31+
DisplayName: "block_count",
32+
Units: "lock_samples",
33+
},
34+
"delay": {
35+
DisplayName: "block_duration",
36+
Units: "lock_nanoseconds",
37+
},
38+
},
39+
Name: "block",
40+
},
41+
Mutex: &Merger{
42+
SampleTypes: []string{"contentions", "delay"},
43+
MergeRatios: []float64{-1, -1},
44+
SampleTypeConfig: map[string]*upstream.SampleType{
45+
"contentions": {
46+
DisplayName: "mutex_count",
47+
Units: "lock_samples",
48+
},
49+
"delay": {
50+
DisplayName: "mutex_duration",
51+
Units: "lock_nanoseconds",
52+
},
53+
},
54+
Name: "mutex",
55+
},
56+
Heap: &Merger{
57+
SampleTypes: []string{"alloc_objects", "alloc_space", "inuse_objects", "inuse_space"},
58+
MergeRatios: []float64{-1, -1, 0, 0},
59+
SampleTypeConfig: map[string]*upstream.SampleType{
60+
"alloc_objects": {
61+
Units: "objects",
62+
},
63+
"alloc_space": {
64+
Units: "bytes",
65+
},
66+
"inuse_space": {
67+
Units: "bytes",
68+
Aggregation: "average",
69+
},
70+
"inuse_objects": {
71+
Units: "objects",
72+
Aggregation: "average",
73+
},
74+
},
75+
Name: "heap",
76+
},
77+
}
78+
}
79+
80+
func (m *Merger) Merge(prev, cur []byte) (*pprofile.Profile, error) {
81+
p2, err := m.parseProfile(cur)
82+
if err != nil {
83+
return nil, err
84+
}
85+
p1 := m.prev
86+
if p1 == nil {
87+
p1, err = m.parseProfile(prev)
88+
if err != nil {
89+
return nil, err
90+
}
91+
}
92+
93+
err = p1.ScaleN(m.MergeRatios)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
p, err := pprofile.Merge([]*pprofile.Profile{p1, p2})
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
for _, sample := range p.Sample {
104+
if len(sample.Value) > 0 && sample.Value[0] < 0 {
105+
for i := range sample.Value {
106+
sample.Value[i] = 0
107+
}
108+
}
109+
}
110+
111+
m.prev = p2
112+
return p, nil
113+
}
114+
115+
func (m *Merger) parseProfile(bs []byte) (*pprofile.Profile, error) {
116+
p, err := pprofile.ParseData(bs)
117+
if err != nil {
118+
return nil, err
119+
}
120+
if got := len(p.SampleType); got != len(m.SampleTypes) {
121+
return nil, fmt.Errorf("invalid profile: got %d sample types, want %d", got, len(m.SampleTypes))
122+
}
123+
for i, want := range m.SampleTypes {
124+
if got := p.SampleType[i].Type; got != want {
125+
return nil, fmt.Errorf("invalid profile: got %q sample type at index %d, want %q", got, i, want)
126+
}
127+
}
128+
return p, nil
129+
}

pyroscope/api.go

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Config struct {
2020
ProfileTypes []ProfileType
2121
DisableGCRuns bool // this will disable automatic runtime.GC runs between getting the heap profiles
2222
DisableAutomaticResets bool // disable automatic profiler reset every 10 seconds. Reset manually by calling Flush method
23+
DisableCumulativeMerge bool // disable client side merging of cumulative profiles (alloc_objects, alloc_count, block_count, block_duration, mutex_count, mutex_duration) merging
2324
}
2425

2526
type Profiler struct {
@@ -65,6 +66,7 @@ func Start(cfg Config) (*Profiler, error) {
6566
ProfilingTypes: cfg.ProfileTypes,
6667
DisableGCRuns: cfg.DisableGCRuns,
6768
DisableAutomaticResets: cfg.DisableAutomaticResets,
69+
DisableCumulativeMerge: cfg.DisableCumulativeMerge,
6870
SampleRate: cfg.SampleRate,
6971
UploadRate: 10 * time.Second,
7072
}

pyroscope/session.go

+42-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pyroscope
33
import (
44
"bytes"
55
"github.com/pyroscope-io/client/internal/alignedticker"
6+
"github.com/pyroscope-io/client/internal/cumulativepprof"
67
"runtime"
78
"runtime/pprof"
89
"sync"
@@ -19,6 +20,7 @@ type Session struct {
1920
profileTypes []ProfileType
2021
uploadRate time.Duration
2122
disableGCRuns bool
23+
disableCumulativeMerge bool
2224
DisableAutomaticResets bool
2325

2426
logger Logger
@@ -40,6 +42,8 @@ type Session struct {
4042
lastGCGeneration uint32
4143
appName string
4244
startTime time.Time
45+
46+
mergers *cumulativepprof.Mergers
4347
}
4448

4549
type SessionConfig struct {
@@ -50,6 +54,7 @@ type SessionConfig struct {
5054
ProfilingTypes []ProfileType
5155
DisableGCRuns bool
5256
DisableAutomaticResets bool
57+
DisableCumulativeMerge bool
5358
SampleRate uint32
5459
UploadRate time.Duration
5560
}
@@ -71,6 +76,7 @@ func NewSession(c SessionConfig) (*Session, error) {
7176
profileTypes: c.ProfilingTypes,
7277
disableGCRuns: c.DisableGCRuns,
7378
DisableAutomaticResets: c.DisableAutomaticResets,
79+
disableCumulativeMerge: c.DisableCumulativeMerge,
7480
sampleRate: c.SampleRate,
7581
uploadRate: c.UploadRate,
7682
stopCh: make(chan struct{}),
@@ -81,8 +87,9 @@ func NewSession(c SessionConfig) (*Session, error) {
8187
goroutinesBuf: &bytes.Buffer{},
8288
mutexBuf: &bytes.Buffer{},
8389
blockBuf: &bytes.Buffer{},
84-
}
8590

91+
mergers: cumulativepprof.NewMergers(),
92+
}
8693
return ps, nil
8794
}
8895

@@ -265,7 +272,7 @@ func (ps *Session) uploadData(startTime, endTime time.Time) {
265272
curBlockBuf := copyBuf(ps.blockBuf.Bytes())
266273
ps.blockBuf.Reset()
267274
if ps.blockPrevBytes != nil {
268-
ps.upstream.Upload(&upstream.UploadJob{
275+
job := &upstream.UploadJob{
269276
Name: ps.appName,
270277
StartTime: startTime,
271278
EndTime: endTime,
@@ -285,7 +292,9 @@ func (ps *Session) uploadData(startTime, endTime time.Time) {
285292
Cumulative: true,
286293
},
287294
},
288-
})
295+
}
296+
ps.mergeCumulativeProfile(ps.mergers.Block, job)
297+
ps.upstream.Upload(job)
289298
}
290299
ps.blockPrevBytes = curBlockBuf
291300
}
@@ -297,7 +306,7 @@ func (ps *Session) uploadData(startTime, endTime time.Time) {
297306
curMutexBuf := copyBuf(ps.mutexBuf.Bytes())
298307
ps.mutexBuf.Reset()
299308
if ps.mutexPrevBytes != nil {
300-
ps.upstream.Upload(&upstream.UploadJob{
309+
job := &upstream.UploadJob{
301310
Name: ps.appName,
302311
StartTime: startTime,
303312
EndTime: endTime,
@@ -317,7 +326,9 @@ func (ps *Session) uploadData(startTime, endTime time.Time) {
317326
Cumulative: true,
318327
},
319328
},
320-
})
329+
}
330+
ps.mergeCumulativeProfile(ps.mergers.Mutex, job)
331+
ps.upstream.Upload(job)
321332
}
322333
ps.mutexPrevBytes = curMutexBuf
323334
}
@@ -336,8 +347,8 @@ func (ps *Session) uploadData(startTime, endTime time.Time) {
336347
pprof.WriteHeapProfile(ps.memBuf)
337348
curMemBytes := copyBuf(ps.memBuf.Bytes())
338349
ps.memBuf.Reset()
339-
if ps.memPrevBytes != nil {
340-
ps.upstream.Upload(&upstream.UploadJob{
350+
if ps.memPrevBytes != nil { //todo does this if statement loose first 10s profile?
351+
job := &upstream.UploadJob{
341352
Name: ps.appName,
342353
StartTime: startTime,
343354
EndTime: endTime,
@@ -346,14 +357,37 @@ func (ps *Session) uploadData(startTime, endTime time.Time) {
346357
Format: upstream.FormatPprof,
347358
Profile: curMemBytes,
348359
PrevProfile: ps.memPrevBytes,
349-
})
360+
}
361+
ps.mergeCumulativeProfile(ps.mergers.Heap, job)
362+
ps.upstream.Upload(job)
350363
}
351364
ps.memPrevBytes = curMemBytes
352365
ps.lastGCGeneration = currentGCGeneration
353366
}
354367
}
355368
}
356369

370+
func (ps *Session) mergeCumulativeProfile(m *cumulativepprof.Merger, job *upstream.UploadJob) {
371+
// todo should we filter by enabled ps.profileTypes to reduce profile size ? maybe add a separate option ?
372+
if ps.disableCumulativeMerge {
373+
return
374+
}
375+
p, err := m.Merge(job.PrevProfile, job.Profile)
376+
if err != nil {
377+
ps.logger.Errorf("failed to merge %s profiles %v", m.Name, err)
378+
return
379+
}
380+
var prof bytes.Buffer
381+
err = p.Write(&prof)
382+
if err != nil {
383+
ps.logger.Errorf("failed to serialize merged %s profiles %v", m.Name, err)
384+
return
385+
}
386+
job.PrevProfile = nil
387+
job.Profile = prof.Bytes()
388+
job.SampleTypeConfig = m.SampleTypeConfig
389+
}
390+
357391
func (ps *Session) Stop() {
358392
ps.trieMutex.Lock()
359393
defer ps.trieMutex.Unlock()

0 commit comments

Comments
 (0)