profile.go raw

   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