profile.mx raw

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