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 }