1 // Copyright 2014 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 15 // Package profile provides a representation of profile.proto and
16 // methods to encode/decode profiles in this format.
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 // Profile is an in-memory representation of profile.proto.
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 // The following fields are modified during encoding and copying,
54 // so are protected by a Mutex.
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 // ValueType corresponds to Profile.ValueType
66 type ValueType struct {
67 Type string // cpu, wall, inuse_space, etc
68 Unit string // seconds, nanoseconds, bytes, etc
69 70 typeX int64
71 unitX int64
72 }
73 74 // Sample corresponds to Profile.Sample
75 type Sample struct {
76 Location []*Location
77 Value []int64
78 // Label is a per-label-key map to values for string labels.
79 //
80 // In general, having multiple values for the given label key is strongly
81 // discouraged - see docs for the sample label field in profile.proto. The
82 // main reason this unlikely state is tracked here is to make the
83 // decoding->encoding roundtrip not lossy. But we expect that the value
84 // slices present in this map are always of length 1.
85 Label map[string][]string
86 // NumLabel is a per-label-key map to values for numeric labels. See a note
87 // above on handling multiple values for a label.
88 NumLabel map[string][]int64
89 // NumUnit is a per-label-key map to the unit names of corresponding numeric
90 // label values. The unit info may be missing even if the label is in
91 // NumLabel, see the docs in profile.proto for details. When the value is
92 // slice is present and not nil, its length must be equal to the length of
93 // the corresponding value slice in NumLabel.
94 NumUnit map[string][]string
95 96 locationIDX []uint64
97 labelX []label
98 }
99 100 // label corresponds to Profile.Label
101 type label struct {
102 keyX int64
103 // Exactly one of the two following values must be set
104 strX int64
105 numX int64 // Integer value for this label
106 // can be set if numX has value
107 unitX int64
108 }
109 110 // Mapping corresponds to Profile.Mapping
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 // Name of the kernel relocation symbol ("_text" or "_stext"), extracted from File.
127 // For linux kernel mappings generated by some tools, correct symbolization depends
128 // on knowing which of the two possible relocation symbols was used for `Start`.
129 // This is given to us as a suffix in `File` (e.g. "[kernel.kallsyms]_stext").
130 //
131 // Note, this public field is not persisted in the proto. For the purposes of
132 // copying / merging / hashing profiles, it is considered subsumed by `File`.
133 KernelRelocationSymbol string
134 }
135 136 // Location corresponds to Profile.Location
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 // Line corresponds to Profile.Line
148 type Line struct {
149 Function *Function
150 Line int64
151 Column int64
152 153 functionIDX uint64
154 }
155 156 // Function corresponds to Profile.Function
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 // Parse parses a profile and checks for its validity. The input
170 // may be a gzip-compressed encoded protobuf or one of many legacy
171 // profile formats which may be unsupported in the future.
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 // ParseData parses a profile from a buffer and checks for its
181 // validity.
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, // goroutine, threadcreate
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 // ParseUncompressed parses an uncompressed protobuf into a profile.
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 // massageMappings applies heuristic-based changes to the profile
256 // mappings to account for quirks of some environments.
257 func (p *Profile) massageMappings() {
258 // Merge adjacent regions with matching names, checking that the offsets match
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 // Use heuristics to identify main binary and move it to the top of the list of mappings
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 // Swap what we guess is main to position 0.
292 p.Mapping[0], p.Mapping[i] = p.Mapping[i], p.Mapping[0]
293 break
294 }
295 296 // Keep the mapping IDs neatly sorted
297 for i, m := range p.Mapping {
298 m.ID = uint64(i + 1)
299 }
300 }
301 302 // adjacent returns whether two mapping entries represent the same
303 // mapping that has been split into two. Check that their addresses are adjacent,
304 // and if the offsets match, if they are available.
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 // Write writes the profile as a gzip-compressed marshaled protobuf.
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 // WriteUncompressed writes the profile as a marshaled protobuf.
353 func (p *Profile) WriteUncompressed(w io.Writer) error {
354 _, err := w.Write(serialize(p))
355 return err
356 }
357 358 // CheckValid tests whether the profile is valid. Checks include, but are
359 // not limited to:
360 // - len(Profile.Sample[n].value) == len(Profile.value_unit)
361 // - Sample.id has a corresponding Profile.Location
362 func (p *Profile) CheckValid() error {
363 // Check that sample values are consistent
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 // Check that all mappings/locations/functions are in the tables
383 // Check that there are no duplicate ids
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 // Aggregate merges the locations in the profile into equivalence
441 // classes preserving the request attributes. It also updates the
442 // samples to point to the merged locations.
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 // Aggregate functions
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 // Aggregate locations
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 // NumLabelUnits returns a map of numeric label keys to the units
491 // associated with those keys and a map of those keys to any units
492 // that were encountered but not used.
493 // Unit for a given key is the first encountered unit for that key. If multiple
494 // units are encountered for values paired with a particular key, then the first
495 // unit encountered is used and all other units are returned in sorted order
496 // in map of ignored units.
497 // If no units are encountered for a particular key, the unit is then inferred
498 // based on the key.
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 // Determine units based on numeric tags for each sample.
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 // Infer units for keys without any units associated with
525 // numeric tag values.
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 // Copy ignored units into more readable format
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 // String dumps a text representation of a profile. Intended mainly
555 // for debugging purposes.
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 // string dumps a text representation of a mapping. Intended mainly
603 // for debugging purposes.
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 // string dumps a text representation of a location. Intended mainly
627 // for debugging purposes.
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 // Do not print location details past the first line
655 locStr = " "
656 }
657 return strings.Join(ss, "\n")
658 }
659 660 // string dumps a text representation of a sample. Intended mainly
661 // for debugging purposes.
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 // labelsToString returns a string representation of a
684 // map representing labels.
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 // numLabelsToString returns a string representation of a map
695 // representing numeric labels.
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 // SetLabel sets the specified key to the specified value for all samples in the
717 // profile.
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 // RemoveLabel removes all labels associated with the specified key for all
729 // samples in the profile.
730 func (p *Profile) RemoveLabel(key string) {
731 for _, sample := range p.Sample {
732 delete(sample.Label, key)
733 }
734 }
735 736 // HasLabel returns true if a sample has a label with indicated key and value.
737 func (s *Sample) HasLabel(key, value string) bool {
738 return slices.Contains(s.Label[key], value)
739 }
740 741 // SetNumLabel sets the specified key to the specified value for all samples in the
742 // profile. "unit" is a slice that describes the units that each corresponding member
743 // of "values" is measured in (e.g. bytes or seconds). If there is no relevant
744 // unit for a given value, that member of "unit" should be the empty string.
745 // "unit" must either have the same length as "value", or be nil.
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 // RemoveNumLabel removes all numerical labels associated with the specified key for all
762 // samples in the profile.
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 // DiffBaseSample returns true if a sample belongs to the diff base and false
771 // otherwise.
772 func (s *Sample) DiffBaseSample() bool {
773 return s.HasLabel("pprof::base", "true")
774 }
775 776 // Scale multiplies all sample values in a profile by a constant and keeps
777 // only samples that have at least one non-zero value.
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 // ScaleN multiplies each sample values in a sample by a different amount
790 // and keeps only samples that have at least one non-zero value.
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 // HasFunctions determines if all locations in this profile have
825 // symbolized function information.
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 // HasFileLines determines if all locations in this profile have
836 // symbolized file and line number information.
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 // Unsymbolizable returns true if a mapping points to a binary for which
847 // locations can't be symbolized in principle, at least now. Examples are
848 // "[vdso]", "[vsyscall]" and some others, see the code.
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 // Copy makes a fully independent copy of a profile.
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