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 }