github.com/grafana/pyroscope@v1.18.0/pkg/og/structs/flamebearer/flamebearer.go (about) 1 package flamebearer 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/grafana/pyroscope/pkg/og/storage/heatmap" 8 "github.com/grafana/pyroscope/pkg/og/storage/metadata" 9 "github.com/grafana/pyroscope/pkg/og/storage/segment" 10 "github.com/grafana/pyroscope/pkg/og/storage/tree" 11 ) 12 13 //revive:disable:max-public-structs Config structs 14 15 // swagger:model 16 // FlamebearerProfile is a versioned flambearer based profile. 17 // It's the native format both for rendering and file saving (in adhoc mode). 18 type FlamebearerProfile struct { 19 // Version of the data format. No version / version zero is an unformalized format. 20 Version uint `json:"version"` 21 FlamebearerProfileV1 22 Telemetry map[string]interface{} `json:"telemetry,omitempty"` 23 } 24 25 // swagger:model 26 // FlamebearerProfileV1 defines the v1 of the profile format 27 type FlamebearerProfileV1 struct { 28 // Flamebearer data. 29 // required: true 30 Flamebearer FlamebearerV1 `json:"flamebearer"` 31 // Metadata associated to the profile. 32 // required: true 33 Metadata FlamebearerMetadataV1 `json:"metadata"` 34 // Timeline associated to the profile, used for continuous profiling only. 35 Timeline *FlamebearerTimelineV1 `json:"timeline"` 36 Groups map[string]*FlamebearerTimelineV1 `json:"groups"` 37 Heatmap *Heatmap `json:"heatmap"` 38 // Number of samples in the left / base profile. Only used in "double" format. 39 LeftTicks uint64 `json:"leftTicks,omitempty"` 40 // Number of samples in the right / diff profile. Only used in "double" format. 41 RightTicks uint64 `json:"rightTicks,omitempty"` 42 } 43 44 // swagger:model 45 // FlamebearerV1 defines the actual profiling data. 46 type FlamebearerV1 struct { 47 // Names is the sequence of symbol names. 48 // required: true 49 Names []string `json:"names"` 50 // Levels contains the flamebearer nodes. Each level represents a row in the flamegraph. 51 // For each row / level, there's a sequence of values. These values are grouped in chunks 52 // which size depend on the flamebearer format: 4 for "single", 7 for "double". 53 // For "single" format, each chunk has the following data: 54 // i+0 = x offset (prefix sum of the level total values), delta encoded. 55 // i+1 = total samples (including the samples in its children nodes). 56 // i+2 = self samples (excluding the samples in its children nodes). 57 // i+3 = index in names array 58 // 59 // For "double" format, each chunk has the following data: 60 // i+0 = x offset (prefix sum of the level total values), delta encoded, base / left tree. 61 // i+1 = total samples (including the samples in its children nodes) , base / left tree. 62 // i+2 = self samples (excluding the samples in its children nodes) , base / left tree. 63 // i+3 = x offset (prefix sum of the level total values), delta encoded, diff / right tree. 64 // i+4 = total samples (including the samples in its children nodes) , diff / right tree. 65 // i+5 = self samples (excluding the samples in its children nodes) , diff / right tree. 66 // i+6 = index in the names array 67 // 68 // required: true 69 Levels [][]int `json:"levels"` 70 // Total number of samples. 71 // required: true 72 NumTicks int `json:"numTicks"` 73 // Maximum self value in any node. 74 // required: true 75 MaxSelf int `json:"maxSelf"` 76 } 77 78 type FlamebearerMetadataV1 struct { 79 // Data format. Supported values are "single" and "double" (diff). 80 // required: true 81 Format string `json:"format"` 82 // Name of the spy / profiler used to generate the profile, if any. 83 SpyName string `json:"spyName"` 84 // Sample rate at which the profiler was operating. 85 SampleRate uint32 `json:"sampleRate"` 86 // The unit of measurement for the profiled data. 87 Units metadata.Units `json:"units"` 88 // A name that identifies the profile. 89 Name string `json:"name"` 90 } 91 92 type FlamebearerTimelineV1 struct { 93 // Time at which the timeline starts, as a Unix timestamp. 94 // required: true 95 StartTime int64 `json:"startTime"` 96 // A sequence of samples starting at startTime, spaced by durationDelta seconds 97 // required: true 98 Samples []uint64 `json:"samples"` 99 // Time delta between samples, in seconds. 100 // required: true 101 DurationDelta int64 `json:"durationDelta"` 102 Watermarks map[int]int64 `json:"watermarks"` 103 } 104 105 type Heatmap struct { 106 // Values matrix contain values that indicate count of value occurrences, 107 // satisfying boundaries of X and Y bins: [StartTime:EndTime) and (MinValue:MaxValue]. 108 // A value can be accessed via Values[x][y], where: 109 // 0 <= x < TimeBuckets, and 110 // 0 <= y < ValueBuckets. 111 Values [][]uint64 `json:"values"` 112 // TimeBuckets denote number of bins on X axis. 113 // Length of Values array. 114 TimeBuckets int64 `json:"timeBuckets"` 115 // ValueBuckets denote number of bins on Y axis. 116 // Length of any item in the Values array. 117 ValueBuckets int64 `json:"valueBuckets"` 118 // StartTime and EndTime indicate boundaries of X axis: [StartTime:EndTime). 119 StartTime int64 `json:"startTime"` 120 EndTime int64 `json:"endTime"` 121 // MinValue and MaxValue indicate boundaries of Y axis: (MinValue:MaxValue]. 122 MinValue uint64 `json:"minValue"` 123 MaxValue uint64 `json:"maxValue"` 124 // MinDepth and MaxDepth indicate boundaries of Z axis: [MinDepth:MaxDepth]. 125 // MinDepth is the minimal non-zero value that can be found in Values. 126 MinDepth uint64 `json:"minDepth"` 127 MaxDepth uint64 `json:"maxDepth"` 128 } 129 130 type ProfileConfig struct { 131 Name string 132 MaxNodes int 133 Metadata metadata.Metadata 134 Tree *tree.Tree 135 Timeline *segment.Timeline 136 Heatmap *heatmap.Heatmap 137 Groups map[string]*segment.Timeline 138 Telemetry map[string]interface{} 139 } 140 141 func NewProfile(in ProfileConfig) FlamebearerProfile { 142 fb := in.Tree.FlamebearerStruct(in.MaxNodes) 143 return FlamebearerProfile{ 144 Version: 1, 145 Telemetry: in.Telemetry, 146 FlamebearerProfileV1: FlamebearerProfileV1{ 147 Flamebearer: newFlambearer(fb), 148 Metadata: newMetadata(in.Name, fb.Format, in.Metadata), 149 Timeline: newTimeline(in.Timeline), 150 Heatmap: newHeatmap(in.Heatmap), 151 Groups: convertGroups(in.Groups), 152 }, 153 } 154 } 155 156 func convertGroups(v map[string]*segment.Timeline) map[string]*FlamebearerTimelineV1 { 157 res := make(map[string]*FlamebearerTimelineV1) 158 for k, v := range v { 159 res[k] = newTimeline(v) 160 } 161 return res 162 } 163 164 func newFlambearer(fb *tree.Flamebearer) FlamebearerV1 { 165 return FlamebearerV1{ 166 Names: fb.Names, 167 Levels: fb.Levels, 168 NumTicks: fb.NumTicks, 169 MaxSelf: fb.MaxSelf, 170 } 171 } 172 173 func newMetadata(name string, format tree.Format, md metadata.Metadata) FlamebearerMetadataV1 { 174 return FlamebearerMetadataV1{ 175 Name: name, 176 Format: string(format), 177 SpyName: md.SpyName, 178 SampleRate: md.SampleRate, 179 Units: md.Units, 180 } 181 } 182 183 func newTimeline(timeline *segment.Timeline) *FlamebearerTimelineV1 { 184 if timeline == nil { 185 return nil 186 } 187 return &FlamebearerTimelineV1{ 188 StartTime: timeline.StartTime, 189 Samples: timeline.Samples, 190 DurationDelta: timeline.DurationDeltaNormalized, 191 Watermarks: timeline.Watermarks, 192 } 193 } 194 195 func newHeatmap(h *heatmap.Heatmap) *Heatmap { 196 if h == nil { 197 return nil 198 } 199 return &Heatmap{ 200 Values: h.Values, 201 TimeBuckets: h.TimeBuckets, 202 ValueBuckets: h.ValueBuckets, 203 StartTime: h.StartTime.UnixNano(), 204 EndTime: h.EndTime.UnixNano(), 205 MinValue: h.MinValue, 206 MaxValue: h.MaxValue, 207 MinDepth: h.MinDepth, 208 MaxDepth: h.MaxDepth, 209 } 210 } 211 212 func (fb FlamebearerProfile) Validate() error { 213 if fb.Version > 1 { 214 return fmt.Errorf("unsupported version %d", fb.Version) 215 } 216 return fb.FlamebearerProfileV1.Validate() 217 } 218 219 // Validate the V1 profile. 220 // A custom validation is used as the constraints are hard to define in a generic way 221 // (e.g. using https://github.com/go-playground/validator) 222 func (fb FlamebearerProfileV1) Validate() error { 223 format := tree.Format(fb.Metadata.Format) 224 if format != tree.FormatSingle && format != tree.FormatDouble { 225 return fmt.Errorf("unsupported format %s", format) 226 } 227 if len(fb.Flamebearer.Names) == 0 { 228 return fmt.Errorf("a profile must have at least one symbol name") 229 } 230 if len(fb.Flamebearer.Levels) == 0 { 231 return fmt.Errorf("a profile must have at least one profiling level") 232 } 233 var mod int 234 switch format { 235 case tree.FormatSingle: 236 mod = 4 237 case tree.FormatDouble: 238 mod = 7 239 default: // This shouldn't happen at this point. 240 return fmt.Errorf("unsupported format %s", format) 241 } 242 for _, l := range fb.Flamebearer.Levels { 243 if len(l)%mod != 0 { 244 return fmt.Errorf("a profile level should have a multiple of %d values, but there's a level with %d values", mod, len(l)) 245 } 246 for i := mod - 1; i < len(l); i += mod { 247 if l[i] >= len(fb.Flamebearer.Names) { 248 return fmt.Errorf("invalid name index %d, it should be smaller than %d", l[i], len(fb.Flamebearer.Levels)) 249 } 250 if l[i] < 0 { 251 return fmt.Errorf("invalid name index %d, it should be a non-negative value", l[i]) 252 } 253 } 254 } 255 return nil 256 } 257 258 // ProfileToTree converts a FlamebearerProfile into a Tree 259 // It currently only supports Single profiles 260 func ProfileToTree(fb FlamebearerProfile) (*tree.Tree, error) { 261 if fb.Metadata.Format != string(tree.FormatSingle) { 262 return nil, fmt.Errorf("unsupported flamebearer format %s", fb.Metadata.Format) 263 } 264 if fb.Version != 1 { 265 return nil, fmt.Errorf("unsupported flamebearer version %d", fb.Version) 266 } 267 268 return flamebearerV1ToTree(fb.Flamebearer) 269 } 270 271 func flamebearerV1ToTree(fb FlamebearerV1) (*tree.Tree, error) { 272 t := tree.New() 273 deltaDecoding(fb.Levels, 0, 4) 274 for i, l := range fb.Levels { 275 if i == 0 { 276 // Skip the first level: it'll contain the root ("total") node.. 277 continue 278 } 279 for j := 0; j < len(l); j += 4 { 280 self := l[j+2] 281 if self > 0 { 282 t.InsertStackString(buildStack(fb, i, j), uint64(self)) 283 } 284 } 285 } 286 return t, nil 287 } 288 289 func deltaDecoding(levels [][]int, start, step int) { 290 for _, l := range levels { 291 prev := 0 292 for i := start; i < len(l); i += step { 293 delta := l[i] + l[i+1] 294 l[i] += prev 295 prev += delta 296 } 297 } 298 } 299 300 func buildStack(fb FlamebearerV1, level, idx int) []string { 301 // The stack will contain names in the range [1, level]. 302 // Level 0 is not included as its the root ("total") node. 303 stack := make([]string, level) 304 stack[level-1] = fb.Names[fb.Levels[level][idx+3]] 305 x := fb.Levels[level][idx] 306 for i := level - 1; i > 0; i-- { 307 j := sort.Search(len(fb.Levels[i])/4, func(j int) bool { return fb.Levels[i][j*4] > x }) - 1 308 stack[i-1] = fb.Names[fb.Levels[i][j*4+3]] 309 x = fb.Levels[i][j*4] 310 } 311 return stack 312 }