1
2
3
4
5
6
7
8 package profile
9
10 import (
11 "bytes"
12 "compress/gzip"
13 "fmt"
14 "io"
15 "strings"
16 "time"
17 )
18
19
20 type Profile struct {
21 SampleType []*ValueType
22 DefaultSampleType string
23 Sample []*Sample
24 Mapping []*Mapping
25 Location []*Location
26 Function []*Function
27 Comments []string
28
29 DropFrames string
30 KeepFrames string
31
32 TimeNanos int64
33 DurationNanos int64
34 PeriodType *ValueType
35 Period int64
36
37 commentX []int64
38 dropFramesX int64
39 keepFramesX int64
40 stringTable []string
41 defaultSampleTypeX int64
42 }
43
44
45 type ValueType struct {
46 Type string
47 Unit string
48
49 typeX int64
50 unitX int64
51 }
52
53
54 type Sample struct {
55 Location []*Location
56 Value []int64
57 Label map[string][]string
58 NumLabel map[string][]int64
59 NumUnit map[string][]string
60
61 locationIDX []uint64
62 labelX []Label
63 }
64
65
66 type Label struct {
67 keyX int64
68
69 strX int64
70 numX int64
71 }
72
73
74 type Mapping struct {
75 ID uint64
76 Start uint64
77 Limit uint64
78 Offset uint64
79 File string
80 BuildID string
81 HasFunctions bool
82 HasFilenames bool
83 HasLineNumbers bool
84 HasInlineFrames bool
85
86 fileX int64
87 buildIDX int64
88 }
89
90
91 type Location struct {
92 ID uint64
93 Mapping *Mapping
94 Address uint64
95 Line []Line
96 IsFolded bool
97
98 mappingIDX uint64
99 }
100
101
102 type Line struct {
103 Function *Function
104 Line int64
105
106 functionIDX uint64
107 }
108
109
110 type Function struct {
111 ID uint64
112 Name string
113 SystemName string
114 Filename string
115 StartLine int64
116
117 nameX int64
118 systemNameX int64
119 filenameX int64
120 }
121
122
123
124 func Parse(r io.Reader) (*Profile, error) {
125 orig, err := io.ReadAll(r)
126 if err != nil {
127 return nil, err
128 }
129
130 if len(orig) >= 2 && orig[0] == 0x1f && orig[1] == 0x8b {
131 gz, err := gzip.NewReader(bytes.NewBuffer(orig))
132 if err != nil {
133 return nil, fmt.Errorf("decompressing profile: %v", err)
134 }
135 data, err := io.ReadAll(gz)
136 if err != nil {
137 return nil, fmt.Errorf("decompressing profile: %v", err)
138 }
139 orig = data
140 }
141
142 p, err := parseUncompressed(orig)
143 if err != nil {
144 return nil, fmt.Errorf("parsing profile: %w", err)
145 }
146
147 if err := p.CheckValid(); err != nil {
148 return nil, fmt.Errorf("malformed profile: %v", err)
149 }
150 return p, nil
151 }
152
153 var errMalformed = fmt.Errorf("malformed profile format")
154 var ErrNoData = fmt.Errorf("empty input file")
155
156 func parseUncompressed(data []byte) (*Profile, error) {
157 if len(data) == 0 {
158 return nil, ErrNoData
159 }
160
161 p := &Profile{}
162 if err := unmarshal(data, p); err != nil {
163 return nil, err
164 }
165
166 if err := p.postDecode(); err != nil {
167 return nil, err
168 }
169
170 return p, nil
171 }
172
173
174 func (p *Profile) Write(w io.Writer) error {
175 p.preEncode()
176 b := marshal(p)
177 zw := gzip.NewWriter(w)
178 if _, err := zw.Write(b); err != nil {
179 zw.Close()
180 return err
181 }
182 return zw.Close()
183 }
184
185
186
187
188
189 func (p *Profile) CheckValid() error {
190
191 sampleLen := len(p.SampleType)
192 if sampleLen == 0 && len(p.Sample) != 0 {
193 return fmt.Errorf("missing sample type information")
194 }
195 for _, s := range p.Sample {
196 if len(s.Value) != sampleLen {
197 return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType))
198 }
199 }
200
201
202
203 mappings := make(map[uint64]*Mapping, len(p.Mapping))
204 for _, m := range p.Mapping {
205 if m.ID == 0 {
206 return fmt.Errorf("found mapping with reserved ID=0")
207 }
208 if mappings[m.ID] != nil {
209 return fmt.Errorf("multiple mappings with same id: %d", m.ID)
210 }
211 mappings[m.ID] = m
212 }
213 functions := make(map[uint64]*Function, len(p.Function))
214 for _, f := range p.Function {
215 if f.ID == 0 {
216 return fmt.Errorf("found function with reserved ID=0")
217 }
218 if functions[f.ID] != nil {
219 return fmt.Errorf("multiple functions with same id: %d", f.ID)
220 }
221 functions[f.ID] = f
222 }
223 locations := make(map[uint64]*Location, len(p.Location))
224 for _, l := range p.Location {
225 if l.ID == 0 {
226 return fmt.Errorf("found location with reserved id=0")
227 }
228 if locations[l.ID] != nil {
229 return fmt.Errorf("multiple locations with same id: %d", l.ID)
230 }
231 locations[l.ID] = l
232 if m := l.Mapping; m != nil {
233 if m.ID == 0 || mappings[m.ID] != m {
234 return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)
235 }
236 }
237 for _, ln := range l.Line {
238 if f := ln.Function; f != nil {
239 if f.ID == 0 || functions[f.ID] != f {
240 return fmt.Errorf("inconsistent function %p: %d", f, f.ID)
241 }
242 }
243 }
244 }
245 return nil
246 }
247
248
249
250
251 func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error {
252 for _, m := range p.Mapping {
253 m.HasInlineFrames = m.HasInlineFrames && inlineFrame
254 m.HasFunctions = m.HasFunctions && function
255 m.HasFilenames = m.HasFilenames && filename
256 m.HasLineNumbers = m.HasLineNumbers && linenumber
257 }
258
259
260 if !function || !filename {
261 for _, f := range p.Function {
262 if !function {
263 f.Name = ""
264 f.SystemName = ""
265 }
266 if !filename {
267 f.Filename = ""
268 }
269 }
270 }
271
272
273 if !inlineFrame || !address || !linenumber {
274 for _, l := range p.Location {
275 if !inlineFrame && len(l.Line) > 1 {
276 l.Line = l.Line[len(l.Line)-1:]
277 }
278 if !linenumber {
279 for i := range l.Line {
280 l.Line[i].Line = 0
281 }
282 }
283 if !address {
284 l.Address = 0
285 }
286 }
287 }
288
289 return p.CheckValid()
290 }
291
292
293
294 func (p *Profile) String() string {
295
296 ss := make([]string, 0, len(p.Sample)+len(p.Mapping)+len(p.Location))
297 if pt := p.PeriodType; pt != nil {
298 ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
299 }
300 ss = append(ss, fmt.Sprintf("Period: %d", p.Period))
301 if p.TimeNanos != 0 {
302 ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))
303 }
304 if p.DurationNanos != 0 {
305 ss = append(ss, fmt.Sprintf("Duration: %v", time.Duration(p.DurationNanos)))
306 }
307
308 ss = append(ss, "Samples:")
309 var sh1 string
310 for _, s := range p.SampleType {
311 sh1 = sh1 + fmt.Sprintf("%s/%s ", s.Type, s.Unit)
312 }
313 ss = append(ss, strings.TrimSpace(sh1))
314 for _, s := range p.Sample {
315 var sv string
316 for _, v := range s.Value {
317 sv = fmt.Sprintf("%s %10d", sv, v)
318 }
319 sv = sv + ": "
320 for _, l := range s.Location {
321 sv = sv + fmt.Sprintf("%d ", l.ID)
322 }
323 ss = append(ss, sv)
324 const labelHeader = " "
325 if len(s.Label) > 0 {
326 ls := labelHeader
327 for k, v := range s.Label {
328 ls = ls + fmt.Sprintf("%s:%v ", k, v)
329 }
330 ss = append(ss, ls)
331 }
332 if len(s.NumLabel) > 0 {
333 ls := labelHeader
334 for k, v := range s.NumLabel {
335 ls = ls + fmt.Sprintf("%s:%v ", k, v)
336 }
337 ss = append(ss, ls)
338 }
339 }
340
341 ss = append(ss, "Locations")
342 for _, l := range p.Location {
343 locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
344 if m := l.Mapping; m != nil {
345 locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
346 }
347 if len(l.Line) == 0 {
348 ss = append(ss, locStr)
349 }
350 for li := range l.Line {
351 lnStr := "??"
352 if fn := l.Line[li].Function; fn != nil {
353 lnStr = fmt.Sprintf("%s %s:%d s=%d",
354 fn.Name,
355 fn.Filename,
356 l.Line[li].Line,
357 fn.StartLine)
358 if fn.Name != fn.SystemName {
359 lnStr = lnStr + "(" + fn.SystemName + ")"
360 }
361 }
362 ss = append(ss, locStr+lnStr)
363
364 locStr = " "
365 }
366 }
367
368 ss = append(ss, "Mappings")
369 for _, m := range p.Mapping {
370 bits := ""
371 if m.HasFunctions {
372 bits += "[FN]"
373 }
374 if m.HasFilenames {
375 bits += "[FL]"
376 }
377 if m.HasLineNumbers {
378 bits += "[LN]"
379 }
380 if m.HasInlineFrames {
381 bits += "[IN]"
382 }
383 ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
384 m.ID,
385 m.Start, m.Limit, m.Offset,
386 m.File,
387 m.BuildID,
388 bits))
389 }
390
391 return strings.Join(ss, "\n") + "\n"
392 }
393
394
395
396
397
398 func (p *Profile) Merge(pb *Profile, r float64) error {
399 if err := p.Compatible(pb); err != nil {
400 return err
401 }
402
403 pb = pb.Copy()
404
405
406 if pb.Period > p.Period {
407 p.Period = pb.Period
408 }
409
410 p.DurationNanos += pb.DurationNanos
411
412 p.Mapping = append(p.Mapping, pb.Mapping...)
413 for i, m := range p.Mapping {
414 m.ID = uint64(i + 1)
415 }
416 p.Location = append(p.Location, pb.Location...)
417 for i, l := range p.Location {
418 l.ID = uint64(i + 1)
419 }
420 p.Function = append(p.Function, pb.Function...)
421 for i, f := range p.Function {
422 f.ID = uint64(i + 1)
423 }
424
425 if r != 1.0 {
426 for _, s := range pb.Sample {
427 for i, v := range s.Value {
428 s.Value[i] = int64((float64(v) * r))
429 }
430 }
431 }
432 p.Sample = append(p.Sample, pb.Sample...)
433 return p.CheckValid()
434 }
435
436
437
438
439 func (p *Profile) Compatible(pb *Profile) error {
440 if !compatibleValueTypes(p.PeriodType, pb.PeriodType) {
441 return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType)
442 }
443
444 if len(p.SampleType) != len(pb.SampleType) {
445 return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
446 }
447
448 for i := range p.SampleType {
449 if !compatibleValueTypes(p.SampleType[i], pb.SampleType[i]) {
450 return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
451 }
452 }
453
454 return nil
455 }
456
457
458
459 func (p *Profile) HasFunctions() bool {
460 for _, l := range p.Location {
461 if l.Mapping == nil || !l.Mapping.HasFunctions {
462 return false
463 }
464 }
465 return true
466 }
467
468
469
470 func (p *Profile) HasFileLines() bool {
471 for _, l := range p.Location {
472 if l.Mapping == nil || (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {
473 return false
474 }
475 }
476 return true
477 }
478
479 func compatibleValueTypes(v1, v2 *ValueType) bool {
480 if v1 == nil || v2 == nil {
481 return true
482 }
483 return v1.Type == v2.Type && v1.Unit == v2.Unit
484 }
485
486
487 func (p *Profile) Copy() *Profile {
488 p.preEncode()
489 b := marshal(p)
490
491 pp := &Profile{}
492 if err := unmarshal(b, pp); err != nil {
493 panic(err)
494 }
495 if err := pp.postDecode(); err != nil {
496 panic(err)
497 }
498
499 return pp
500 }
501
502
503
504
505 type Demangler func(name []string) (map[string]string, error)
506
507
508
509
510 func (p *Profile) Demangle(d Demangler) error {
511
512 var names []string
513 for _, fn := range p.Function {
514 names = append(names, fn.SystemName)
515 }
516
517
518 demangled, err := d(names)
519 if err != nil {
520 return err
521 }
522 for _, fn := range p.Function {
523 if dd, ok := demangled[fn.SystemName]; ok {
524 fn.Name = dd
525 }
526 }
527 return nil
528 }
529
530
531 func (p *Profile) Empty() bool {
532 return len(p.Sample) == 0
533 }
534
535
536 func (p *Profile) Scale(ratio float64) {
537 if ratio == 1 {
538 return
539 }
540 ratios := make([]float64, len(p.SampleType))
541 for i := range p.SampleType {
542 ratios[i] = ratio
543 }
544 p.ScaleN(ratios)
545 }
546
547
548 func (p *Profile) ScaleN(ratios []float64) error {
549 if len(p.SampleType) != len(ratios) {
550 return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))
551 }
552 allOnes := true
553 for _, r := range ratios {
554 if r != 1 {
555 allOnes = false
556 break
557 }
558 }
559 if allOnes {
560 return nil
561 }
562 for _, s := range p.Sample {
563 for i, v := range s.Value {
564 if ratios[i] != 1 {
565 s.Value[i] = int64(float64(v) * ratios[i])
566 }
567 }
568 }
569 return nil
570 }
571
View as plain text