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  }