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  }