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  }