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 }