Skip to content

Commit 0c06993

Browse files
authored
perf: Use stacktrace tree for adhoc uploads (#4438)
* chore: Implement adhoc upload through model.StacktraceTree * chore: Ensure adhoc API request get logged * Keep state between loops per type * Add benchmark
1 parent 9bc2847 commit 0c06993

File tree

3 files changed

+109
-27
lines changed

3 files changed

+109
-27
lines changed

pkg/api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,5 +306,5 @@ func (a *API) RegisterAdmin(ad *operations.Admin) {
306306
}
307307

308308
func (a *API) RegisterAdHocProfiles(ahp *adhocprofiles.AdHocProfiles) {
309-
adhocprofilesv1connect.RegisterAdHocProfileServiceHandler(a.server.HTTP, ahp, a.connectOptionsAuthRecovery()...)
309+
adhocprofilesv1connect.RegisterAdHocProfileServiceHandler(a.server.HTTP, ahp, a.connectOptionsAuthLogRecovery()...)
310310
}

pkg/og/structs/flamebearer/convert/convert.go

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8-
profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
98
"path"
109
"reflect"
1110
"strconv"
1211
"strings"
13-
"time"
1412
"unicode"
1513

16-
"github.com/grafana/pyroscope/pkg/og/agent/spy"
14+
profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
15+
typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
16+
"github.com/grafana/pyroscope/pkg/model"
1717
"github.com/grafana/pyroscope/pkg/og/convert/perf"
1818
"github.com/grafana/pyroscope/pkg/og/convert/pprof"
1919
"github.com/grafana/pyroscope/pkg/og/storage/metadata"
@@ -187,37 +187,96 @@ func JSONToProfile(b []byte, name string, maxNodes int) ([]*flamebearer.Flamebea
187187
return []*flamebearer.FlamebearerProfile{&p}, nil
188188
}
189189

190+
func getProfileType(name string, sampleType int, p *profilev1.Profile) (*typesv1.ProfileType, error) {
191+
tp := &typesv1.ProfileType{
192+
Name: name,
193+
}
194+
195+
// check if the sampleID is valid
196+
if sampleType < 0 || sampleType >= len(p.SampleType) {
197+
return nil, fmt.Errorf("invalid sampleID: %d", sampleType)
198+
}
199+
200+
invalidStr := func(i int) bool {
201+
return i < 0 || i > len(p.StringTable)
202+
}
203+
if v := int(p.PeriodType.Type); invalidStr(v) {
204+
return nil, fmt.Errorf("invalid PeriodType: %d", v)
205+
} else {
206+
tp.PeriodType = p.StringTable[v]
207+
}
208+
if v := int(p.PeriodType.Unit); invalidStr(v) {
209+
return nil, fmt.Errorf("invalid PeriodUnit: %d", v)
210+
} else {
211+
tp.PeriodUnit = p.StringTable[v]
212+
}
213+
if v := int(p.SampleType[sampleType].Type); invalidStr(v) {
214+
return nil, fmt.Errorf("invalid SampleType[%d]: %d", sampleType, v)
215+
} else {
216+
tp.SampleType = p.StringTable[v]
217+
}
218+
if v := int(p.SampleType[sampleType].Unit); invalidStr(v) {
219+
return nil, fmt.Errorf("invalid SampleUnit[%d]: %d", sampleType, v)
220+
} else {
221+
tp.SampleUnit = p.StringTable[v]
222+
}
223+
224+
tp.ID = fmt.Sprintf("%s:%s:%s:%s:%s", name, tp.SampleType, tp.SampleUnit, tp.PeriodType, tp.PeriodUnit)
225+
return tp, nil
226+
}
227+
190228
func PprofToProfile(b []byte, name string, maxNodes int) ([]*flamebearer.FlamebearerProfile, error) {
191229
p := new(profilev1.Profile)
192230
if err := pprof.Decode(bytes.NewReader(b), p); err != nil {
193231
return nil, fmt.Errorf("parsing pprof: %w", err)
194232
}
233+
234+
t := model.NewStacktraceTree(int(maxNodes * 2))
235+
stack := make([]int32, 0, 64)
236+
m := make(map[uint64]int32)
237+
195238
fbs := make([]*flamebearer.FlamebearerProfile, 0)
196-
for _, stype := range tree.SampleTypes(p) {
197-
sampleRate := uint32(100)
198-
units := metadata.SamplesUnits
199-
if c, ok := tree.DefaultSampleTypeMapping[stype]; ok {
200-
units = c.Units
201-
if c.Sampled && p.Period > 0 {
202-
sampleRate = uint32(time.Second / time.Duration(p.Period))
239+
for sampleType := range p.SampleType {
240+
t.Reset()
241+
242+
for i := range p.Sample {
243+
stack = stack[:0]
244+
for j := range p.Sample[i].LocationId {
245+
locIdx := int(p.Sample[i].LocationId[j]) - 1
246+
if locIdx < 0 || len(p.Location) <= locIdx {
247+
return nil, fmt.Errorf("invalid location ID %d in sample %d", p.Sample[i].LocationId[j], i)
248+
}
249+
250+
loc := p.Location[locIdx]
251+
if len(loc.Line) > 0 {
252+
for l := range loc.Line {
253+
stack = append(stack, int32(p.Function[loc.Line[l].FunctionId-1].Name))
254+
}
255+
continue
256+
}
257+
addr, ok := m[loc.Address]
258+
if !ok {
259+
addr = int32(len(p.StringTable))
260+
p.StringTable = append(p.StringTable, strconv.FormatInt(int64(loc.Address), 16))
261+
m[loc.Address] = addr
262+
}
263+
stack = append(stack, addr)
203264
}
265+
266+
if sampleType < 0 || sampleType >= len(p.Sample[i].Value) {
267+
return nil, fmt.Errorf("invalid sampleType index %d for sample %d (len=%d)", sampleType, i, len(p.Sample[i].Value))
268+
}
269+
270+
t.Insert(stack, p.Sample[i].Value[sampleType])
204271
}
205-
t := tree.New()
206-
tree.Get(p, stype, func(_labels *spy.Labels, name []byte, val int) error {
207-
t.Insert(name, uint64(val))
208-
return nil
209-
})
210-
fb := flamebearer.NewProfile(flamebearer.ProfileConfig{
211-
Tree: t,
212-
Name: stype,
213-
MaxNodes: maxNodes,
214-
Metadata: metadata.Metadata{
215-
SpyName: "unknown",
216-
SampleRate: sampleRate,
217-
Units: units,
218-
},
219-
})
220-
fbs = append(fbs, &fb)
272+
273+
tp, err := getProfileType(name, sampleType, p)
274+
if err != nil {
275+
return nil, err
276+
}
277+
278+
fg := model.NewFlameGraph(t.Tree(int64(maxNodes), p.StringTable), int64(maxNodes))
279+
fbs = append(fbs, model.ExportToFlamebearer(fg, tp))
221280
}
222281
if len(fbs) == 0 {
223282
return nil, errors.New("no supported sample type found")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package convert
2+
3+
import (
4+
"io"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func BenchmarkPprofToProfile(b *testing.B) {
12+
f, err := os.Open("./testdata/cpu-unknown.pb.gz")
13+
require.NoError(b, err)
14+
defer f.Close()
15+
data, err := io.ReadAll(f)
16+
require.NoError(b, err)
17+
18+
b.ResetTimer()
19+
for i := 0; i < b.N; i++ {
20+
PprofToProfile(data, "test", 16384)
21+
}
22+
b.StopTimer()
23+
}

0 commit comments

Comments
 (0)