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 }