github.com/grafana/pyroscope@v1.18.0/pkg/og/structs/flamebearer/convert/convert.go (about) 1 package convert 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "path" 9 "reflect" 10 "strconv" 11 "strings" 12 "unicode" 13 14 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 15 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 16 "github.com/grafana/pyroscope/pkg/model" 17 "github.com/grafana/pyroscope/pkg/og/convert/perf" 18 "github.com/grafana/pyroscope/pkg/og/storage/metadata" 19 "github.com/grafana/pyroscope/pkg/og/storage/tree" 20 "github.com/grafana/pyroscope/pkg/og/structs/flamebearer" 21 "github.com/grafana/pyroscope/pkg/pprof" 22 ) 23 24 // ProfileFile represents content to be converted to flamebearer. 25 type ProfileFile struct { 26 // Name of the file in which the profile was saved. Optional. 27 // example: pyroscope.server.cpu-2022-01-23T14:31:43Z.json 28 Name string 29 // Type of profile. Optional. 30 Type ProfileFileType 31 TypeData ProfileFileTypeData 32 // Raw profile bytes. Required, min length 2. 33 Data []byte 34 } 35 36 type ProfileFileType string 37 38 type ProfileFileTypeData struct { 39 SpyName string 40 Units metadata.Units 41 } 42 43 const ( 44 ProfileFileTypeJSON ProfileFileType = "json" 45 ProfileFileTypePprof ProfileFileType = "pprof" 46 ProfileFileTypeCollapsed ProfileFileType = "collapsed" 47 ProfileFileTypePerfScript ProfileFileType = "perf_script" 48 ) 49 50 type ConverterFn func(b []byte, name string, limits Limits) ([]*flamebearer.FlamebearerProfile, error) 51 52 var formatConverters = map[ProfileFileType]ConverterFn{ 53 ProfileFileTypeJSON: JSONToProfile, 54 ProfileFileTypePprof: PprofToProfile, 55 ProfileFileTypeCollapsed: CollapsedToProfile, 56 ProfileFileTypePerfScript: PerfScriptToProfile, 57 } 58 59 type Limits struct { 60 MaxNodes int 61 MaxProfileSizeBytes int 62 } 63 64 func FlamebearerFromFile(f ProfileFile, limits Limits) ([]*flamebearer.FlamebearerProfile, error) { 65 convertFn, _, err := Converter(f) 66 if err != nil { 67 return nil, err 68 } 69 return convertFn(f.Data, f.Name, limits) 70 } 71 72 // Converter returns a ConverterFn that converts to 73 // FlamebearerProfile and overrides any specified fields. 74 func Converter(p ProfileFile) (ConverterFn, ProfileFileType, error) { 75 convertFn, err := converter(p) 76 if err != nil { 77 return nil, "", err 78 } 79 return func(b []byte, name string, limits Limits) ([]*flamebearer.FlamebearerProfile, error) { 80 fbs, err := convertFn(b, name, limits) 81 if err != nil { 82 return nil, fmt.Errorf("unable to process the profile. The profile was detected as %q: %w", 83 converterToFormat(convertFn), err) 84 } 85 // Overwrite fields if available 86 if p.TypeData.SpyName != "" { 87 for _, fb := range fbs { 88 fb.Metadata.SpyName = p.TypeData.SpyName 89 } 90 } 91 // Replace the units if provided 92 if p.TypeData.Units != "" { 93 for _, fb := range fbs { 94 if fb.Metadata.Units == "" { 95 fb.Metadata.Units = p.TypeData.Units 96 } 97 } 98 } 99 return fbs, nil 100 }, converterToFormat(convertFn), nil 101 } 102 103 // Note that converterToFormat works only for converter output, 104 // Converter wraps the returned function into anonymous one. 105 func converterToFormat(f ConverterFn) ProfileFileType { 106 switch reflect.ValueOf(f).Pointer() { 107 case reflect.ValueOf(JSONToProfile).Pointer(): 108 return ProfileFileTypeJSON 109 case reflect.ValueOf(PprofToProfile).Pointer(): 110 return ProfileFileTypePprof 111 case reflect.ValueOf(CollapsedToProfile).Pointer(): 112 return ProfileFileTypeCollapsed 113 case reflect.ValueOf(PerfScriptToProfile).Pointer(): 114 return ProfileFileTypePerfScript 115 } 116 return "unknown" 117 } 118 119 // TODO(kolesnikovae): 120 // 121 // Consider simpler (but more reliable) logic for format identification 122 // with fallbacks: from the most strict format to the loosest one, e.g: 123 // pprof, json, collapsed, perf. 124 func converter(p ProfileFile) (ConverterFn, error) { 125 if f, ok := formatConverters[p.Type]; ok { 126 return f, nil 127 } 128 ext := strings.TrimPrefix(path.Ext(p.Name), ".") 129 if f, ok := formatConverters[ProfileFileType(ext)]; ok { 130 return f, nil 131 } 132 if ext == "txt" { 133 if perf.IsPerfScript(p.Data) { 134 return PerfScriptToProfile, nil 135 } 136 return CollapsedToProfile, nil 137 } 138 if len(p.Data) < 2 { 139 return nil, errors.New("profile is too short") 140 } 141 if p.Data[0] == '{' { 142 return JSONToProfile, nil 143 } 144 if p.Data[0] == '\x1f' && p.Data[1] == '\x8b' { 145 // gzip magic number, assume pprof 146 return PprofToProfile, nil 147 } 148 // Unclear whether it's uncompressed pprof or collapsed, let's check if all the bytes are printable 149 // This will be slow for collapsed format, but should be fast enough for pprof, which is the most usual case, 150 // but we have a reasonable upper bound just in case. 151 // TODO(abeaumont): This won't work with collapsed format with non-ascii encodings. 152 for i, b := range p.Data { 153 if i == 100 { 154 break 155 } 156 if !unicode.IsPrint(rune(b)) && !unicode.IsSpace(rune(b)) { 157 return PprofToProfile, nil 158 } 159 } 160 if perf.IsPerfScript(p.Data) { 161 return PerfScriptToProfile, nil 162 } 163 return CollapsedToProfile, nil 164 } 165 166 func JSONToProfile(b []byte, name string, limits Limits) ([]*flamebearer.FlamebearerProfile, error) { 167 var profile flamebearer.FlamebearerProfile 168 if err := json.Unmarshal(b, &profile); err != nil { 169 return nil, fmt.Errorf("unable to unmarshall JSON: %w", err) 170 } 171 if err := profile.Validate(); err != nil { 172 return nil, fmt.Errorf("invalid profile: %w", err) 173 } 174 175 t, err := flamebearer.ProfileToTree(profile) 176 if err != nil { 177 return nil, fmt.Errorf("could not convert flameabearer to tree: %w", err) 178 } 179 180 pc := flamebearer.ProfileConfig{ 181 Tree: t, 182 Name: profile.Metadata.Name, 183 MaxNodes: limits.MaxNodes, 184 Metadata: metadata.Metadata{ 185 SpyName: profile.Metadata.SpyName, 186 SampleRate: profile.Metadata.SampleRate, 187 Units: profile.Metadata.Units, 188 }, 189 } 190 191 p := flamebearer.NewProfile(pc) 192 return []*flamebearer.FlamebearerProfile{&p}, nil 193 } 194 195 func getProfileType(name string, sampleType int, p *profilev1.Profile) (*typesv1.ProfileType, error) { 196 tp := &typesv1.ProfileType{ 197 Name: name, 198 } 199 200 // check if the sampleID is valid 201 if sampleType < 0 || sampleType >= len(p.SampleType) { 202 return nil, fmt.Errorf("invalid sampleID: %d", sampleType) 203 } 204 205 if p.PeriodType == nil { 206 return nil, fmt.Errorf("PeriodType is nil") 207 } 208 209 invalidStr := func(i int) bool { 210 return i < 0 || i > len(p.StringTable) 211 } 212 if v := int(p.PeriodType.Type); invalidStr(v) { 213 return nil, fmt.Errorf("invalid PeriodType: %d", v) 214 } else { 215 tp.PeriodType = p.StringTable[v] 216 } 217 if v := int(p.PeriodType.Unit); invalidStr(v) { 218 return nil, fmt.Errorf("invalid PeriodUnit: %d", v) 219 } else { 220 tp.PeriodUnit = p.StringTable[v] 221 } 222 if v := int(p.SampleType[sampleType].Type); invalidStr(v) { 223 return nil, fmt.Errorf("invalid SampleType[%d]: %d", sampleType, v) 224 } else { 225 tp.SampleType = p.StringTable[v] 226 } 227 if v := int(p.SampleType[sampleType].Unit); invalidStr(v) { 228 return nil, fmt.Errorf("invalid SampleUnit[%d]: %d", sampleType, v) 229 } else { 230 tp.SampleUnit = p.StringTable[v] 231 } 232 233 tp.ID = fmt.Sprintf("%s:%s:%s:%s:%s", name, tp.SampleType, tp.SampleUnit, tp.PeriodType, tp.PeriodUnit) 234 return tp, nil 235 } 236 237 func PprofToProfile(b []byte, name string, limits Limits) ([]*flamebearer.FlamebearerProfile, error) { 238 prof, err := pprof.RawFromBytesWithLimit(b, int64(limits.MaxProfileSizeBytes)) 239 if err != nil { 240 return nil, fmt.Errorf("parsing pprof: %w", err) 241 } 242 p := prof.Profile 243 244 t := model.NewStacktraceTree(int(limits.MaxNodes * 2)) 245 stack := make([]int32, 0, 64) 246 m := make(map[uint64]int32) 247 248 fbs := make([]*flamebearer.FlamebearerProfile, 0) 249 for sampleType := range p.SampleType { 250 t.Reset() 251 252 for i := range p.Sample { 253 stack = stack[:0] 254 for j := range p.Sample[i].LocationId { 255 locIdx := int(p.Sample[i].LocationId[j]) - 1 256 if locIdx < 0 || len(p.Location) <= locIdx { 257 return nil, fmt.Errorf("invalid location ID %d in sample %d", p.Sample[i].LocationId[j], i) 258 } 259 260 loc := p.Location[locIdx] 261 if len(loc.Line) > 0 { 262 for l := range loc.Line { 263 stack = append(stack, int32(p.Function[loc.Line[l].FunctionId-1].Name)) 264 } 265 continue 266 } 267 addr, ok := m[loc.Address] 268 if !ok { 269 addr = int32(len(p.StringTable)) 270 p.StringTable = append(p.StringTable, strconv.FormatInt(int64(loc.Address), 16)) 271 m[loc.Address] = addr 272 } 273 stack = append(stack, addr) 274 } 275 276 if sampleType < 0 || sampleType >= len(p.Sample[i].Value) { 277 return nil, fmt.Errorf("invalid sampleType index %d for sample %d (len=%d)", sampleType, i, len(p.Sample[i].Value)) 278 } 279 280 t.Insert(stack, p.Sample[i].Value[sampleType]) 281 } 282 283 tp, err := getProfileType(name, sampleType, p) 284 if err != nil { 285 return nil, err 286 } 287 288 fg := model.NewFlameGraph(t.Tree(int64(limits.MaxNodes), p.StringTable), int64(limits.MaxNodes)) 289 fbs = append(fbs, model.ExportToFlamebearer(fg, tp)) 290 } 291 if len(fbs) == 0 { 292 return nil, errors.New("no supported sample type found") 293 } 294 return fbs, nil 295 } 296 297 func CollapsedToProfile(b []byte, name string, limits Limits) ([]*flamebearer.FlamebearerProfile, error) { 298 t := tree.New() 299 for _, line := range bytes.Split(b, []byte("\n")) { 300 if len(line) == 0 { 301 continue 302 } 303 i := bytes.LastIndexByte(line, ' ') 304 if i < 0 { 305 return nil, errors.New("unable to find stacktrace and value separator") 306 } 307 value, err := strconv.ParseUint(string(line[i+1:]), 10, 64) 308 if err != nil { 309 return nil, fmt.Errorf("unable to parse sample value: %w", err) 310 } 311 t.Insert(line[:i], value) 312 } 313 fb := flamebearer.NewProfile(flamebearer.ProfileConfig{ 314 Name: name, 315 Tree: t, 316 MaxNodes: limits.MaxNodes, 317 Metadata: metadata.Metadata{ 318 SpyName: "unknown", 319 SampleRate: 100, // We don't have this information, use the default 320 }, 321 }) 322 return []*flamebearer.FlamebearerProfile{&fb}, nil 323 } 324 325 func PerfScriptToProfile(b []byte, name string, limits Limits) ([]*flamebearer.FlamebearerProfile, error) { 326 t := tree.New() 327 p := perf.NewScriptParser(b) 328 events, err := p.ParseEvents() 329 if err != nil { 330 return nil, err 331 } 332 for _, e := range events { 333 t.InsertStack(e, 1) 334 } 335 fb := flamebearer.NewProfile(flamebearer.ProfileConfig{ 336 Name: name, 337 Tree: t, 338 MaxNodes: limits.MaxNodes, 339 Metadata: metadata.Metadata{ 340 SpyName: "unknown", 341 SampleRate: 100, // We don't have this information, use the default 342 }, 343 }) 344 return []*flamebearer.FlamebearerProfile{&fb}, nil 345 }