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

     1  package symdb
     2  
     3  import (
     4  	"unsafe"
     5  
     6  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
     7  	"github.com/grafana/pyroscope/pkg/model"
     8  	schemav1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
     9  	"github.com/grafana/pyroscope/pkg/slices"
    10  )
    11  
    12  const (
    13  	truncationMark    = 1 << 30
    14  	truncatedNodeName = "other"
    15  )
    16  
    17  type pprofTree struct {
    18  	symbols *Symbols
    19  	samples *schemav1.Samples
    20  	profile googlev1.Profile
    21  	lut     []uint32
    22  	cur     int
    23  
    24  	maxNodes  int64
    25  	truncated int
    26  
    27  	functionTree *model.StacktraceTree
    28  	stacktraces  []truncatedStacktraceSample
    29  	// Two buffers are needed as we handle both function and location
    30  	// stacks simultaneously.
    31  	functionsBuf []int32
    32  	locationsBuf []uint64
    33  
    34  	selection *SelectedStackTraces
    35  	fnNames   func(locations []int32) ([]int32, bool)
    36  
    37  	// After truncation many samples will have the same stack trace.
    38  	// The map is used to deduplicate them. The key is sample.LocationId
    39  	// slice turned into a string: the underlying memory must not change.
    40  	sampleMap map[string]*googlev1.Sample
    41  	// As an optimisation, we merge all the stack trace samples that were
    42  	// fully truncated to a single sample.
    43  	fullyTruncated int64
    44  }
    45  
    46  type truncatedStacktraceSample struct {
    47  	stacktraceID    uint32
    48  	functionNodeIdx int32
    49  	value           int64
    50  }
    51  
    52  func (r *pprofTree) init(symbols *Symbols, samples schemav1.Samples) {
    53  	r.symbols = symbols
    54  	r.samples = &samples
    55  	// We optimistically assume that each stacktrace has only
    56  	// 2 unique nodes. For pathological cases it may exceed 10.
    57  	r.functionTree = model.NewStacktraceTree(samples.Len() * 2)
    58  	r.stacktraces = make([]truncatedStacktraceSample, 0, samples.Len())
    59  	r.sampleMap = make(map[string]*googlev1.Sample, samples.Len())
    60  	if r.selection != nil && len(r.selection.callSite) > 0 {
    61  		r.fnNames = r.locFunctionsFiltered
    62  	} else {
    63  		r.fnNames = r.locFunctions
    64  	}
    65  }
    66  
    67  func (r *pprofTree) InsertStacktrace(stacktraceID uint32, locations []int32) {
    68  	value := int64(r.samples.Values[r.cur])
    69  	r.cur++
    70  	functions, ok := r.fnNames(locations)
    71  	if ok {
    72  		functionNodeIdx := r.functionTree.Insert(functions, value)
    73  		r.stacktraces = append(r.stacktraces, truncatedStacktraceSample{
    74  			stacktraceID:    stacktraceID,
    75  			functionNodeIdx: functionNodeIdx,
    76  			value:           value,
    77  		})
    78  	}
    79  }
    80  
    81  func (r *pprofTree) locFunctions(locations []int32) ([]int32, bool) {
    82  	r.functionsBuf = r.functionsBuf[:0]
    83  	for i := 0; i < len(locations); i++ {
    84  		lines := r.symbols.Locations[locations[i]].Line
    85  		for j := 0; j < len(lines); j++ {
    86  			r.functionsBuf = append(r.functionsBuf, int32(lines[j].FunctionId))
    87  		}
    88  	}
    89  	return r.functionsBuf, true
    90  }
    91  
    92  func (r *pprofTree) locFunctionsFiltered(locations []int32) ([]int32, bool) {
    93  	r.functionsBuf = r.functionsBuf[:0]
    94  	var pos int
    95  	pathLen := int(r.selection.depth)
    96  	// Even if len(locations) < pathLen, we still
    97  	// need to inspect locations line by line.
    98  	for i := len(locations) - 1; i >= 0; i-- {
    99  		lines := r.symbols.Locations[locations[i]].Line
   100  		for j := len(lines) - 1; j >= 0; j-- {
   101  			f := lines[j].FunctionId
   102  			if pos < pathLen {
   103  				if r.selection.callSite[pos] != r.selection.funcNames[f] {
   104  					return nil, false
   105  				}
   106  				pos++
   107  			}
   108  			r.functionsBuf = append(r.functionsBuf, int32(f))
   109  		}
   110  	}
   111  	if pos < pathLen {
   112  		return nil, false
   113  	}
   114  	slices.Reverse(r.functionsBuf)
   115  	return r.functionsBuf, true
   116  }
   117  
   118  func (r *pprofTree) buildPprof() *googlev1.Profile {
   119  	r.markNodesForTruncation()
   120  	for _, n := range r.stacktraces {
   121  		r.addSample(n)
   122  	}
   123  	r.createSamples()
   124  	createSampleTypeStub(&r.profile)
   125  	copyLocations(&r.profile, r.symbols, r.lut)
   126  	copyFunctions(&r.profile, r.symbols, r.lut)
   127  	copyMappings(&r.profile, r.symbols, r.lut)
   128  	copyStrings(&r.profile, r.symbols, r.lut)
   129  	if r.truncated > 0 || r.fullyTruncated > 0 {
   130  		createLocationStub(&r.profile)
   131  	}
   132  	return &r.profile
   133  }
   134  
   135  func (r *pprofTree) markNodesForTruncation() {
   136  	minValue := r.functionTree.MinValue(r.maxNodes)
   137  	if minValue == 0 {
   138  		return
   139  	}
   140  	for i := range r.functionTree.Nodes {
   141  		if r.functionTree.Nodes[i].Total < minValue {
   142  			r.functionTree.Nodes[i].Location |= truncationMark
   143  			r.truncated++
   144  		}
   145  	}
   146  }
   147  
   148  func (r *pprofTree) addSample(n truncatedStacktraceSample) {
   149  	// Find the original stack trace and remove truncated
   150  	// locations based on the truncated functions.
   151  	var off int
   152  	r.functionsBuf, off = r.buildFunctionsStack(r.functionsBuf, n.functionNodeIdx)
   153  	if off < 0 {
   154  		// The stack has no functions without the truncation mark.
   155  		r.fullyTruncated += n.value
   156  		return
   157  	}
   158  	r.locationsBuf = r.symbols.Stacktraces.LookupLocations(r.locationsBuf, n.stacktraceID)
   159  	if off > 0 {
   160  		// Some functions were truncated.
   161  		r.locationsBuf = truncateLocations(r.locationsBuf, r.functionsBuf, off, r.symbols)
   162  		// Otherwise, if the offset is zero, the stack can be taken as is.
   163  	}
   164  	// Truncation may result in vast duplication of stack traces.
   165  	// Even if a particular stack trace is not truncated, we still
   166  	// remember it, as there might be another truncated stack trace
   167  	// that fully matches it.
   168  	// Note that this is safe to take locationsBuf memory for the
   169  	// map key lookup as it is not retained.
   170  	if s, dup := r.sampleMap[uint64sliceString(r.locationsBuf)]; dup {
   171  		s.Value[0] += n.value
   172  		return
   173  	}
   174  	// If this is a new stack trace, copy locations, create
   175  	// the sample, and add the stack trace to the map.
   176  	// TODO(kolesnikovae): Do not allocate new slices per sample.
   177  	//  Instead, pre-allocated slabs and reference samples from them.
   178  	locationsCopy := make([]uint64, len(r.locationsBuf))
   179  	copy(locationsCopy, r.locationsBuf)
   180  	s := &googlev1.Sample{LocationId: locationsCopy, Value: []int64{n.value}}
   181  	r.profile.Sample = append(r.profile.Sample, s)
   182  	r.sampleMap[uint64sliceString(locationsCopy)] = s
   183  }
   184  
   185  func (r *pprofTree) buildFunctionsStack(funcs []int32, idx int32) ([]int32, int) {
   186  	offset := -1
   187  	funcs = funcs[:0]
   188  	for i := idx; i > 0; i = r.functionTree.Nodes[i].Parent {
   189  		n := r.functionTree.Nodes[i]
   190  		if offset < 0 && n.Location&truncationMark == 0 {
   191  			// Remember the first node to keep.
   192  			offset = len(funcs)
   193  		}
   194  		funcs = append(funcs, n.Location&^truncationMark)
   195  	}
   196  	return funcs, offset
   197  }
   198  
   199  func (r *pprofTree) createSamples() {
   200  	samples := len(r.sampleMap)
   201  	r.profile.Sample = make([]*googlev1.Sample, samples, samples+1)
   202  	var i int
   203  	for _, s := range r.sampleMap {
   204  		r.profile.Sample[i] = s
   205  		i++
   206  	}
   207  	if r.fullyTruncated > 0 {
   208  		r.createStubSample()
   209  	}
   210  }
   211  
   212  func truncateLocations(locations []uint64, functions []int32, offset int, symbols *Symbols) []uint64 {
   213  	if offset < 1 {
   214  		return locations
   215  	}
   216  	f := len(functions)
   217  	l := len(locations)
   218  	for ; l > 0 && f >= offset; l-- {
   219  		location := symbols.Locations[locations[l-1]]
   220  		for j := len(location.Line) - 1; j >= 0; j-- {
   221  			f--
   222  		}
   223  	}
   224  	if l > 0 {
   225  		locations[0] = truncationMark
   226  		return append(locations[:1], locations[l:]...)
   227  	}
   228  	return locations[l:]
   229  }
   230  
   231  func uint64sliceString(u []uint64) string {
   232  	if len(u) == 0 {
   233  		return ""
   234  	}
   235  	p := (*byte)(unsafe.Pointer(&u[0]))
   236  	return unsafe.String(p, len(u)*8)
   237  }
   238  
   239  func (r *pprofTree) createStubSample() {
   240  	r.profile.Sample = append(r.profile.Sample, &googlev1.Sample{
   241  		LocationId: []uint64{truncationMark},
   242  		Value:      []int64{r.fullyTruncated},
   243  	})
   244  }
   245  
   246  func createLocationStub(profile *googlev1.Profile) {
   247  	var stubNodeNameIdx int64
   248  	for i, s := range profile.StringTable {
   249  		if s == truncatedNodeName {
   250  			stubNodeNameIdx = int64(i)
   251  			break
   252  		}
   253  	}
   254  	if stubNodeNameIdx == 0 {
   255  		stubNodeNameIdx = int64(len(profile.StringTable))
   256  		profile.StringTable = append(profile.StringTable, truncatedNodeName)
   257  	}
   258  	stubFn := &googlev1.Function{
   259  		Id:         uint64(len(profile.Function) + 1),
   260  		Name:       stubNodeNameIdx,
   261  		SystemName: stubNodeNameIdx,
   262  	}
   263  	profile.Function = append(profile.Function, stubFn)
   264  	// in the case there is no mapping, we need to create one
   265  	if len(profile.Mapping) == 0 {
   266  		profile.Mapping = append(profile.Mapping, &googlev1.Mapping{Id: 1})
   267  	}
   268  	stubLoc := &googlev1.Location{
   269  		Id:        uint64(len(profile.Location) + 1),
   270  		Line:      []*googlev1.Line{{FunctionId: stubFn.Id}},
   271  		MappingId: 1,
   272  	}
   273  	profile.Location = append(profile.Location, stubLoc)
   274  	for _, s := range profile.Sample {
   275  		for i, loc := range s.LocationId {
   276  			if loc == truncationMark {
   277  				s.LocationId[i] = stubLoc.Id
   278  			}
   279  		}
   280  	}
   281  }