github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/symdb/resolver.go (about)

     1  package symdb
     2  
     3  import (
     4  	"context"
     5  	"runtime"
     6  	"sync"
     7  
     8  	"github.com/opentracing/opentracing-go"
     9  	"github.com/parquet-go/parquet-go"
    10  	"golang.org/x/sync/errgroup"
    11  
    12  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    13  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    14  	"github.com/grafana/pyroscope/pkg/model"
    15  	schemav1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
    16  	"github.com/grafana/pyroscope/pkg/pprof"
    17  	"github.com/grafana/pyroscope/pkg/util"
    18  )
    19  
    20  // Resolver converts stack trace samples to one of the profile
    21  // formats, such as tree or pprof.
    22  //
    23  // Resolver asynchronously loads symbols for each partition as
    24  // they are added with AddSamples or Partition calls.
    25  //
    26  // A new Resolver must be created for each profile.
    27  type Resolver struct {
    28  	ctx    context.Context
    29  	cancel context.CancelFunc
    30  	span   opentracing.Span
    31  
    32  	s SymbolsReader
    33  	g *errgroup.Group
    34  	c int
    35  	m sync.RWMutex
    36  	p map[uint64]*lazyPartition
    37  
    38  	maxNodes        int64
    39  	sts             *typesv1.StackTraceSelector
    40  	sanitizeOnMerge bool
    41  }
    42  
    43  type ResolverOption func(*Resolver)
    44  
    45  // WithResolverMaxConcurrent specifies how many partitions
    46  // can be resolved concurrently.
    47  func WithResolverMaxConcurrent(n int) ResolverOption {
    48  	return func(r *Resolver) {
    49  		r.c = n
    50  	}
    51  }
    52  
    53  // WithResolverMaxNodes specifies the desired maximum number
    54  // of nodes the resulting profile should include.
    55  func WithResolverMaxNodes(n int64) ResolverOption {
    56  	return func(r *Resolver) {
    57  		r.maxNodes = n
    58  	}
    59  }
    60  
    61  // WithResolverStackTraceSelector specifies the stack trace selector.
    62  // Only stack traces that belong to the callSite (have the prefix provided)
    63  // will be selected. If empty, the filter is ignored.
    64  // Subtree root location is the last element.
    65  func WithResolverStackTraceSelector(sts *typesv1.StackTraceSelector) ResolverOption {
    66  	return func(r *Resolver) {
    67  		r.sts = sts
    68  	}
    69  }
    70  
    71  func WithResolverSanitizeOnMerge(sanitizeOnMerge bool) ResolverOption {
    72  	return func(r *Resolver) {
    73  		r.sanitizeOnMerge = sanitizeOnMerge
    74  	}
    75  }
    76  
    77  type lazyPartition struct {
    78  	id uint64
    79  
    80  	m       sync.Mutex
    81  	samples *SampleAppender
    82  
    83  	fetchOnce sync.Once
    84  	resolver  *Resolver
    85  	reader    PartitionReader
    86  	selection *SelectedStackTraces
    87  	err       error
    88  }
    89  
    90  func (p *lazyPartition) fetch(ctx context.Context) error {
    91  	p.fetchOnce.Do(func() {
    92  		p.reader, p.err = p.resolver.s.Partition(ctx, p.id)
    93  		if p.err == nil && p.resolver.sts != nil {
    94  			p.selection = SelectStackTraces(p.reader.Symbols(), p.resolver.sts)
    95  		}
    96  	})
    97  	return p.err
    98  }
    99  
   100  func NewResolver(ctx context.Context, s SymbolsReader, opts ...ResolverOption) *Resolver {
   101  	r := Resolver{
   102  		s: s,
   103  		c: runtime.GOMAXPROCS(-1),
   104  		p: make(map[uint64]*lazyPartition),
   105  	}
   106  	for _, opt := range opts {
   107  		opt(&r)
   108  	}
   109  	r.span, r.ctx = opentracing.StartSpanFromContext(ctx, "NewResolver")
   110  	r.ctx, r.cancel = context.WithCancel(r.ctx)
   111  	r.g, r.ctx = errgroup.WithContext(r.ctx)
   112  	return &r
   113  }
   114  
   115  func (r *Resolver) Release() {
   116  	r.cancel()
   117  	// Wait for all partitions to be fetched / canceled.
   118  	if err := r.g.Wait(); err != nil {
   119  		r.span.SetTag("error", err)
   120  	}
   121  	// Release acquired partition readers.
   122  	var wg sync.WaitGroup
   123  	for _, p := range r.p {
   124  		wg.Add(1)
   125  		p := p
   126  		go func() {
   127  			defer wg.Done()
   128  			if p.reader != nil {
   129  				p.reader.Release()
   130  			}
   131  		}()
   132  	}
   133  	wg.Wait()
   134  	r.span.Finish()
   135  }
   136  
   137  // AddSamples adds a collection of stack trace samples to the resolver.
   138  // Samples can be added to partitions concurrently.
   139  func (r *Resolver) AddSamples(partition uint64, s schemav1.Samples) {
   140  	r.withPartitionSamples(partition, func(samples *SampleAppender) {
   141  		samples.AppendMany(s.StacktraceIDs, s.Values)
   142  	})
   143  }
   144  
   145  func (r *Resolver) AddSamplesWithSpanSelector(partition uint64, s schemav1.Samples, spanSelector model.SpanSelector) {
   146  	r.withPartitionSamples(partition, func(samples *SampleAppender) {
   147  		for i, sid := range s.StacktraceIDs {
   148  			if _, ok := spanSelector[s.Spans[i]]; ok && sid > 0 {
   149  				samples.Append(sid, s.Values[i])
   150  			}
   151  		}
   152  	})
   153  }
   154  
   155  func (r *Resolver) AddSamplesFromParquetRow(partition uint64, stacktraceIDs, values []parquet.Value) {
   156  	r.withPartitionSamples(partition, func(samples *SampleAppender) {
   157  		for i, sid := range stacktraceIDs {
   158  			if s := sid.Uint32(); s > 0 {
   159  				samples.Append(s, values[i].Uint64())
   160  			}
   161  		}
   162  	})
   163  }
   164  
   165  func (r *Resolver) AddSamplesWithSpanSelectorFromParquetRow(partition uint64, stacktraces, values, spans []parquet.Value, spanSelector model.SpanSelector) {
   166  	r.withPartitionSamples(partition, func(samples *SampleAppender) {
   167  		for i, sid := range stacktraces {
   168  			spanID := spans[i].Uint64()
   169  			stackID := sid.Uint32()
   170  			if spanID == 0 || stackID == 0 {
   171  				continue
   172  			}
   173  			if _, ok := spanSelector[spanID]; ok {
   174  				samples.Append(stackID, values[i].Uint64())
   175  			}
   176  		}
   177  	})
   178  }
   179  
   180  func (r *Resolver) withPartitionSamples(partition uint64, fn func(*SampleAppender)) {
   181  	p := r.partition(partition)
   182  	p.m.Lock()
   183  	defer p.m.Unlock()
   184  	fn(p.samples)
   185  }
   186  
   187  func (r *Resolver) CallSiteValues(values *CallSiteValues, partition uint64, samples schemav1.Samples) error {
   188  	p := r.partition(partition)
   189  	if err := p.fetch(r.ctx); err != nil {
   190  		return err
   191  	}
   192  	p.m.Lock()
   193  	defer p.m.Unlock()
   194  	p.selection.CallSiteValues(values, samples)
   195  	return nil
   196  }
   197  
   198  func (r *Resolver) CallSiteValuesParquet(values *CallSiteValues, partition uint64, stacktraceID, value []parquet.Value) error {
   199  	p := r.partition(partition)
   200  	if err := p.fetch(r.ctx); err != nil {
   201  		return err
   202  	}
   203  	p.m.Lock()
   204  	defer p.m.Unlock()
   205  	p.selection.CallSiteValuesParquet(values, stacktraceID, value)
   206  	return nil
   207  }
   208  
   209  func (r *Resolver) partition(partition uint64) *lazyPartition {
   210  	r.m.RLock()
   211  	p, ok := r.p[partition]
   212  	if ok {
   213  		r.m.RUnlock()
   214  		return p
   215  	}
   216  	r.m.RUnlock()
   217  	r.m.Lock()
   218  	p, ok = r.p[partition]
   219  	if ok {
   220  		r.m.Unlock()
   221  		return p
   222  	}
   223  	p = &lazyPartition{
   224  		id:       partition,
   225  		samples:  NewSampleAppender(),
   226  		resolver: r,
   227  	}
   228  	r.p[partition] = p
   229  	r.m.Unlock()
   230  	// Fetch partition in the background, not blocking the caller.
   231  	// p.reader must be accessed only after p.fetch returns.
   232  	r.g.Go(util.RecoverPanic(func() error {
   233  		return p.fetch(r.ctx)
   234  	}))
   235  	// r.g.Wait() is called at Resolver.Release.
   236  	return p
   237  }
   238  
   239  func (r *Resolver) Tree() (*model.Tree, error) {
   240  	span, ctx := opentracing.StartSpanFromContext(r.ctx, "Resolver.Tree")
   241  	defer span.Finish()
   242  	var lock sync.Mutex
   243  	tree := new(model.Tree)
   244  	err := r.withSymbols(ctx, func(symbols *Symbols, appender *SampleAppender) error {
   245  		resolved, err := symbols.Tree(ctx, appender, r.maxNodes, SelectStackTraces(symbols, r.sts))
   246  		if err != nil {
   247  			return err
   248  		}
   249  		lock.Lock()
   250  		tree.Merge(resolved)
   251  		lock.Unlock()
   252  		return nil
   253  	})
   254  	return tree, err
   255  }
   256  
   257  func (r *Resolver) Pprof() (*googlev1.Profile, error) {
   258  	span, ctx := opentracing.StartSpanFromContext(r.ctx, "Resolver.Pprof")
   259  	defer span.Finish()
   260  
   261  	if r.canSkipProfileMerge() {
   262  		// this is the same as the block below, without the profile merge
   263  		var p *googlev1.Profile
   264  		err := r.withSymbols(ctx, func(symbols *Symbols, appender *SampleAppender) error {
   265  			resolved, err := symbols.Pprof(ctx, appender, r.maxNodes, SelectStackTraces(symbols, r.sts))
   266  			if err != nil {
   267  				return err
   268  			}
   269  			p = resolved
   270  			return nil
   271  		})
   272  		if err != nil {
   273  			return nil, err
   274  		}
   275  		if p == nil { // for consistency with the return value when using the merge path
   276  			return &googlev1.Profile{
   277  				SampleType:  []*googlev1.ValueType{new(googlev1.ValueType)},
   278  				PeriodType:  new(googlev1.ValueType),
   279  				StringTable: []string{""},
   280  			}, nil
   281  		}
   282  		return p, nil
   283  	}
   284  
   285  	var p pprof.ProfileMerge
   286  	err := r.withSymbols(ctx, func(symbols *Symbols, appender *SampleAppender) error {
   287  		resolved, err := symbols.Pprof(ctx, appender, r.maxNodes, SelectStackTraces(symbols, r.sts))
   288  		if err != nil {
   289  			return err
   290  		}
   291  		return p.Merge(resolved, r.sanitizeOnMerge)
   292  	})
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  	return p.Profile(), nil
   297  }
   298  
   299  func (r *Resolver) withSymbols(ctx context.Context, fn func(*Symbols, *SampleAppender) error) error {
   300  	g, _ := errgroup.WithContext(ctx)
   301  	g.SetLimit(r.c)
   302  	for _, p := range r.p {
   303  		p := p
   304  		g.Go(util.RecoverPanic(func() error {
   305  			if err := p.fetch(ctx); err != nil {
   306  				return err
   307  			}
   308  			return fn(p.reader.Symbols(), p.samples)
   309  		}))
   310  	}
   311  	return g.Wait()
   312  }
   313  
   314  func (r *Resolver) canSkipProfileMerge() bool {
   315  	if len(r.p) > 1 {
   316  		return false
   317  	}
   318  	if r.sts != nil && r.sts.GoPgo != nil && r.sts.GoPgo.AggregateCallees {
   319  		// we rely on merges to implement GoPgo.AggregateCallees
   320  		return false
   321  	}
   322  
   323  	return true
   324  }
   325  
   326  func (r *Symbols) Pprof(
   327  	ctx context.Context,
   328  	appender *SampleAppender,
   329  	maxNodes int64,
   330  	selection *SelectedStackTraces,
   331  ) (*googlev1.Profile, error) {
   332  	return buildPprof(ctx, r, appender.Samples(), maxNodes, selection)
   333  }
   334  
   335  func (r *Symbols) Tree(
   336  	ctx context.Context,
   337  	appender *SampleAppender,
   338  	maxNodes int64,
   339  	selection *SelectedStackTraces,
   340  ) (*model.Tree, error) {
   341  	return buildTree(ctx, r, appender, maxNodes, selection)
   342  }