github.com/GuanceCloud/cliutils@v1.1.21/pprofparser/service/parsing/pprof.go (about)

     1  package parsing
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/GuanceCloud/cliutils/pprofparser/domain/events"
    16  	"github.com/GuanceCloud/cliutils/pprofparser/domain/languages"
    17  	"github.com/GuanceCloud/cliutils/pprofparser/domain/parameter"
    18  	"github.com/GuanceCloud/cliutils/pprofparser/domain/pprof"
    19  	"github.com/GuanceCloud/cliutils/pprofparser/domain/quantity"
    20  	"github.com/GuanceCloud/cliutils/pprofparser/domain/tracing"
    21  	"github.com/GuanceCloud/cliutils/pprofparser/service/storage"
    22  	"github.com/GuanceCloud/cliutils/pprofparser/tools/logtoolkit"
    23  	"github.com/GuanceCloud/cliutils/pprofparser/tools/parsetoolkit"
    24  	"github.com/google/pprof/profile"
    25  	"github.com/pierrec/lz4/v4"
    26  )
    27  
    28  const (
    29  	LabelExceptionType   = "exception type"
    30  	LabelThreadID        = "thread id"
    31  	LabelThreadNativeID  = "thread native id"
    32  	LabelThreadName      = "thread name"
    33  	LabelSpanID          = "span id"
    34  	LabelLocalRootSpanID = "local root span id"
    35  )
    36  
    37  var (
    38  	ZIPMagic  = []byte{0x50, 0x4b, 3, 4}
    39  	LZ4Magic  = []byte{4, 34, 77, 24}
    40  	GZIPMagic = []byte{31, 139}
    41  )
    42  
    43  var lineNoRegExp = regexp.MustCompile(`^\d+$`)
    44  
    45  type PProf struct {
    46  	from          string
    47  	workspaceUUID string
    48  	profiles      []*parameter.Profile
    49  	filterBySpan  bool
    50  	span          *parameter.Span
    51  	spanIDSet     *tracing.SpanIDSet
    52  	DisplayCtl
    53  }
    54  
    55  type Decompressor struct {
    56  	io.Reader
    57  	r io.Reader
    58  }
    59  
    60  func NewDecompressor(r io.Reader) io.ReadCloser {
    61  	bufReader := bufio.NewReader(r)
    62  
    63  	magics, err := bufReader.Peek(4)
    64  	if err != nil {
    65  		return &Decompressor{
    66  			r:      r,
    67  			Reader: bufReader,
    68  		}
    69  	}
    70  
    71  	if bytes.Compare(LZ4Magic, magics) == 0 {
    72  		return &Decompressor{
    73  			r:      r,
    74  			Reader: lz4.NewReader(bufReader),
    75  		}
    76  	}
    77  
    78  	return &Decompressor{
    79  		r:      r,
    80  		Reader: bufReader,
    81  	}
    82  }
    83  
    84  func (d *Decompressor) Close() error {
    85  	var err error
    86  	if rc, ok := d.Reader.(io.Closer); ok {
    87  		if e := rc.Close(); e != nil {
    88  			err = e
    89  		}
    90  	}
    91  
    92  	if rc, ok := d.r.(io.Closer); ok {
    93  		if e := rc.Close(); e != nil {
    94  			err = e
    95  		}
    96  	}
    97  	return err
    98  }
    99  
   100  func NewPProfParser(
   101  	from string,
   102  	workspaceUUID string,
   103  	profiles []*parameter.Profile,
   104  	filterBySpan bool,
   105  	span *parameter.Span,
   106  	spanIDSet *tracing.SpanIDSet,
   107  	ctl DisplayCtl,
   108  ) *PProf {
   109  	return &PProf{
   110  		from:          from,
   111  		workspaceUUID: workspaceUUID,
   112  		profiles:      profiles,
   113  		filterBySpan:  filterBySpan,
   114  		span:          span,
   115  		spanIDSet:     spanIDSet,
   116  		DisplayCtl:    ctl,
   117  	}
   118  }
   119  
   120  func isGlobPattern(pattern string) bool {
   121  	return strings.ContainsAny(pattern, "?*")
   122  }
   123  
   124  func (p *PProf) mergePProf(filename string) (*profile.Profile, error) {
   125  	if len(p.profiles) == 0 {
   126  		return nil, fmt.Errorf("empty profiles")
   127  	}
   128  
   129  	client, err := storage.GetStorage(storage.LocalDisk)
   130  	if err != nil {
   131  		return nil, fmt.Errorf("init oss client err: %w", err)
   132  	}
   133  
   134  	filenames := []string{filename}
   135  
   136  	if strings.ContainsRune(filename, '|') {
   137  		filenames = strings.Split(filename, "|")
   138  	}
   139  
   140  	profSrc := make([]*profile.Profile, 0, len(p.profiles))
   141  
   142  	for _, prof := range p.profiles {
   143  		startTime, err := prof.StartTime()
   144  		if err != nil {
   145  			return nil, fmt.Errorf("cast ProfileStart to int64 fail: %w", err)
   146  		}
   147  
   148  		profilePath := ""
   149  
   150  	FilenameLoop:
   151  		for _, name := range filenames {
   152  			if name == "" {
   153  				continue
   154  			}
   155  
   156  			pattern := client.GetProfilePath(p.workspaceUUID, prof.ProfileID, startTime, name)
   157  			if ok, err := client.IsFileExists(pattern); ok && err == nil {
   158  				profilePath = pattern
   159  				break
   160  			}
   161  
   162  			// check whether the filename is a glob pattern
   163  			if isGlobPattern(name) {
   164  				matches, err := filepath.Glob(pattern)
   165  				if err != nil {
   166  					return nil, fmt.Errorf("illegal glob pattern [%s]; %w", pattern, err)
   167  				}
   168  
   169  				for _, match := range matches {
   170  					baseName := filepath.Base(match)
   171  					if baseName != events.DefaultMetaFileName && baseName != events.DefaultMetaFileNameWithExt {
   172  						profilePath = match
   173  						break FilenameLoop
   174  					}
   175  				}
   176  			}
   177  		}
   178  
   179  		if profilePath == "" {
   180  			return nil, fmt.Errorf("no available profile file found: [%s]: %w", filename, fs.ErrNotExist)
   181  		}
   182  
   183  		reader, err := client.ReadFile(profilePath)
   184  
   185  		if err != nil {
   186  			if errors.Is(err, fs.ErrNotExist) {
   187  				return nil, fmt.Errorf("profile file [%s] not exists: %w", profilePath, err)
   188  			}
   189  			if ok, err := client.IsFileExists(profilePath); err == nil && !ok {
   190  				return nil, fmt.Errorf("profile file [%s] not exists:%w", profilePath, fs.ErrNotExist)
   191  			}
   192  			return nil, fmt.Errorf("unable to read profile file [%s]: %w", profilePath, err)
   193  		}
   194  
   195  		parsedPProf, err := parseAndClose(NewDecompressor(reader))
   196  		if err != nil {
   197  			logtoolkit.Errorf("parse profile [path:%s] fail: %s", profilePath, err)
   198  			continue
   199  		}
   200  
   201  		profSrc = append(profSrc, parsedPProf)
   202  	}
   203  
   204  	if len(profSrc) == 0 {
   205  		return nil, fmt.Errorf("no available profile")
   206  	}
   207  
   208  	mergedPProf, err := profile.Merge(profSrc)
   209  	if err != nil {
   210  		return nil, fmt.Errorf("merge profile fail: %w", err)
   211  	}
   212  	if err := mergedPProf.CheckValid(); err != nil {
   213  		return nil, fmt.Errorf("invalid merged profile file: %w", err)
   214  	}
   215  	return mergedPProf, nil
   216  }
   217  
   218  func (p *PProf) Summary() (map[events.Type]*EventSummary, int64, error) {
   219  	lang, err := parameter.VerifyLanguage(p.profiles)
   220  	if err != nil {
   221  		return nil, 0, fmt.Errorf("unable to resolve language: %w", err)
   222  		//=======
   223  		//		return nil, 0, fmt.Errorf("GetSummary VerifyLanguage err: %w", err)
   224  		//	}
   225  		//
   226  		//	ok, err := IsPySpyProfile(param.Profiles, param.WorkspaceUUID)
   227  		//	if ok && err == nil {
   228  		//		prof := param.Profiles[0]
   229  		//		startNanos, err := jsontoolkit.IFaceCast2Int64(prof.ProfileStart)
   230  		//		if err != nil {
   231  		//			return nil, 0, fmt.Errorf("resolve Profile start timestamp fail: %w", err)
   232  		//		}
   233  		//		endNanos, err := jsontoolkit.IFaceCast2Int64(prof.ProfileEnd)
   234  		//		if err != nil {
   235  		//			return nil, 0, fmt.Errorf("resolve Profile end timestamp fail: %w", err)
   236  		//		}
   237  		//		profileFile := storage.DefaultDiskStorage.GetProfilePath(param.WorkspaceUUID, prof.ProfileID, startNanos, events.DefaultProfileFilename)
   238  		//
   239  		//		summaries, err := GetPySpySummary(profileFile)
   240  		//		if err != nil {
   241  		//			return nil, 0, fmt.Errorf("get py-spy profile summary fail: %w", err)
   242  		//		}
   243  		//		return summaries, endNanos - startNanos, nil
   244  		//	} else if err != nil {
   245  		//		logtoolkit.Warnf("judge if profile is from py-spy err: %s", err)
   246  		//>>>>>>> 66994b8f59cd601e6e7fbac181122f708d77ef3a:service/parsing/multiparser.go
   247  	}
   248  
   249  	fileSampleTypes := getFileSampleTypes(lang)
   250  	if len(fileSampleTypes) == 0 {
   251  		return nil, 0, fmt.Errorf("getFileSampleTypes: not supported language [%s]", lang)
   252  	}
   253  
   254  	summariesTypedMap := make(map[events.Type]*EventSummary)
   255  	var totalDurationNanos int64 = 0
   256  
   257  	filesCount := 0
   258  	for filename, sampleTypes := range fileSampleTypes {
   259  
   260  		mergedPProf, err := p.mergePProf(filename)
   261  		if err != nil {
   262  			if errors.Is(err, fs.ErrNotExist) {
   263  				continue
   264  			}
   265  			return nil, 0, fmt.Errorf("merge pprof: %w", err)
   266  		}
   267  		filesCount++
   268  
   269  		if mergedPProf.DurationNanos > totalDurationNanos {
   270  			totalDurationNanos = mergedPProf.DurationNanos
   271  		}
   272  
   273  		// pprof.SampleType 和 pprof.Sample[xx].Value 一一对应
   274  		summaryMap := make(map[int]*EventSummary)
   275  
   276  		for i, st := range mergedPProf.SampleType {
   277  
   278  			if et, ok := sampleTypes[st.Type]; ok {
   279  
   280  				if p.from == parameter.FromTrace && !p.ShowInTrace(et) {
   281  					continue
   282  				}
   283  
   284  				if p.from == parameter.FromProfile && !p.ShowInProfile(et) {
   285  					continue
   286  				}
   287  
   288  				unit, err := quantity.ParseUnit(et.GetQuantityKind(), st.Unit)
   289  				if err != nil {
   290  					return nil, 0, fmt.Errorf("parseUnit error: %w", err)
   291  				}
   292  
   293  				summaryMap[i] = &EventSummary{
   294  					SummaryValueType: &SummaryValueType{
   295  						Type: et,
   296  						Unit: unit,
   297  					},
   298  					Value: 0,
   299  				}
   300  			}
   301  		}
   302  
   303  		for _, sample := range mergedPProf.Sample {
   304  			// 需要进行span过滤
   305  			if p.filterBySpan {
   306  				spanID := parsetoolkit.GetStringLabel(sample, LabelSpanID)
   307  				rootSpanId := parsetoolkit.GetStringLabel(sample, LabelLocalRootSpanID)
   308  				// 没有spanID的数据去掉
   309  				if spanID == "" {
   310  					continue
   311  				}
   312  				if p.spanIDSet != nil {
   313  					if p.spanIDSet == tracing.AllTraceSpanSet {
   314  						if rootSpanId != p.span.SpanID {
   315  							continue
   316  						}
   317  					} else if !p.spanIDSet.Contains(spanID) {
   318  						continue
   319  					}
   320  				}
   321  			}
   322  			for i, v := range sample.Value {
   323  				if _, ok := summaryMap[i]; ok {
   324  					summaryMap[i].Value += v
   325  				}
   326  			}
   327  		}
   328  
   329  		for _, summary := range summaryMap {
   330  			summariesTypedMap[summary.Type] = summary
   331  		}
   332  	}
   333  
   334  	if filesCount == 0 {
   335  		sb := &strings.Builder{}
   336  		for i, pro := range p.profiles {
   337  			if i > 0 {
   338  				sb.WriteByte(';')
   339  			}
   340  			sb.WriteString(pro.ProfileID)
   341  		}
   342  		return nil, 0, fmt.Errorf("no corresponding profiling file exists, workspaceUUID [%s], profileID [%s]", p.workspaceUUID, sb.String())
   343  	}
   344  
   345  	return summariesTypedMap, totalDurationNanos, nil
   346  }
   347  
   348  // parseAndClose parse profile from a readable stream, and try to close the reader when end
   349  func parseAndClose(r io.Reader) (*profile.Profile, error) {
   350  	if r == nil {
   351  		return nil, fmt.Errorf("nil reader")
   352  	}
   353  
   354  	if closable, ok := r.(io.Closer); ok {
   355  		defer closable.Close()
   356  	}
   357  
   358  	goPprof, err := profile.Parse(r)
   359  
   360  	if err != nil {
   361  		return nil, fmt.Errorf("parse pprof err: %w", err)
   362  	}
   363  
   364  	return goPprof, nil
   365  }
   366  
   367  // ResolveFlameGraph (lang languages.Lang, eType events.Type, pprofSampleType string, filterBySpan bool, span *parameter.Span, spanIDSet *dql.SpanIDSet)
   368  func (p *PProf) ResolveFlameGraph(eventType events.Type) (*pprof.Frame, AggregatorSelectSlice, error) {
   369  
   370  	lang, err := parameter.VerifyLanguage(p.profiles)
   371  	if err != nil {
   372  		return nil, nil, fmt.Errorf("VerifyLanguage fail: %s", err)
   373  	}
   374  
   375  	sampleFile, err := GetFileByEvent(lang, eventType)
   376  	if err != nil {
   377  		return nil, nil, fmt.Errorf("GetFileByEvent: %s", err)
   378  	}
   379  
   380  	mergedPProf, err := p.mergePProf(sampleFile.Filename)
   381  	if err != nil {
   382  		return nil, nil, fmt.Errorf("merge pprof: %w", err)
   383  	}
   384  
   385  	valueIdx, valueUnit, err := p.getIdxOfTypeAndUnit(sampleFile.SampleType, mergedPProf)
   386  	if err != nil {
   387  		return nil, nil, fmt.Errorf("render frame: %w", err)
   388  	}
   389  
   390  	unit, err := quantity.ParseUnit(eventType.GetQuantityKind(), valueUnit)
   391  	if err != nil {
   392  		return nil, nil, fmt.Errorf("ParseUnit fail: %w", err)
   393  	}
   394  
   395  	rootFrame := &pprof.Frame{
   396  		SubFrames: make(pprof.SubFrames),
   397  	}
   398  
   399  	aggregatorList := PythonAggregatorList
   400  	switch lang {
   401  	case languages.GoLang:
   402  		aggregatorList = GoAggregatorList
   403  	case languages.NodeJS:
   404  		aggregatorList = PyroscopeNodeJSAggregatorList
   405  	case languages.DotNet:
   406  		aggregatorList = DDTraceDotnetAggregatorList
   407  	case languages.PHP:
   408  		aggregatorList = DDTracePHPAggregatorList
   409  	}
   410  
   411  	aggregatorSelectMap := make(AggregatorSelectSlice, 0, len(aggregatorList))
   412  
   413  	for _, aggregator := range aggregatorList {
   414  		aggregatorSelectMap = append(aggregatorSelectMap, &AggregatorSelect{
   415  			Aggregator: aggregator,
   416  			Mapping:    aggregator.Mapping,
   417  			Options:    make(map[string]*AggregatorOption),
   418  		})
   419  	}
   420  
   421  	totalValue := int64(0)
   422  	for _, smp := range mergedPProf.Sample {
   423  		if smp.Value[valueIdx] == 0 {
   424  			// 过滤值为0的采样数据
   425  			continue
   426  		}
   427  
   428  		// span 过滤,必须有spanID的才显示
   429  		if p.filterBySpan {
   430  			spanID := parsetoolkit.GetStringLabel(smp, LabelSpanID)
   431  			rootSpanId := parsetoolkit.GetStringLabel(smp, LabelLocalRootSpanID)
   432  			if spanID == "" {
   433  				continue
   434  			}
   435  			if p.spanIDSet == tracing.AllTraceSpanSet {
   436  				if rootSpanId != p.span.SpanID {
   437  					continue
   438  				}
   439  			} else if p.spanIDSet != nil {
   440  				if !p.spanIDSet.Contains(spanID) {
   441  					continue
   442  				}
   443  			}
   444  		}
   445  
   446  		currentFrame := rootFrame
   447  
   448  		totalValue += smp.Value[valueIdx]
   449  
   450  		for _, aggregatorSelect := range aggregatorSelectMap {
   451  			aggregator := aggregatorSelect.Aggregator
   452  			identifier := aggregator.GetIdentifier(lang, smp, false)
   453  
   454  			if _, ok := aggregatorSelect.Options[identifier]; ok {
   455  				aggregatorSelect.Options[identifier].Value += smp.Value[valueIdx]
   456  			} else {
   457  				mappingValues := make([]string, 0, len(aggregator.GetMappingFuncs))
   458  				for _, mFunc := range aggregator.GetMappingFuncs {
   459  					mappingValues = append(mappingValues, mFunc(lang, smp, false))
   460  				}
   461  				aggregatorSelect.Options[identifier] = &AggregatorOption{
   462  					Title:         aggregator.GetDisplayStr(lang, smp, false),
   463  					Value:         smp.Value[valueIdx],
   464  					Unit:          unit,
   465  					MappingValues: mappingValues,
   466  				}
   467  			}
   468  		}
   469  
   470  		for i := len(smp.Location) - 1; i >= 0; i-- {
   471  			location := smp.Location[i]
   472  			line := location.Line[len(location.Line)-1]
   473  
   474  			var funcIdentifier string
   475  			//if i == 0 {
   476  			// 最后一层必须严格相同, 不是最后一层行号不相同也允许合并
   477  			//funcIdentifier = fmt.Sprintf("%s###%s###%d", line.Function.Filename, line.Function.Name, line.Line)
   478  			funcIdentifier = strconv.FormatUint(location.ID, 10)
   479  			//} else {
   480  			//	funcIdentifier = fmt.Sprintf("%s###%s###%s", parsetoolkit.GetLabel(smp, LabelThreadID), line.Function.Filename, line.Function.Name)
   481  			//}
   482  
   483  			subFrame, ok := currentFrame.SubFrames[funcIdentifier]
   484  
   485  			if ok {
   486  				subFrame.Value += smp.Value[valueIdx]
   487  			} else {
   488  				subFrame = &pprof.Frame{
   489  					Value:       smp.Value[valueIdx],
   490  					Unit:        unit,
   491  					Function:    getFuncNameByLine(lang, line),
   492  					Line:        getLineByLine(lang, line),
   493  					File:        getFileByLine(lang, line),
   494  					Directory:   getDirectoryByLine(lang, line),
   495  					ThreadID:    getThreadIDBySample(smp),
   496  					ThreadName:  getThreadNameBySample(smp),
   497  					Class:       getClassByLine(lang, line),
   498  					Namespace:   getNamespaceByLine(lang, line),
   499  					Assembly:    getAssemblyByLine(lang, line),
   500  					Package:     getPackageNameByLine(lang, line),
   501  					PrintString: GetPrintStrByLine(lang, line),
   502  					SubFrames:   make(pprof.SubFrames),
   503  				}
   504  				currentFrame.SubFrames[funcIdentifier] = subFrame
   505  			}
   506  
   507  			currentFrame = subFrame
   508  		}
   509  	}
   510  
   511  	rootFrame.Value = totalValue
   512  	rootFrame.Unit = unit
   513  
   514  	parsetoolkit.CalcPercentAndQuantity(rootFrame, totalValue)
   515  	aggregatorSelectMap.CalcPercentAndQuantity(totalValue)
   516  
   517  	return rootFrame, aggregatorSelectMap, nil
   518  }
   519  
   520  func (p *PProf) getIdxOfTypeAndUnit(typeName string, pprof *profile.Profile) (int, string, error) {
   521  	for idx, st := range pprof.SampleType {
   522  		if st.Type == typeName {
   523  			return idx, st.Unit, nil
   524  		}
   525  	}
   526  	return 0, "", fmt.Errorf("the pprof does not contain the event type: %s", typeName)
   527  }