github.com/alibaba/ilogtail/pkg@v0.0.0-20250526110833-c53b480d046c/helper/profile/pyroscope/pprof/pprof.go (about) 1 // Copyright 2023 iLogtail Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package pprof 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "mime/multipart" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/cespare/xxhash/v2" 29 "github.com/pyroscope-io/pyroscope/pkg/convert/pprof" 30 "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" 31 "github.com/pyroscope-io/pyroscope/pkg/storage/segment" 32 "github.com/pyroscope-io/pyroscope/pkg/storage/tree" 33 "github.com/pyroscope-io/pyroscope/pkg/util/form" 34 35 "github.com/alibaba/ilogtail/pkg/helper/profile" 36 "github.com/alibaba/ilogtail/pkg/logger" 37 "github.com/alibaba/ilogtail/pkg/protocol" 38 ) 39 40 const ( 41 formFieldProfile, formFieldSampleTypeConfig, formFieldPreviousProfile = "profile", "sample_type_config", "prev_profile" 42 ) 43 44 var DefaultSampleTypeMapping = map[string]*tree.SampleTypeConfig{ 45 "samples": { 46 Units: metadata.SamplesUnits, 47 DisplayName: "cpu", 48 Sampled: true, 49 }, 50 "inuse_objects": { 51 Units: metadata.ObjectsUnits, 52 Aggregation: "avg", 53 }, 54 "alloc_objects": { 55 Units: metadata.ObjectsUnits, 56 Cumulative: true, 57 }, 58 "inuse_space": { 59 Units: metadata.BytesUnits, 60 Aggregation: "avg", 61 }, 62 "alloc_space": { 63 Units: metadata.BytesUnits, 64 Cumulative: true, 65 }, 66 "goroutine": { 67 DisplayName: "goroutines", 68 Units: metadata.GoroutinesUnits, 69 Aggregation: "avg", 70 }, 71 "contentions": { 72 DisplayName: "mutex_count", 73 Units: metadata.LockSamplesUnits, 74 Cumulative: true, 75 }, 76 "delay": { 77 DisplayName: "mutex_duration", 78 Units: metadata.LockNanosecondsUnits, 79 Cumulative: true, 80 }, 81 } 82 83 type RawProfile struct { 84 rawData []byte 85 formDataContentType string 86 profile []byte 87 previousProfile []byte 88 sampleTypeConfig map[string]*tree.SampleTypeConfig 89 parser *Parser 90 pushMode bool 91 92 logs []*protocol.Log // v1 result 93 } 94 95 func NewRawProfileByPull(current, pre []byte, config map[string]*tree.SampleTypeConfig) *RawProfile { 96 return &RawProfile{ 97 profile: current, 98 previousProfile: pre, 99 } 100 } 101 102 func NewRawProfile(data []byte, format string) *RawProfile { 103 return &RawProfile{ 104 rawData: data, 105 formDataContentType: format, 106 pushMode: true, 107 } 108 } 109 110 func (r *RawProfile) Parse(ctx context.Context, meta *profile.Meta, tags map[string]string) (logs []*protocol.Log, err error) { 111 cb := r.extractProfileV1(meta, tags) 112 if err = r.doParse(ctx, meta, cb); err != nil { 113 return nil, err 114 } 115 logs = r.logs 116 r.logs = nil 117 return 118 } 119 120 func (r *RawProfile) doParse(ctx context.Context, meta *profile.Meta, cb profile.CallbackFunc) error { 121 if r.pushMode { 122 if err := r.extractProfileRaw(); err != nil { 123 return fmt.Errorf("cannot extract profile: %w", err) 124 } 125 } 126 127 if len(r.profile) == 0 { 128 return errors.New("empty profile") 129 } 130 131 if meta.SampleRate > 0 { 132 meta.Tags["_sample_rate_"] = strconv.FormatUint(uint64(meta.SampleRate), 10) 133 } 134 if r.parser == nil { 135 if r.sampleTypeConfig == nil { 136 if logger.DebugFlag() { 137 var keys []string 138 for k := range r.sampleTypeConfig { 139 keys = append(keys, k) 140 } 141 logger.Debug(ctx, "pprof default sampleTypeConfig: ", r.sampleTypeConfig == nil, "config:", strings.Join(keys, ",")) 142 } 143 r.sampleTypeConfig = DefaultSampleTypeMapping 144 } 145 r.parser = &Parser{ 146 stackFrameFormatter: Formatter{}, 147 sampleTypesFilter: filterKnownSamples(r.sampleTypeConfig), 148 sampleTypes: r.sampleTypeConfig, 149 } 150 if len(r.previousProfile) > 0 { 151 filter := r.parser.sampleTypesFilter 152 r.parser.sampleTypesFilter = func(s string) bool { 153 if filter != nil { 154 return filter(s) && r.parser.sampleTypes[s].Cumulative 155 } 156 return r.parser.sampleTypes[s].Cumulative 157 } 158 err := pprof.DecodePool(bytes.NewReader(r.previousProfile), func(tf *tree.Profile) error { 159 if err := r.extractLogs(ctx, tf, meta, cb); err != nil { 160 return err 161 } 162 return nil 163 }) 164 if err != nil { 165 return err 166 167 } 168 169 } 170 } 171 172 return pprof.DecodePool(bytes.NewReader(r.profile), func(tf *tree.Profile) error { 173 174 if err := r.extractLogs(ctx, tf, meta, cb); err != nil { 175 return err 176 } 177 return nil 178 }) 179 } 180 181 func sampleRate(p *tree.Profile) int64 { 182 if p.Period <= 0 || p.PeriodType == nil { 183 return 0 184 } 185 sampleUnit := time.Nanosecond 186 switch p.StringTable[p.PeriodType.Unit] { 187 case "microseconds": 188 sampleUnit = time.Microsecond 189 case "milliseconds": 190 sampleUnit = time.Millisecond 191 case "seconds": 192 sampleUnit = time.Second 193 } 194 return p.Period * sampleUnit.Nanoseconds() 195 } 196 197 func (r *RawProfile) extractLogs(ctx context.Context, tp *tree.Profile, meta *profile.Meta, cb profile.CallbackFunc) error { 198 199 stackMap := make(map[uint64]*profile.Stack) 200 valMap := make(map[uint64][]uint64) 201 labelMap := make(map[uint64]map[string]string) 202 typeMap := make(map[uint64][]string) 203 unitMap := make(map[uint64][]string) 204 aggtypeMap := make(map[uint64][]string) 205 206 if len(tp.SampleType) > 0 { 207 meta.Units = profile.Units(tp.StringTable[tp.SampleType[0].Type]) 208 } 209 p := r.parser 210 err := p.iterate(tp, func(vt *tree.ValueType, tl tree.Labels, t *tree.Tree) (keep bool, err error) { 211 if len(tp.StringTable) <= int(vt.Type) || len(tp.StringTable) <= int(vt.Unit) { 212 return true, errors.New("invalid type or unit") 213 } 214 stype := tp.StringTable[vt.Type] 215 sunit := tp.StringTable[vt.Unit] 216 sconfig, ok := p.sampleTypes[stype] 217 if !ok { 218 return false, errors.New("unknown type") 219 } 220 if sconfig.Cumulative { 221 prev, found := p.load(vt.Type, tl) 222 if !found { 223 // Keep the current entry in cache. 224 return true, nil 225 } 226 // Take diff with the previous tree. 227 // The result is written to prev, t is not changed. 228 t = prev.Diff(t) 229 } 230 var sampleDuration int64 231 if sconfig.Sampled { 232 sampleDuration = sampleRate(tp) 233 } 234 t.IterateStacks(func(name string, self uint64, stack []string) { 235 if name == "" { 236 return 237 } 238 id := xxhash.Sum64String(strings.Join(stack, "")) 239 stackMap[id] = &profile.Stack{ 240 Name: profile.FormatPositionAndName(name, profile.FormatType(meta.SpyName)), 241 Stack: profile.FormatPostionAndNames(stack[1:], profile.FormatType(meta.SpyName)), 242 } 243 aggtypeMap[id] = append(aggtypeMap[id], p.getAggregationType(stype, string(meta.AggregationType))) 244 typeMap[id] = append(typeMap[id], p.getDisplayName(stype)) 245 if sconfig.Sampled && sampleDuration != 0 && stype == string(profile.SamplesUnits) { 246 sunit = string(profile.NanosecondsUnit) 247 self *= uint64(sampleDuration) 248 } 249 unitMap[id] = append(unitMap[id], sunit) 250 valMap[id] = append(valMap[id], self) 251 labelMap[id] = buildKey(meta.Tags, tl, tp.StringTable).Labels() 252 }) 253 return true, nil 254 }) 255 if err != nil { 256 return fmt.Errorf("iterate profile tree error: %w", err) 257 } 258 for id, fs := range stackMap { 259 if len(valMap[id]) == 0 || len(typeMap[id]) == 0 || len(unitMap[id]) == 0 || len(aggtypeMap[id]) == 0 { 260 logger.Warning(ctx, "PPROF_PROFILE_ALARM", "stack don't have enough meta or values", fs) 261 continue 262 } 263 if tp.GetTimeNanos() != 0 { 264 cb(id, fs, valMap[id], typeMap[id], unitMap[id], aggtypeMap[id], tp.GetTimeNanos(), tp.GetTimeNanos()+tp.GetDurationNanos(), labelMap[id]) 265 } else { 266 cb(id, fs, valMap[id], typeMap[id], unitMap[id], aggtypeMap[id], meta.StartTime.UnixNano(), meta.EndTime.UnixNano(), labelMap[id]) 267 } 268 } 269 return nil 270 } 271 272 func (r *RawProfile) extractProfileV1(meta *profile.Meta, tags map[string]string) profile.CallbackFunc { 273 profileIDStr := profile.GetProfileID(meta) 274 return func(id uint64, stack *profile.Stack, vals []uint64, types, units, aggs []string, startTime, endTime int64, labels map[string]string) { 275 for k, v := range tags { 276 labels[k] = v 277 } 278 b, _ := json.Marshal(labels) 279 var content []*protocol.Log_Content 280 content = append(content, 281 &protocol.Log_Content{ 282 Key: "name", 283 Value: stack.Name, 284 }, 285 &protocol.Log_Content{ 286 Key: "stack", 287 Value: strings.Join(stack.Stack, "\n"), 288 }, 289 &protocol.Log_Content{ 290 Key: "stackID", 291 Value: strconv.FormatUint(id, 16), 292 }, 293 &protocol.Log_Content{ 294 Key: "language", 295 Value: meta.SpyName, 296 }, 297 &protocol.Log_Content{ 298 Key: "dataType", 299 Value: "CallStack", 300 }, 301 &protocol.Log_Content{ 302 Key: "durationNs", 303 Value: strconv.FormatInt(endTime-startTime, 10), 304 }, 305 &protocol.Log_Content{ 306 Key: "profileID", 307 Value: profileIDStr, 308 }, 309 &protocol.Log_Content{ 310 Key: "labels", 311 Value: string(b), 312 }, 313 ) 314 for i, v := range vals { 315 var res []*protocol.Log_Content 316 if i != len(vals)-1 { 317 res = make([]*protocol.Log_Content, len(content)) 318 copy(res, content) 319 } else { 320 res = content 321 } 322 res = append(res, 323 &protocol.Log_Content{ 324 Key: "units", 325 Value: units[i], 326 }, 327 &protocol.Log_Content{ 328 Key: "valueTypes", 329 Value: types[i], 330 }, 331 &protocol.Log_Content{ 332 Key: "aggTypes", 333 Value: aggs[i], 334 }, 335 &protocol.Log_Content{ 336 Key: "type", 337 Value: profile.DetectProfileType(types[i]).Kind, 338 }, 339 &protocol.Log_Content{ 340 Key: "val", 341 Value: strconv.FormatFloat(float64(v), 'f', 2, 64), 342 }, 343 ) 344 log := &protocol.Log{ 345 Contents: res, 346 } 347 protocol.SetLogTimeWithNano(log, uint32(startTime/1e9), uint32(startTime%1e9)) 348 r.logs = append(r.logs, log) 349 } 350 } 351 } 352 353 func buildKey(appLabels map[string]string, labels tree.Labels, table []string) *segment.Key { 354 finalLabels := map[string]string{} 355 for k, v := range appLabels { 356 finalLabels[k] = v 357 } 358 for _, v := range labels { 359 ks := table[v.Key] 360 if ks == "" { 361 continue 362 } 363 vs := table[v.Str] 364 if vs == "" { 365 continue 366 } 367 finalLabels[ks] = vs 368 } 369 return segment.NewKey(finalLabels) 370 } 371 372 func (r *RawProfile) extractProfileRaw() error { 373 if r.formDataContentType == "" { 374 r.profile = r.rawData 375 return nil 376 } 377 boundary, err := form.ParseBoundary(r.formDataContentType) 378 if err != nil { 379 return err 380 } 381 f, err := multipart.NewReader(bytes.NewReader(r.rawData), boundary).ReadForm(32 << 20) 382 if err != nil { 383 return err 384 } 385 defer func() { 386 _ = f.RemoveAll() 387 }() 388 389 if r.profile, err = form.ReadField(f, formFieldProfile); err != nil { 390 return err 391 } 392 r.previousProfile, err = form.ReadField(f, formFieldPreviousProfile) 393 if err != nil { 394 return err 395 } 396 if c, err := form.ReadField(f, formFieldSampleTypeConfig); err != nil { 397 return err 398 } else if c != nil { 399 var config map[string]*tree.SampleTypeConfig 400 if err = json.Unmarshal(c, &config); err != nil { 401 return err 402 } 403 r.sampleTypeConfig = config 404 } 405 return nil 406 }