Source file src/cmd/vendor/github.com/google/pprof/profile/profile.go

     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  

View as plain text