Source file src/internal/profile/profile.go

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

View as plain text