github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/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 "time" 13 "unicode" 14 15 "github.com/pyroscope-io/pyroscope/pkg/agent/spy" 16 "github.com/pyroscope-io/pyroscope/pkg/convert/perf" 17 "github.com/pyroscope-io/pyroscope/pkg/convert/pprof" 18 "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" 19 "github.com/pyroscope-io/pyroscope/pkg/storage/tree" 20 "github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer" 21 ) 22 23 // ProfileFile represents content to be converted to flamebearer. 24 type ProfileFile struct { 25 // Name of the file in which the profile was saved. Optional. 26 // example: pyroscope.server.cpu-2022-01-23T14:31:43Z.json 27 Name string 28 // Type of profile. Optional. 29 Type ProfileFileType 30 TypeData ProfileFileTypeData 31 // Raw profile bytes. Required, min length 2. 32 Data []byte 33 } 34 35 type ProfileFileType string 36 37 type ProfileFileTypeData struct { 38 SpyName string 39 Units metadata.Units 40 } 41 42 const ( 43 ProfileFileTypeJSON ProfileFileType = "json" 44 ProfileFileTypePprof ProfileFileType = "pprof" 45 ProfileFileTypeCollapsed ProfileFileType = "collapsed" 46 ProfileFileTypePerfScript ProfileFileType = "perf_script" 47 ) 48 49 type ConverterFn func(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) 50 51 var formatConverters = map[ProfileFileType]ConverterFn{ 52 ProfileFileTypeJSON: JSONToProfile, 53 ProfileFileTypePprof: PprofToProfile, 54 ProfileFileTypeCollapsed: CollapsedToProfile, 55 ProfileFileTypePerfScript: PerfScriptToProfile, 56 } 57 58 func FlamebearerFromFile(f ProfileFile, maxNodes int) (*flamebearer.FlamebearerProfile, error) { 59 convertFn, _, err := Converter(f) 60 if err != nil { 61 return nil, err 62 } 63 return convertFn(f.Data, f.Name, maxNodes) 64 } 65 66 // Converter returns a ConverterFn that converts to 67 // FlamebearerProfile and overrides any specified fields. 68 func Converter(p ProfileFile) (ConverterFn, ProfileFileType, error) { 69 convertFn, err := converter(p) 70 if err != nil { 71 return nil, "", err 72 } 73 return func(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) { 74 fb, err := convertFn(b, name, maxNodes) 75 if err != nil { 76 return nil, fmt.Errorf("unable to process the profile. The profile was detected as %q: %w", 77 converterToFormat(convertFn), err) 78 } 79 // Overwrite fields if available 80 if p.TypeData.SpyName != "" { 81 fb.Metadata.SpyName = p.TypeData.SpyName 82 } 83 // Replace the units if provided 84 if p.TypeData.Units != "" { 85 fb.Metadata.Units = p.TypeData.Units 86 } 87 return fb, nil 88 }, converterToFormat(convertFn), nil 89 } 90 91 // Note that converterToFormat works only for converter output, 92 // Converter wraps the returned function into anonymous one. 93 func converterToFormat(f ConverterFn) ProfileFileType { 94 switch reflect.ValueOf(f).Pointer() { 95 case reflect.ValueOf(JSONToProfile).Pointer(): 96 return ProfileFileTypeJSON 97 case reflect.ValueOf(PprofToProfile).Pointer(): 98 return ProfileFileTypePprof 99 case reflect.ValueOf(CollapsedToProfile).Pointer(): 100 return ProfileFileTypeCollapsed 101 case reflect.ValueOf(PerfScriptToProfile).Pointer(): 102 return ProfileFileTypePerfScript 103 } 104 return "unknown" 105 } 106 107 // TODO(kolesnikovae): 108 // 109 // Consider simpler (but more reliable) logic for format identification 110 // with fallbacks: from the most strict format to the loosest one, e.g: 111 // pprof, json, collapsed, perf. 112 func converter(p ProfileFile) (ConverterFn, error) { 113 if f, ok := formatConverters[p.Type]; ok { 114 return f, nil 115 } 116 ext := strings.TrimPrefix(path.Ext(p.Name), ".") 117 if f, ok := formatConverters[ProfileFileType(ext)]; ok { 118 return f, nil 119 } 120 if ext == "txt" { 121 if perf.IsPerfScript(p.Data) { 122 return PerfScriptToProfile, nil 123 } 124 return CollapsedToProfile, nil 125 } 126 if len(p.Data) < 2 { 127 return nil, errors.New("profile is too short") 128 } 129 if p.Data[0] == '{' { 130 return JSONToProfile, nil 131 } 132 if p.Data[0] == '\x1f' && p.Data[1] == '\x8b' { 133 // gzip magic number, assume pprof 134 return PprofToProfile, nil 135 } 136 // Unclear whether it's uncompressed pprof or collapsed, let's check if all the bytes are printable 137 // This will be slow for collapsed format, but should be fast enough for pprof, which is the most usual case, 138 // but we have a reasonable upper bound just in case. 139 // TODO(abeaumont): This won't work with collapsed format with non-ascii encodings. 140 for i, b := range p.Data { 141 if i == 100 { 142 break 143 } 144 if !unicode.IsPrint(rune(b)) && !unicode.IsSpace(rune(b)) { 145 return PprofToProfile, nil 146 } 147 } 148 if perf.IsPerfScript(p.Data) { 149 return PerfScriptToProfile, nil 150 } 151 return CollapsedToProfile, nil 152 } 153 154 func JSONToProfile(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) { 155 var profile flamebearer.FlamebearerProfile 156 if err := json.Unmarshal(b, &profile); err != nil { 157 return nil, fmt.Errorf("unable to unmarshall JSON: %w", err) 158 } 159 if err := profile.Validate(); err != nil { 160 return nil, fmt.Errorf("invalid profile: %w", err) 161 } 162 if name != "" { 163 profile.Metadata.Name = name 164 } 165 166 t, err := flamebearer.ProfileToTree(profile) 167 if err != nil { 168 return nil, fmt.Errorf("could not convert flameabearer to tree: %w", err) 169 } 170 171 pc := flamebearer.ProfileConfig{ 172 Tree: t, 173 Name: profile.Metadata.Name, 174 MaxNodes: maxNodes, 175 Metadata: metadata.Metadata{ 176 SpyName: profile.Metadata.SpyName, 177 SampleRate: profile.Metadata.SampleRate, 178 Units: profile.Metadata.Units, 179 }, 180 } 181 182 p := flamebearer.NewProfile(pc) 183 return &p, nil 184 } 185 186 func PprofToProfile(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) { 187 var p tree.Profile 188 if err := pprof.Decode(bytes.NewReader(b), &p); err != nil { 189 return nil, fmt.Errorf("parsing pprof: %w", err) 190 } 191 // TODO(abeaumont): Support multiple sample types 192 for _, stype := range p.SampleTypes() { 193 sampleRate := uint32(100) 194 units := metadata.SamplesUnits 195 if c, ok := tree.DefaultSampleTypeMapping[stype]; ok { 196 units = c.Units 197 if c.Sampled && p.Period > 0 { 198 sampleRate = uint32(time.Second / time.Duration(p.Period)) 199 } 200 } 201 t := tree.New() 202 p.Get(stype, func(_labels *spy.Labels, name []byte, val int) error { 203 t.Insert(name, uint64(val)) 204 return nil 205 }) 206 fb := flamebearer.NewProfile(flamebearer.ProfileConfig{ 207 Tree: t, 208 Name: name, 209 MaxNodes: maxNodes, 210 Metadata: metadata.Metadata{ 211 SpyName: "unknown", 212 SampleRate: sampleRate, 213 Units: units, 214 }, 215 }) 216 return &fb, nil 217 } 218 return nil, errors.New("no supported sample type found") 219 } 220 221 func CollapsedToProfile(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) { 222 t := tree.New() 223 for _, line := range bytes.Split(b, []byte("\n")) { 224 if len(line) == 0 { 225 continue 226 } 227 i := bytes.LastIndexByte(line, ' ') 228 if i < 0 { 229 return nil, errors.New("unable to find stacktrace and value separator") 230 } 231 value, err := strconv.ParseUint(string(line[i+1:]), 10, 64) 232 if err != nil { 233 return nil, fmt.Errorf("unable to parse sample value: %w", err) 234 } 235 t.Insert(line[:i], value) 236 } 237 fb := flamebearer.NewProfile(flamebearer.ProfileConfig{ 238 Name: name, 239 Tree: t, 240 MaxNodes: maxNodes, 241 Metadata: metadata.Metadata{ 242 SpyName: "unknown", 243 SampleRate: 100, // We don't have this information, use the default 244 }, 245 }) 246 return &fb, nil 247 } 248 249 func PerfScriptToProfile(b []byte, name string, maxNodes int) (*flamebearer.FlamebearerProfile, error) { 250 t := tree.New() 251 p := perf.NewScriptParser(b) 252 events, err := p.ParseEvents() 253 if err != nil { 254 return nil, err 255 } 256 for _, e := range events { 257 t.InsertStack(e, 1) 258 } 259 fb := flamebearer.NewProfile(flamebearer.ProfileConfig{ 260 Name: name, 261 Tree: t, 262 MaxNodes: maxNodes, 263 Metadata: metadata.Metadata{ 264 SpyName: "unknown", 265 SampleRate: 100, // We don't have this information, use the default 266 }, 267 }) 268 return &fb, nil 269 }