1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package profile
18
19 import (
20 "bytes"
21 "compress/gzip"
22 "fmt"
23 "io"
24 "math"
25 "path/filepath"
26 "regexp"
27 "slices"
28 "sort"
29 "strings"
30 "sync"
31 "time"
32 )
33
34
35 type Profile struct {
36 SampleType []*ValueType
37 DefaultSampleType string
38 Sample []*Sample
39 Mapping []*Mapping
40 Location []*Location
41 Function []*Function
42 Comments []string
43 DocURL string
44
45 DropFrames string
46 KeepFrames string
47
48 TimeNanos int64
49 DurationNanos int64
50 PeriodType *ValueType
51 Period int64
52
53
54
55 encodeMu sync.Mutex
56
57 commentX []int64
58 docURLX int64
59 dropFramesX int64
60 keepFramesX int64
61 stringTable []string
62 defaultSampleTypeX int64
63 }
64
65
66 type ValueType struct {
67 Type string
68 Unit string
69
70 typeX int64
71 unitX int64
72 }
73
74
75 type Sample struct {
76 Location []*Location
77 Value []int64
78
79
80
81
82
83
84
85 Label map[string][]string
86
87
88 NumLabel map[string][]int64
89
90
91
92
93
94 NumUnit map[string][]string
95
96 locationIDX []uint64
97 labelX []label
98 }
99
100
101 type label struct {
102 keyX int64
103
104 strX int64
105 numX int64
106
107 unitX int64
108 }
109
110
111 type Mapping struct {
112 ID uint64
113 Start uint64
114 Limit uint64
115 Offset uint64
116 File string
117 BuildID string
118 HasFunctions bool
119 HasFilenames bool
120 HasLineNumbers bool
121 HasInlineFrames bool
122
123 fileX int64
124 buildIDX int64
125
126
127
128
129
130
131
132
133 KernelRelocationSymbol string
134 }
135
136
137 type Location struct {
138 ID uint64
139 Mapping *Mapping
140 Address uint64
141 Line []Line
142 IsFolded bool
143
144 mappingIDX uint64
145 }
146
147
148 type Line struct {
149 Function *Function
150 Line int64
151 Column int64
152
153 functionIDX uint64
154 }
155
156
157 type Function struct {
158 ID uint64
159 Name string
160 SystemName string
161 Filename string
162 StartLine int64
163
164 nameX int64
165 systemNameX int64
166 filenameX int64
167 }
168
169
170
171
172 func Parse(r io.Reader) (*Profile, error) {
173 data, err := io.ReadAll(r)
174 if err != nil {
175 return nil, err
176 }
177 return ParseData(data)
178 }
179
180
181
182 func ParseData(data []byte) (*Profile, error) {
183 var p *Profile
184 var err error
185 if len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b {
186 gz, err := gzip.NewReader(bytes.NewBuffer(data))
187 if err == nil {
188 data, err = io.ReadAll(gz)
189 }
190 if err != nil {
191 return nil, fmt.Errorf("decompressing profile: %v", err)
192 }
193 }
194 if p, err = ParseUncompressed(data); err != nil && err != errNoData && err != errConcatProfile {
195 p, err = parseLegacy(data)
196 }
197
198 if err != nil {
199 return nil, fmt.Errorf("parsing profile: %v", err)
200 }
201
202 if err := p.CheckValid(); err != nil {
203 return nil, fmt.Errorf("malformed profile: %v", err)
204 }
205 return p, nil
206 }
207
208 var errUnrecognized = fmt.Errorf("unrecognized profile format")
209 var errMalformed = fmt.Errorf("malformed profile format")
210 var errNoData = fmt.Errorf("empty input file")
211 var errConcatProfile = fmt.Errorf("concatenated profiles detected")
212
213 func parseLegacy(data []byte) (*Profile, error) {
214 parsers := []func([]byte) (*Profile, error){
215 parseCPU,
216 parseHeap,
217 parseGoCount,
218 parseThread,
219 parseContention,
220 parseJavaProfile,
221 }
222
223 for _, parser := range parsers {
224 p, err := parser(data)
225 if err == nil {
226 p.addLegacyFrameInfo()
227 return p, nil
228 }
229 if err != errUnrecognized {
230 return nil, err
231 }
232 }
233 return nil, errUnrecognized
234 }
235
236
237 func ParseUncompressed(data []byte) (*Profile, error) {
238 if len(data) == 0 {
239 return nil, errNoData
240 }
241 p := &Profile{}
242 if err := unmarshal(data, p); err != nil {
243 return nil, err
244 }
245
246 if err := p.postDecode(); err != nil {
247 return nil, err
248 }
249
250 return p, nil
251 }
252
253 var libRx = regexp.MustCompile(`([.]so$|[.]so[._][0-9]+)`)
254
255
256
257 func (p *Profile) massageMappings() {
258
259 if len(p.Mapping) > 1 {
260 mappings := []*Mapping{p.Mapping[0]}
261 for _, m := range p.Mapping[1:] {
262 lm := mappings[len(mappings)-1]
263 if adjacent(lm, m) {
264 lm.Limit = m.Limit
265 if m.File != "" {
266 lm.File = m.File
267 }
268 if m.BuildID != "" {
269 lm.BuildID = m.BuildID
270 }
271 p.updateLocationMapping(m, lm)
272 continue
273 }
274 mappings = append(mappings, m)
275 }
276 p.Mapping = mappings
277 }
278
279
280 for i, m := range p.Mapping {
281 file := strings.TrimSpace(strings.Replace(m.File, "(deleted)", "", -1))
282 if len(file) == 0 {
283 continue
284 }
285 if len(libRx.FindStringSubmatch(file)) > 0 {
286 continue
287 }
288 if file[0] == '[' {
289 continue
290 }
291
292 p.Mapping[0], p.Mapping[i] = p.Mapping[i], p.Mapping[0]
293 break
294 }
295
296
297 for i, m := range p.Mapping {
298 m.ID = uint64(i + 1)
299 }
300 }
301
302
303
304
305 func adjacent(m1, m2 *Mapping) bool {
306 if m1.File != "" && m2.File != "" {
307 if m1.File != m2.File {
308 return false
309 }
310 }
311 if m1.BuildID != "" && m2.BuildID != "" {
312 if m1.BuildID != m2.BuildID {
313 return false
314 }
315 }
316 if m1.Limit != m2.Start {
317 return false
318 }
319 if m1.Offset != 0 && m2.Offset != 0 {
320 offset := m1.Offset + (m1.Limit - m1.Start)
321 if offset != m2.Offset {
322 return false
323 }
324 }
325 return true
326 }
327
328 func (p *Profile) updateLocationMapping(from, to *Mapping) {
329 for _, l := range p.Location {
330 if l.Mapping == from {
331 l.Mapping = to
332 }
333 }
334 }
335
336 func serialize(p *Profile) []byte {
337 p.encodeMu.Lock()
338 p.preEncode()
339 b := marshal(p)
340 p.encodeMu.Unlock()
341 return b
342 }
343
344
345 func (p *Profile) Write(w io.Writer) error {
346 zw := gzip.NewWriter(w)
347 defer zw.Close()
348 _, err := zw.Write(serialize(p))
349 return err
350 }
351
352
353 func (p *Profile) WriteUncompressed(w io.Writer) error {
354 _, err := w.Write(serialize(p))
355 return err
356 }
357
358
359
360
361
362 func (p *Profile) CheckValid() error {
363
364 sampleLen := len(p.SampleType)
365 if sampleLen == 0 && len(p.Sample) != 0 {
366 return fmt.Errorf("missing sample type information")
367 }
368 for _, s := range p.Sample {
369 if s == nil {
370 return fmt.Errorf("profile has nil sample")
371 }
372 if len(s.Value) != sampleLen {
373 return fmt.Errorf("mismatch: sample has %d values vs. %d types", len(s.Value), len(p.SampleType))
374 }
375 for _, l := range s.Location {
376 if l == nil {
377 return fmt.Errorf("sample has nil location")
378 }
379 }
380 }
381
382
383
384 mappings := make(map[uint64]*Mapping, len(p.Mapping))
385 for _, m := range p.Mapping {
386 if m == nil {
387 return fmt.Errorf("profile has nil mapping")
388 }
389 if m.ID == 0 {
390 return fmt.Errorf("found mapping with reserved ID=0")
391 }
392 if mappings[m.ID] != nil {
393 return fmt.Errorf("multiple mappings with same id: %d", m.ID)
394 }
395 mappings[m.ID] = m
396 }
397 functions := make(map[uint64]*Function, len(p.Function))
398 for _, f := range p.Function {
399 if f == nil {
400 return fmt.Errorf("profile has nil function")
401 }
402 if f.ID == 0 {
403 return fmt.Errorf("found function with reserved ID=0")
404 }
405 if functions[f.ID] != nil {
406 return fmt.Errorf("multiple functions with same id: %d", f.ID)
407 }
408 functions[f.ID] = f
409 }
410 locations := make(map[uint64]*Location, len(p.Location))
411 for _, l := range p.Location {
412 if l == nil {
413 return fmt.Errorf("profile has nil location")
414 }
415 if l.ID == 0 {
416 return fmt.Errorf("found location with reserved id=0")
417 }
418 if locations[l.ID] != nil {
419 return fmt.Errorf("multiple locations with same id: %d", l.ID)
420 }
421 locations[l.ID] = l
422 if m := l.Mapping; m != nil {
423 if m.ID == 0 || mappings[m.ID] != m {
424 return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)
425 }
426 }
427 for _, ln := range l.Line {
428 f := ln.Function
429 if f == nil {
430 return fmt.Errorf("location id: %d has a line with nil function", l.ID)
431 }
432 if f.ID == 0 || functions[f.ID] != f {
433 return fmt.Errorf("inconsistent function %p: %d", f, f.ID)
434 }
435 }
436 }
437 return nil
438 }
439
440
441
442
443 func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, columnnumber, address bool) error {
444 for _, m := range p.Mapping {
445 m.HasInlineFrames = m.HasInlineFrames && inlineFrame
446 m.HasFunctions = m.HasFunctions && function
447 m.HasFilenames = m.HasFilenames && filename
448 m.HasLineNumbers = m.HasLineNumbers && linenumber
449 }
450
451
452 if !function || !filename {
453 for _, f := range p.Function {
454 if !function {
455 f.Name = ""
456 f.SystemName = ""
457 }
458 if !filename {
459 f.Filename = ""
460 }
461 }
462 }
463
464
465 if !inlineFrame || !address || !linenumber || !columnnumber {
466 for _, l := range p.Location {
467 if !inlineFrame && len(l.Line) > 1 {
468 l.Line = l.Line[len(l.Line)-1:]
469 }
470 if !linenumber {
471 for i := range l.Line {
472 l.Line[i].Line = 0
473 l.Line[i].Column = 0
474 }
475 }
476 if !columnnumber {
477 for i := range l.Line {
478 l.Line[i].Column = 0
479 }
480 }
481 if !address {
482 l.Address = 0
483 }
484 }
485 }
486
487 return p.CheckValid()
488 }
489
490
491
492
493
494
495
496
497
498
499 func (p *Profile) NumLabelUnits() (map[string]string, map[string][]string) {
500 numLabelUnits := map[string]string{}
501 ignoredUnits := map[string]map[string]bool{}
502 encounteredKeys := map[string]bool{}
503
504
505 for _, s := range p.Sample {
506 for k := range s.NumLabel {
507 encounteredKeys[k] = true
508 for _, unit := range s.NumUnit[k] {
509 if unit == "" {
510 continue
511 }
512 if wantUnit, ok := numLabelUnits[k]; !ok {
513 numLabelUnits[k] = unit
514 } else if wantUnit != unit {
515 if v, ok := ignoredUnits[k]; ok {
516 v[unit] = true
517 } else {
518 ignoredUnits[k] = map[string]bool{unit: true}
519 }
520 }
521 }
522 }
523 }
524
525
526 for key := range encounteredKeys {
527 unit := numLabelUnits[key]
528 if unit == "" {
529 switch key {
530 case "alignment", "request":
531 numLabelUnits[key] = "bytes"
532 default:
533 numLabelUnits[key] = key
534 }
535 }
536 }
537
538
539 unitsIgnored := make(map[string][]string, len(ignoredUnits))
540 for key, values := range ignoredUnits {
541 units := make([]string, len(values))
542 i := 0
543 for unit := range values {
544 units[i] = unit
545 i++
546 }
547 sort.Strings(units)
548 unitsIgnored[key] = units
549 }
550
551 return numLabelUnits, unitsIgnored
552 }
553
554
555
556 func (p *Profile) String() string {
557 ss := make([]string, 0, len(p.Comments)+len(p.Sample)+len(p.Mapping)+len(p.Location))
558 for _, c := range p.Comments {
559 ss = append(ss, "Comment: "+c)
560 }
561 if url := p.DocURL; url != "" {
562 ss = append(ss, fmt.Sprintf("Doc: %s", url))
563 }
564 if pt := p.PeriodType; pt != nil {
565 ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
566 }
567 ss = append(ss, fmt.Sprintf("Period: %d", p.Period))
568 if p.TimeNanos != 0 {
569 ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))
570 }
571 if p.DurationNanos != 0 {
572 ss = append(ss, fmt.Sprintf("Duration: %.4v", time.Duration(p.DurationNanos)))
573 }
574
575 ss = append(ss, "Samples:")
576 var sh1 string
577 for _, s := range p.SampleType {
578 dflt := ""
579 if s.Type == p.DefaultSampleType {
580 dflt = "[dflt]"
581 }
582 sh1 = sh1 + fmt.Sprintf("%s/%s%s ", s.Type, s.Unit, dflt)
583 }
584 ss = append(ss, strings.TrimSpace(sh1))
585 for _, s := range p.Sample {
586 ss = append(ss, s.string())
587 }
588
589 ss = append(ss, "Locations")
590 for _, l := range p.Location {
591 ss = append(ss, l.string())
592 }
593
594 ss = append(ss, "Mappings")
595 for _, m := range p.Mapping {
596 ss = append(ss, m.string())
597 }
598
599 return strings.Join(ss, "\n") + "\n"
600 }
601
602
603
604 func (m *Mapping) string() string {
605 bits := ""
606 if m.HasFunctions {
607 bits = bits + "[FN]"
608 }
609 if m.HasFilenames {
610 bits = bits + "[FL]"
611 }
612 if m.HasLineNumbers {
613 bits = bits + "[LN]"
614 }
615 if m.HasInlineFrames {
616 bits = bits + "[IN]"
617 }
618 return fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
619 m.ID,
620 m.Start, m.Limit, m.Offset,
621 m.File,
622 m.BuildID,
623 bits)
624 }
625
626
627
628 func (l *Location) string() string {
629 ss := []string{}
630 locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
631 if m := l.Mapping; m != nil {
632 locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
633 }
634 if l.IsFolded {
635 locStr = locStr + "[F] "
636 }
637 if len(l.Line) == 0 {
638 ss = append(ss, locStr)
639 }
640 for li := range l.Line {
641 lnStr := "??"
642 if fn := l.Line[li].Function; fn != nil {
643 lnStr = fmt.Sprintf("%s %s:%d:%d s=%d",
644 fn.Name,
645 fn.Filename,
646 l.Line[li].Line,
647 l.Line[li].Column,
648 fn.StartLine)
649 if fn.Name != fn.SystemName {
650 lnStr = lnStr + "(" + fn.SystemName + ")"
651 }
652 }
653 ss = append(ss, locStr+lnStr)
654
655 locStr = " "
656 }
657 return strings.Join(ss, "\n")
658 }
659
660
661
662 func (s *Sample) string() string {
663 ss := []string{}
664 var sv string
665 for _, v := range s.Value {
666 sv = fmt.Sprintf("%s %10d", sv, v)
667 }
668 sv = sv + ": "
669 for _, l := range s.Location {
670 sv = sv + fmt.Sprintf("%d ", l.ID)
671 }
672 ss = append(ss, sv)
673 const labelHeader = " "
674 if len(s.Label) > 0 {
675 ss = append(ss, labelHeader+labelsToString(s.Label))
676 }
677 if len(s.NumLabel) > 0 {
678 ss = append(ss, labelHeader+numLabelsToString(s.NumLabel, s.NumUnit))
679 }
680 return strings.Join(ss, "\n")
681 }
682
683
684
685 func labelsToString(labels map[string][]string) string {
686 ls := []string{}
687 for k, v := range labels {
688 ls = append(ls, fmt.Sprintf("%s:%v", k, v))
689 }
690 sort.Strings(ls)
691 return strings.Join(ls, " ")
692 }
693
694
695
696 func numLabelsToString(numLabels map[string][]int64, numUnits map[string][]string) string {
697 ls := []string{}
698 for k, v := range numLabels {
699 units := numUnits[k]
700 var labelString string
701 if len(units) == len(v) {
702 values := make([]string, len(v))
703 for i, vv := range v {
704 values[i] = fmt.Sprintf("%d %s", vv, units[i])
705 }
706 labelString = fmt.Sprintf("%s:%v", k, values)
707 } else {
708 labelString = fmt.Sprintf("%s:%v", k, v)
709 }
710 ls = append(ls, labelString)
711 }
712 sort.Strings(ls)
713 return strings.Join(ls, " ")
714 }
715
716
717
718 func (p *Profile) SetLabel(key string, value []string) {
719 for _, sample := range p.Sample {
720 if sample.Label == nil {
721 sample.Label = map[string][]string{key: value}
722 } else {
723 sample.Label[key] = value
724 }
725 }
726 }
727
728
729
730 func (p *Profile) RemoveLabel(key string) {
731 for _, sample := range p.Sample {
732 delete(sample.Label, key)
733 }
734 }
735
736
737 func (s *Sample) HasLabel(key, value string) bool {
738 return slices.Contains(s.Label[key], value)
739 }
740
741
742
743
744
745
746 func (p *Profile) SetNumLabel(key string, value []int64, unit []string) {
747 for _, sample := range p.Sample {
748 if sample.NumLabel == nil {
749 sample.NumLabel = map[string][]int64{key: value}
750 } else {
751 sample.NumLabel[key] = value
752 }
753 if sample.NumUnit == nil {
754 sample.NumUnit = map[string][]string{key: unit}
755 } else {
756 sample.NumUnit[key] = unit
757 }
758 }
759 }
760
761
762
763 func (p *Profile) RemoveNumLabel(key string) {
764 for _, sample := range p.Sample {
765 delete(sample.NumLabel, key)
766 delete(sample.NumUnit, key)
767 }
768 }
769
770
771
772 func (s *Sample) DiffBaseSample() bool {
773 return s.HasLabel("pprof::base", "true")
774 }
775
776
777
778 func (p *Profile) Scale(ratio float64) {
779 if ratio == 1 {
780 return
781 }
782 ratios := make([]float64, len(p.SampleType))
783 for i := range p.SampleType {
784 ratios[i] = ratio
785 }
786 p.ScaleN(ratios)
787 }
788
789
790
791 func (p *Profile) ScaleN(ratios []float64) error {
792 if len(p.SampleType) != len(ratios) {
793 return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))
794 }
795 allOnes := true
796 for _, r := range ratios {
797 if r != 1 {
798 allOnes = false
799 break
800 }
801 }
802 if allOnes {
803 return nil
804 }
805 fillIdx := 0
806 for _, s := range p.Sample {
807 keepSample := false
808 for i, v := range s.Value {
809 if ratios[i] != 1 {
810 val := int64(math.Round(float64(v) * ratios[i]))
811 s.Value[i] = val
812 keepSample = keepSample || val != 0
813 }
814 }
815 if keepSample {
816 p.Sample[fillIdx] = s
817 fillIdx++
818 }
819 }
820 p.Sample = p.Sample[:fillIdx]
821 return nil
822 }
823
824
825
826 func (p *Profile) HasFunctions() bool {
827 for _, l := range p.Location {
828 if l.Mapping != nil && !l.Mapping.HasFunctions {
829 return false
830 }
831 }
832 return true
833 }
834
835
836
837 func (p *Profile) HasFileLines() bool {
838 for _, l := range p.Location {
839 if l.Mapping != nil && (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {
840 return false
841 }
842 }
843 return true
844 }
845
846
847
848
849 func (m *Mapping) Unsymbolizable() bool {
850 name := filepath.Base(m.File)
851 switch {
852 case strings.HasPrefix(name, "["):
853 case strings.HasPrefix(name, "linux-vdso"):
854 case strings.HasPrefix(m.File, "/dev/dri/"):
855 case m.File == "//anon":
856 case m.File == "":
857 case strings.HasPrefix(m.File, "/memfd:"):
858 default:
859 return false
860 }
861 return true
862 }
863
864
865 func (p *Profile) Copy() *Profile {
866 pp := &Profile{}
867 if err := unmarshal(serialize(p), pp); err != nil {
868 panic(err)
869 }
870 if err := pp.postDecode(); err != nil {
871 panic(err)
872 }
873
874 return pp
875 }
876
View as plain text