github.com/grafana/pyroscope@v1.18.0/pkg/pprof/fix_go_truncated.go (about)

     1  package pprof
     2  
     3  import (
     4  	"bytes"
     5  	"slices"
     6  	"sort"
     7  	"unsafe"
     8  
     9  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    10  )
    11  
    12  const (
    13  	minGroupSize      = 2
    14  	maxRecursiveDepth = 56 // Profiles with deeply recursive stack traces are ignored.
    15  
    16  	tokens    = 8
    17  	tokenLen  = 16
    18  	suffixLen = tokens + tokenLen // stacktraces shorter than suffixLen are not considered as truncated or missing stacks copied from
    19  
    20  	tokenBytesLen  = tokenLen * 8
    21  	suffixBytesLen = suffixLen * 8
    22  )
    23  
    24  // PotentialTruncatedGoStacktraces reports whether there are
    25  // any chances that the profile may have truncated stack traces.
    26  func PotentialTruncatedGoStacktraces(p *profilev1.Profile) bool {
    27  	var minDepth int
    28  	var maxDepth int
    29  
    30  	if hasGoHeapSampleTypes(p) {
    31  		minDepth = 32
    32  
    33  		// Go heap profiles in Go 1.23+ have a depth limit of 128 frames. Let's not try to fix truncation if we see any longer stacks.
    34  		maxDepth = 33
    35  	} else if hasGoCPUSampleTypes(p) {
    36  		minDepth = 64
    37  	} else {
    38  		return false
    39  	}
    40  
    41  	// Some truncated heap stacks have depth less than the depth limit.
    42  	// https://github.com/golang/go/blob/f7c330eac7777612574d8a1652fd415391f6095e/src/runtime/mprof.go#L446
    43  	minDepth -= 4
    44  
    45  	deepEnough := false
    46  	for _, s := range p.Sample {
    47  		if len(s.LocationId) >= minDepth {
    48  			deepEnough = true
    49  		}
    50  		// when it's too deep we no longer perform reassemlbing of truncated stacktraces
    51  		if maxDepth != 0 && len(s.LocationId) >= maxDepth {
    52  			return false
    53  		}
    54  	}
    55  	return deepEnough
    56  }
    57  
    58  func hasGoHeapSampleTypes(p *profilev1.Profile) bool {
    59  	for _, st := range p.SampleType {
    60  		switch p.StringTable[st.Type] {
    61  		case
    62  			"alloc_objects",
    63  			"alloc_space",
    64  			"inuse_objects",
    65  			"inuse_space":
    66  			return true
    67  		}
    68  	}
    69  	return false
    70  }
    71  
    72  func hasGoCPUSampleTypes(p *profilev1.Profile) bool {
    73  	for _, st := range p.SampleType {
    74  		switch p.StringTable[st.Type] {
    75  		case
    76  			"cpu":
    77  			return true
    78  		}
    79  	}
    80  	return false
    81  }
    82  
    83  // RepairGoTruncatedStacktraces repairs truncated stack traces
    84  // in Go heap profiles.
    85  //
    86  // Go heap profile has a depth limit of 32 frames, which often
    87  // renders profiles unreadable, and also increases cardinality
    88  // of stack traces.
    89  //
    90  // The function guesses truncated frames based on neighbors and
    91  // repairs stack traces if there are high chances that this
    92  // part is present in the profile. The heuristic is as follows:
    93  //
    94  // For each stack trace S taller than 24 frames: if there is another
    95  // stack trace R taller than 24 frames that overlaps with the given
    96  // one by at least 16 frames in a row from the top, and has frames
    97  // above its root, stack S considered truncated, and the missing part
    98  // is copied from R.
    99  func RepairGoTruncatedStacktraces(p *profilev1.Profile) {
   100  	// Group stack traces by bottom (closest to the root) locations.
   101  	// Typically, there are very few groups (a hundred or two).
   102  	samples, groups := split(p)
   103  	// Each group's suffix is then tokenized: each part is shifted by one
   104  	// location from the previous one (like n-grams).
   105  	// Tokens are written into the token=>group map, Where the value is the
   106  	// index of the group with the token found at the furthest position from
   107  	// the root (across all groups).
   108  	m := make(map[string]group, len(groups)/2)
   109  	for i := 0; i < len(groups); i++ {
   110  		g := groups[i]
   111  		n := len(groups)
   112  		if i+1 < len(groups) {
   113  			n = groups[i+1]
   114  		}
   115  		if s := n - g; s < (minGroupSize - 1) {
   116  			continue
   117  		}
   118  		// We take suffix of the first sample in the group.
   119  		s := suffix(samples[g].LocationId)
   120  		// Tokenize the suffix: token position is relative to the stack
   121  		// trace root: 0 means that the token is the closest to the root.
   122  		//    TODO: unroll?
   123  		//    0 : 64 : 192 // No need.
   124  		//    1 : 56 : 184
   125  		//    2 : 48 : 176
   126  		//    3 : 40 : 168
   127  		//    4 : 32 : 160
   128  		//    5 : 24 : 152
   129  		//    6 : 16 : 144
   130  		//    7 :  8 : 136
   131  		//    8 :  0 : 128
   132  		//
   133  		// We skip the top/right-most token, as it is not needed,
   134  		// because there can be no more complete stack trace.
   135  		for j := uint32(1); j <= tokens; j++ {
   136  			hi := suffixBytesLen - j*tokens
   137  			lo := hi - tokenBytesLen
   138  			// By taking a string representation of the slice,
   139  			// we eliminate the need to hash the token explicitly:
   140  			// Go map will do it this way or another.
   141  			k := unsafeString(s[lo:hi])
   142  			// Current candidate: the group where the token is
   143  			// located at the furthest position from the root.
   144  			c, ok := m[k]
   145  			if !ok || j > c.off {
   146  				// This group has more complete stack traces:
   147  				m[k] = group{
   148  					// gid 0 is reserved as a sentinel value.
   149  					gid: uint32(i + 1),
   150  					off: j,
   151  				}
   152  			}
   153  		}
   154  	}
   155  
   156  	// Now we handle chaining. Consider the following stacks:
   157  	//  1   2   3   4
   158  	//  a   b  [d] (f)
   159  	//  b   c  [e] (g)
   160  	//  c  [d] (f)  h
   161  	//  d  [e] (g)  i
   162  	//
   163  	// We can't associate 3-rd stack with the 1-st one because their tokens
   164  	// do not overlap (given the token size is 2). However, we can associate
   165  	// it transitively through the 2nd stack.
   166  	//
   167  	// Dependencies:
   168  	//  - group i depends on d[i].
   169  	//  - d[i] depends on d[d[i].gid-1].
   170  	d := make([]group, len(groups))
   171  	for i := 0; i < len(groups); i++ {
   172  		g := groups[i]
   173  		t := topToken(samples[g].LocationId)
   174  		k := unsafeString(t)
   175  		c, ok := m[k]
   176  		if !ok || c.gid-1 == uint32(i) {
   177  			// The current group has the most complete stack trace.
   178  			continue
   179  		}
   180  		d[i] = c
   181  	}
   182  
   183  	// Then, for each group, we test, if there is another group with a more
   184  	// complete suffix, overlapping with the given one by at least one token.
   185  	// If such stack trace exists, all stack traces of the group are appended
   186  	// with the missing part.
   187  	for i := 0; i < len(groups); i++ {
   188  		g := groups[i]
   189  		c := d[i]
   190  		var off uint32
   191  		var j int
   192  		for c.gid > 0 && c.off > 0 {
   193  			off += c.off
   194  			n := d[c.gid-1]
   195  			if n.gid == 0 || c.off == 0 {
   196  				// Stop early to preserve c.
   197  				break
   198  			}
   199  			c = n
   200  			j++
   201  			if j == maxRecursiveDepth {
   202  				return
   203  			}
   204  		}
   205  		if off == 0 {
   206  			// The current group has the most complete stack trace.
   207  			continue
   208  		}
   209  		// The reference stack trace.
   210  		appx := samples[groups[c.gid-1]].LocationId
   211  		// It's possible that the reference stack trace does not
   212  		// include the part we're looking for. In this case, we
   213  		// simply ignore the group. Although it's possible to infer
   214  		// this piece from other stacks, this is left for further
   215  		// improvements.
   216  		if int(off) >= len(appx) {
   217  			continue
   218  		}
   219  		appx = appx[uint32(len(appx))-off:]
   220  		// Now we append the missing part to all stack traces of the group.
   221  		n := len(groups)
   222  		if i+1 < len(groups) {
   223  			n = groups[i+1]
   224  		}
   225  		for j := g; j < n; j++ {
   226  			// Locations typically already have some extra capacity,
   227  			// therefore no major allocations are expected here.
   228  			samples[j].LocationId = append(samples[j].LocationId, appx...)
   229  		}
   230  	}
   231  }
   232  
   233  type group struct {
   234  	gid uint32
   235  	off uint32
   236  }
   237  
   238  // suffix returns the last suffixLen locations
   239  // of the given stack trace represented as bytes.
   240  // The return slice is always suffixBytesLen long.
   241  // function panics if s is shorter than suffixLen.
   242  func suffix(s []uint64) []byte {
   243  	return locBytes(s[len(s)-suffixLen:])
   244  }
   245  
   246  // topToken returns the last tokenLen locations
   247  // of the given stack trace represented as bytes.
   248  // The return slice is always tokenBytesLen long.
   249  // function panics if s is shorter than tokenLen.
   250  func topToken(s []uint64) []byte {
   251  	return locBytes(s[len(s)-tokenLen:])
   252  }
   253  
   254  func locBytes(s []uint64) []byte {
   255  	p := (*byte)(unsafe.Pointer(&s[0]))
   256  	return unsafe.Slice(p, len(s)*8)
   257  }
   258  
   259  func unsafeString(b []byte) string {
   260  	return *(*string)(unsafe.Pointer(&b))
   261  }
   262  
   263  // split into groups of samples by stack trace suffixes.
   264  // Return slice contains indices of the first sample
   265  // of each group in the collection of selected samples.
   266  func split(p *profilev1.Profile) ([]*profilev1.Sample, []int) {
   267  	slices.SortFunc(p.Sample, func(a, b *profilev1.Sample) int {
   268  		if len(a.LocationId) < suffixLen {
   269  			return -1
   270  		}
   271  		if len(b.LocationId) < suffixLen {
   272  			return 1
   273  		}
   274  		return bytes.Compare(
   275  			suffix(a.LocationId),
   276  			suffix(b.LocationId),
   277  		)
   278  	})
   279  	o := sort.Search(len(p.Sample), func(i int) bool {
   280  		return len(p.Sample[i].LocationId) >= suffixLen
   281  	})
   282  	if o == len(p.Sample) {
   283  		return nil, nil
   284  	}
   285  	samples := p.Sample[o:]
   286  	const avgGroupSize = 16 // Estimate.
   287  	groups := make([]int, 0, len(samples)/avgGroupSize)
   288  	var prev []byte
   289  	for i := 0; i < len(samples); i++ {
   290  		cur := suffix(samples[i].LocationId)
   291  		if !bytes.Equal(cur, prev) {
   292  			groups = append(groups, i)
   293  			prev = cur
   294  		}
   295  	}
   296  	return samples, groups
   297  }