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  }