github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/symdb/symdb_test.go (about) 1 package symdb 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "sort" 8 "sync/atomic" 9 "testing" 10 "time" 11 12 phlareobj "github.com/grafana/pyroscope/pkg/objstore" 13 "github.com/grafana/pyroscope/pkg/objstore/providers/memory" 14 pprofth "github.com/grafana/pyroscope/pkg/pprof/testhelper" 15 16 "github.com/cespare/xxhash/v2" 17 "github.com/stretchr/testify/require" 18 "github.com/thanos-io/objstore" 19 20 googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 21 phlaremodel "github.com/grafana/pyroscope/pkg/model" 22 "github.com/grafana/pyroscope/pkg/objstore/providers/filesystem" 23 "github.com/grafana/pyroscope/pkg/phlaredb/block" 24 v1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1" 25 "github.com/grafana/pyroscope/pkg/pprof" 26 ) 27 28 type memSuite struct { 29 t testing.TB 30 31 config *Config 32 db *SymDB 33 34 // partition => sample type index => object 35 files [][]string 36 profiles map[uint64]*googlev1.Profile 37 indexed map[uint64][]v1.InMemoryProfile 38 } 39 40 type blockSuite struct { 41 *memSuite 42 reader *Reader 43 testBucket 44 } 45 46 func newMemSuite(t testing.TB, files [][]string) *memSuite { 47 s := memSuite{t: t, files: files} 48 s.init() 49 return &s 50 } 51 52 func newBlockSuite(t testing.TB, files [][]string) *blockSuite { 53 b := blockSuite{memSuite: newMemSuite(t, files)} 54 b.flush() 55 return &b 56 } 57 58 func (s *memSuite) init() { 59 if s.config == nil { 60 s.config = DefaultConfig().WithDirectory(s.t.TempDir()) 61 } 62 if s.db == nil { 63 s.db = NewSymDB(s.config) 64 } 65 s.profiles = make(map[uint64]*googlev1.Profile) 66 s.indexed = make(map[uint64][]v1.InMemoryProfile) 67 for p, files := range s.files { 68 for _, f := range files { 69 s.writeProfileFromFile(uint64(p), f) 70 } 71 } 72 } 73 74 func (s *memSuite) writeProfileFromFile(p uint64, f string) { 75 x, err := pprof.OpenFile(f) 76 require.NoError(s.t, err) 77 s.profiles[p] = x.CloneVT() 78 x.Normalize() 79 w := s.db.PartitionWriter(p) 80 s.indexed[p] = w.WriteProfileSymbols(x.Profile) 81 } 82 83 func (s *blockSuite) flush() { 84 require.NoError(s.t, s.db.Flush()) 85 b, err := filesystem.NewBucket(s.config.Dir, func(x objstore.Bucket) (objstore.Bucket, error) { 86 s.Bucket = x 87 return &s.testBucket, nil 88 }) 89 require.NoError(s.t, err) 90 s.reader, err = Open(context.Background(), b, &block.Meta{Files: s.db.Files()}) 91 require.NoError(s.t, err) 92 } 93 94 func (s *blockSuite) teardown() { 95 require.NoError(s.t, s.reader.Close()) 96 } 97 98 type testBucket struct { 99 getRangeCount atomic.Int64 100 getRangeSize atomic.Int64 101 objstore.Bucket 102 } 103 104 func (b *testBucket) GetRange(ctx context.Context, name string, off, length int64) (io.ReadCloser, error) { 105 b.getRangeCount.Add(1) 106 b.getRangeSize.Add(length) 107 return b.Bucket.GetRange(ctx, name, off, length) 108 } 109 110 func newTestFileWriter(w io.Writer) *writerOffset { 111 return &writerOffset{Writer: w} 112 } 113 114 //nolint:unparam 115 func pprofFingerprint(p *googlev1.Profile, typ int) [][2]uint64 { 116 m := make(map[uint64]uint64, len(p.Sample)) 117 h := xxhash.New() 118 for _, s := range p.Sample { 119 v := uint64(s.Value[typ]) 120 if v == 0 { 121 continue 122 } 123 h.Reset() 124 for _, loc := range s.LocationId { 125 for _, line := range p.Location[loc-1].Line { 126 f := p.Function[line.FunctionId-1] 127 _, _ = h.WriteString(p.StringTable[f.Name]) 128 } 129 } 130 m[h.Sum64()] += v 131 } 132 s := make([][2]uint64, 0, len(p.Sample)) 133 for k, v := range m { 134 s = append(s, [2]uint64{k, v}) 135 } 136 sort.Slice(s, func(i, j int) bool { return s[i][0] < s[j][0] }) 137 return s 138 } 139 140 func treeFingerprint(t *phlaremodel.Tree) [][2]uint64 { 141 m := make([][2]uint64, 0, 1<<10) 142 h := xxhash.New() 143 t.IterateStacks(func(_ string, self int64, stack []string) { 144 h.Reset() 145 for _, loc := range stack { 146 _, _ = h.WriteString(loc) 147 } 148 m = append(m, [2]uint64{h.Sum64(), uint64(self)}) 149 }) 150 sort.Slice(m, func(i, j int) bool { return m[i][0] < m[j][0] }) 151 return m 152 } 153 154 func Test_Stats(t *testing.T) { 155 s := memSuite{ 156 t: t, 157 files: [][]string{{"testdata/profile.pb.gz"}}, 158 config: &Config{ 159 Dir: t.TempDir(), 160 Stacktraces: StacktracesConfig{ 161 MaxNodesPerChunk: 4 << 20, 162 }, 163 Parquet: ParquetConfig{ 164 MaxBufferRowCount: 100 << 10, 165 }, 166 }, 167 } 168 169 s.init() 170 bs := blockSuite{memSuite: &s} 171 bs.flush() 172 defer bs.teardown() 173 174 p, err := bs.reader.Partition(context.Background(), 0) 175 require.NoError(t, err) 176 177 var actual PartitionStats 178 p.WriteStats(&actual) 179 expected := PartitionStats{ 180 StacktracesTotal: 561, 181 MaxStacktraceID: 1713, 182 LocationsTotal: 718, 183 MappingsTotal: 3, 184 FunctionsTotal: 506, 185 StringsTotal: 699, 186 } 187 require.Equal(t, expected, actual) 188 } 189 190 func TestWritePartition(t *testing.T) { 191 p := NewPartitionWriter(0, &Config{ 192 Version: FormatV3, 193 Stacktraces: StacktracesConfig{ 194 MaxNodesPerChunk: 4 << 20, 195 }, 196 Parquet: ParquetConfig{ 197 MaxBufferRowCount: 100 << 10, 198 }, 199 }) 200 profile := pprofth.NewProfileBuilder(time.Now().UnixNano()). 201 CPUProfile(). 202 WithLabels(phlaremodel.LabelNameServiceName, "svc"). 203 ForStacktraceString("foo", "bar"). 204 AddSamples(1). 205 ForStacktraceString("qwe", "foo", "bar"). 206 AddSamples(2) 207 208 profiles := p.WriteProfileSymbols(profile.Profile) 209 symdbBlob := bytes.NewBuffer(nil) 210 err := WritePartition(p, symdbBlob) 211 require.NoError(t, err) 212 213 bucket := phlareobj.NewBucket(memory.NewInMemBucket()) 214 require.NoError(t, bucket.Upload(context.Background(), DefaultFileName, bytes.NewReader(symdbBlob.Bytes()))) 215 reader, err := Open(context.Background(), bucket, testBlockMeta) 216 require.NoError(t, err) 217 218 r := NewResolver(context.Background(), reader) 219 defer r.Release() 220 r.AddSamples(0, profiles[0].Samples) 221 resolved, err := r.Tree() 222 require.NoError(t, err) 223 expected := `. 224 └── bar: self 0 total 3 225 └── foo: self 1 total 3 226 └── qwe: self 2 total 2 227 ` 228 require.Equal(t, expected, resolved.String()) 229 } 230 231 func BenchmarkPartitionWriter_WriteProfileSymbols(b *testing.B) { 232 b.ReportAllocs() 233 234 p, err := pprof.OpenFile("testdata/profile.pb.gz") 235 require.NoError(b, err) 236 p.Normalize() 237 cfg := DefaultConfig().WithDirectory(b.TempDir()) 238 239 db := NewSymDB(cfg) 240 241 for i := 0; i < b.N; i++ { 242 b.StopTimer() 243 newP := p.CloneVT() 244 pw := db.PartitionWriter(uint64(i)) 245 b.StartTimer() 246 247 pw.WriteProfileSymbols(newP) 248 } 249 }