github.com/grafana/pyroscope@v1.18.0/pkg/model/flamegraph.go (about) 1 package model 2 3 import ( 4 "sort" 5 "sync" 6 7 "github.com/samber/lo" 8 9 "github.com/grafana/pyroscope/pkg/og/storage/metadata" 10 "github.com/grafana/pyroscope/pkg/og/structs/flamebearer" 11 12 querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1" 13 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 14 ) 15 16 func NewFlameGraph(t *Tree, maxNodes int64) *querierv1.FlameGraph { 17 var total, max int64 18 for _, node := range t.root { 19 total += node.total 20 } 21 names := []string{} 22 nameLocationCache := map[string]int{} 23 res := []*Stack[int64]{} 24 defer func() { 25 for _, stack := range res { 26 stackIntPool.Put(stack) 27 } 28 }() 29 30 minVal := t.minValue(maxNodes) 31 32 stack := stackNodePool.Get().(*Stack[stackNode]) 33 defer stackNodePool.Put(stack) 34 stack.Reset() 35 stack.Push(stackNode{xOffset: 0, level: 0, node: &node{children: t.root, total: total}}) 36 37 for { 38 current, hasMoreNodes := stack.Pop() 39 if !hasMoreNodes { 40 break 41 } 42 if current.node.self > max { 43 max = current.node.self 44 } 45 var i int 46 var ok bool 47 name := current.node.name 48 if i, ok = nameLocationCache[name]; !ok { 49 i = len(names) 50 if i == 0 { 51 name = "total" 52 } 53 nameLocationCache[name] = i 54 names = append(names, name) 55 } 56 57 if current.level == len(res) { 58 s := stackIntPool.Get().(*Stack[int64]) 59 s.Reset() 60 res = append(res, s) 61 } 62 63 // i+0 = x offset 64 // i+1 = total 65 // i+2 = self 66 // i+3 = index in names array 67 level := res[current.level] 68 level.Push(int64(i)) 69 level.Push((current.node.self)) 70 level.Push((current.node.total)) 71 level.Push(int64(current.xOffset)) 72 current.xOffset += int(current.node.self) 73 74 otherTotal := int64(0) 75 for _, child := range current.node.children { 76 if child.total >= minVal && child.name != "other" { 77 stack.Push(stackNode{xOffset: current.xOffset, level: current.level + 1, node: child}) 78 current.xOffset += int(child.total) 79 } else { 80 otherTotal += child.total 81 } 82 } 83 if otherTotal != 0 { 84 child := &node{ 85 name: "other", 86 parent: current.node, 87 self: otherTotal, 88 total: otherTotal, 89 } 90 stack.Push(stackNode{xOffset: current.xOffset, level: current.level + 1, node: child}) 91 current.xOffset += int(child.total) 92 } 93 } 94 95 result := make([][]int64, len(res)) 96 for i := range result { 97 result[i] = res[i].Slice() 98 } 99 // delta encode xoffsets 100 for _, l := range result { 101 prev := int64(0) 102 for i := 0; i < len(l); i += 4 { 103 l[i] -= prev 104 prev += l[i] + l[i+1] 105 } 106 } 107 levels := make([]*querierv1.Level, len(result)) 108 for i := range levels { 109 levels[i] = &querierv1.Level{ 110 Values: result[i], 111 } 112 } 113 114 return &querierv1.FlameGraph{ 115 Names: names, 116 Levels: levels, 117 Total: total, 118 MaxSelf: max, 119 } 120 } 121 122 // ExportToFlamebearer exports the flamegraph to a Flamebearer struct. 123 func ExportToFlamebearer(fg *querierv1.FlameGraph, profileType *typesv1.ProfileType) *flamebearer.FlamebearerProfile { 124 if fg == nil { 125 fg = &querierv1.FlameGraph{} 126 } 127 unit := metadata.Units(profileType.SampleUnit) 128 sampleRate := uint32(100) 129 130 switch profileType.SampleType { 131 case "inuse_objects", "alloc_objects", "goroutine", "samples": 132 unit = metadata.ObjectsUnits 133 case "cpu": 134 unit = metadata.SamplesUnits 135 sampleRate = uint32(1_000_000_000) 136 137 } 138 levels := make([][]int, len(fg.Levels)) 139 for i := range levels { 140 levels[i] = lo.Map(fg.Levels[i].Values, func(v int64, i int) int { return int(v) }) 141 } 142 return &flamebearer.FlamebearerProfile{ 143 Version: 1, 144 FlamebearerProfileV1: flamebearer.FlamebearerProfileV1{ 145 Flamebearer: flamebearer.FlamebearerV1{ 146 Names: fg.Names, 147 NumTicks: int(fg.Total), 148 MaxSelf: int(fg.MaxSelf), 149 Levels: levels, 150 }, 151 Metadata: flamebearer.FlamebearerMetadataV1{ 152 Format: "single", 153 Units: unit, 154 Name: profileType.SampleType, 155 SampleRate: sampleRate, 156 }, 157 }, 158 } 159 } 160 161 func ExportDiffToFlamebearer(fg *querierv1.FlameGraphDiff, profileType *typesv1.ProfileType) *flamebearer.FlamebearerProfile { 162 // Since a normal flamegraph and a diff are so similar, convert it to reuse the export function 163 singleFlamegraph := &querierv1.FlameGraph{ 164 Names: fg.Names, 165 Levels: fg.Levels, 166 Total: fg.Total, 167 MaxSelf: fg.MaxSelf, 168 } 169 170 fb := ExportToFlamebearer(singleFlamegraph, profileType) 171 fb.LeftTicks = uint64(fg.LeftTicks) 172 fb.RightTicks = uint64(fg.RightTicks) 173 fb.Metadata.Format = "double" 174 175 return fb 176 } 177 178 type FlameGraphMerger struct { 179 mu sync.Mutex 180 t *Tree 181 } 182 183 func NewFlameGraphMerger() *FlameGraphMerger { 184 return &FlameGraphMerger{t: new(Tree)} 185 } 186 187 // MergeFlameGraph adds the flame graph stack traces to the resulting 188 // flame graph. The call is thread-safe, but the resulting flame graph 189 // or tree should be only accessed after all the samples are merged. 190 func (m *FlameGraphMerger) MergeFlameGraph(src *querierv1.FlameGraph) { 191 m.mu.Lock() 192 defer m.mu.Unlock() 193 deltaDecoding(src.Levels, 0, 4) 194 dst := make([]string, 0, len(src.Levels)) 195 for i, l := range src.Levels { 196 if i == 0 { 197 // Skip the root node ("total"). 198 continue 199 } 200 for j := 0; j < len(l.Values); j += 4 { 201 self := l.Values[j+2] 202 if self > 0 { 203 dst = buildStack(dst, src, i, j) 204 m.t.InsertStack(self, dst...) 205 } 206 } 207 } 208 } 209 210 func (m *FlameGraphMerger) MergeTreeBytes(src []byte) error { 211 t, err := UnmarshalTree(src) 212 if err != nil { 213 return err 214 } 215 m.mu.Lock() 216 defer m.mu.Unlock() 217 m.t.Merge(t) 218 return nil 219 } 220 221 func (m *FlameGraphMerger) Tree() *Tree { return m.t } 222 223 func (m *FlameGraphMerger) FlameGraph(maxNodes int64) *querierv1.FlameGraph { 224 t := m.t 225 if t == nil { 226 t = new(Tree) 227 } 228 return NewFlameGraph(t, maxNodes) 229 } 230 231 func deltaDecoding(levels []*querierv1.Level, start, step int) { 232 for _, l := range levels { 233 prev := int64(0) 234 for i := start; i < len(l.Values); i += step { 235 delta := l.Values[i] + l.Values[i+1] 236 l.Values[i] += prev 237 prev += delta 238 } 239 } 240 } 241 242 func buildStack(dst []string, f *querierv1.FlameGraph, level, idx int) []string { 243 if cap(dst) < level { 244 // Actually, it should never be the case, because 245 // we know the depth in advance and can allocate 246 // dst with the right capacity. 247 dst = make([]string, level, level*2) 248 } else { 249 dst = dst[:level] 250 } 251 dst[level-1] = f.Names[f.Levels[level].Values[idx+3]] 252 x := f.Levels[level].Values[idx] 253 for i := level - 1; i > 0; i-- { 254 j := sort.Search(len(f.Levels[i].Values)/4, func(j int) bool { return f.Levels[i].Values[j*4] > x }) - 1 255 dst[i-1] = f.Names[f.Levels[i].Values[j*4+3]] 256 x = f.Levels[i].Values[j*4] 257 } 258 return dst 259 }