github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/convert/speedscope/parser.go (about)

     1  package speedscope
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  
     8  	"github.com/pyroscope-io/pyroscope/pkg/ingestion"
     9  	"github.com/pyroscope-io/pyroscope/pkg/storage"
    10  	"github.com/pyroscope-io/pyroscope/pkg/storage/metadata"
    11  	"github.com/pyroscope-io/pyroscope/pkg/storage/tree"
    12  )
    13  
    14  // RawProfile implements ingestion.RawProfile for Speedscope format
    15  type RawProfile struct {
    16  	RawData []byte
    17  }
    18  
    19  // Parse parses a profile
    20  func (p *RawProfile) Parse(ctx context.Context, putter storage.Putter, _ storage.MetricsExporter, md ingestion.Metadata) error {
    21  	profiles, err := parseAll(p.RawData, md)
    22  	if err != nil {
    23  		return err
    24  	}
    25  
    26  	for _, putInput := range profiles {
    27  		err = putter.Put(ctx, putInput)
    28  		if err != nil {
    29  			return err
    30  		}
    31  	}
    32  	return nil
    33  }
    34  
    35  func parseAll(rawData []byte, md ingestion.Metadata) ([]*storage.PutInput, error) {
    36  	file := speedscopeFile{}
    37  	err := json.Unmarshal(rawData, &file)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	if file.Schema != schema {
    42  		return nil, fmt.Errorf("Unknown schema: %s", file.Schema)
    43  	}
    44  
    45  	results := make([]*storage.PutInput, 0, len(file.Profiles))
    46  	// Not a pointer, we _want_ to copy on call
    47  	input := storage.PutInput{
    48  		StartTime:  md.StartTime,
    49  		EndTime:    md.EndTime,
    50  		SpyName:    md.SpyName,
    51  		SampleRate: md.SampleRate,
    52  		Key:        md.Key,
    53  	}
    54  
    55  	for _, prof := range file.Profiles {
    56  		putInput, err := parseOne(&prof, input, file.Shared.Frames, len(file.Profiles) > 1)
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  		results = append(results, putInput)
    61  	}
    62  	return results, nil
    63  }
    64  
    65  func parseOne(prof *profile, putInput storage.PutInput, frames []frame, multi bool) (*storage.PutInput, error) {
    66  	// Fixup some metadata
    67  	putInput.Units = prof.Unit.chooseMetadataUnit()
    68  	putInput.AggregationType = metadata.SumAggregationType
    69  	if multi {
    70  		putInput.Key = prof.Unit.chooseKey(putInput.Key)
    71  	}
    72  
    73  	// TODO(petethepig): We need a way to tell if it's a default or a value set by user
    74  	//   See https://github.com/pyroscope-io/pyroscope/issues/1598
    75  	if putInput.SampleRate == 100 {
    76  		putInput.SampleRate = uint32(prof.Unit.defaultSampleRate())
    77  	}
    78  
    79  	var err error
    80  	tr := tree.New()
    81  	switch prof.Type {
    82  	case profileEvented:
    83  		err = parseEvented(tr, prof, frames)
    84  	case profileSampled:
    85  		err = parseSampled(tr, prof, frames)
    86  	default:
    87  		return nil, fmt.Errorf("Profile type %s not supported", prof.Type)
    88  	}
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	putInput.Val = tr
    94  	return &putInput, nil
    95  }
    96  
    97  func parseEvented(tr *tree.Tree, prof *profile, frames []frame) error {
    98  	last := prof.StartValue
    99  	indexStack := []int{}
   100  	nameStack := []string{}
   101  	precisionMultiplier := prof.Unit.precisionMultiplier()
   102  
   103  	for _, ev := range prof.Events {
   104  		if ev.At < last {
   105  			return fmt.Errorf("Events out of order, %f < %f", ev.At, last)
   106  		}
   107  		fid := int(ev.Frame)
   108  		if fid < 0 || fid >= len(frames) {
   109  			return fmt.Errorf("Invalid frame %d", fid)
   110  		}
   111  
   112  		if ev.Type == eventClose {
   113  			if len(indexStack) == 0 {
   114  				return fmt.Errorf("No stack to close at %f", ev.At)
   115  			}
   116  			lastIdx := len(indexStack) - 1
   117  			if indexStack[lastIdx] != fid {
   118  				return fmt.Errorf("Closing non-open frame %d", fid)
   119  			}
   120  
   121  			// Close this frame
   122  			tr.InsertStackString(nameStack, uint64(ev.At-last)*precisionMultiplier)
   123  			indexStack = indexStack[:lastIdx]
   124  			nameStack = nameStack[:lastIdx]
   125  		} else if ev.Type == eventOpen {
   126  			// Add any time up til now
   127  			if len(nameStack) > 0 {
   128  				tr.InsertStackString(nameStack, uint64(ev.At-last))
   129  			}
   130  
   131  			// Open the frame
   132  			indexStack = append(indexStack, fid)
   133  			nameStack = append(nameStack, frames[fid].Name)
   134  		} else {
   135  			return fmt.Errorf("Unknown event type %s", ev.Type)
   136  		}
   137  
   138  		last = ev.At
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func parseSampled(tr *tree.Tree, prof *profile, frames []frame) error {
   145  	if len(prof.Samples) != len(prof.Weights) {
   146  		return fmt.Errorf("Unequal lengths of samples and weights: %d != %d", len(prof.Samples), len(prof.Weights))
   147  	}
   148  
   149  	precisionMultiplier := prof.Unit.precisionMultiplier()
   150  	stack := []string{}
   151  	for i, samp := range prof.Samples {
   152  		weight := prof.Weights[i]
   153  		if weight < 0 {
   154  			return fmt.Errorf("Negative weight %f", weight)
   155  		}
   156  
   157  		for _, frameID := range samp {
   158  			fid := int(frameID)
   159  			if fid < 0 || fid > len(frames) {
   160  				return fmt.Errorf("Invalid frame %d", fid)
   161  			}
   162  			stack = append(stack, frames[fid].Name)
   163  		}
   164  		tr.InsertStackString(stack, uint64(weight)*precisionMultiplier)
   165  
   166  		stack = stack[:0] // clear, but retain memory
   167  	}
   168  	return nil
   169  }
   170  
   171  // Bytes returns the raw bytes of the profile
   172  func (p *RawProfile) Bytes() ([]byte, error) {
   173  	return p.RawData, nil
   174  }
   175  
   176  // ContentType returns the HTTP ContentType of the profile
   177  func (*RawProfile) ContentType() string {
   178  	return "application/json"
   179  }