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  	defer zw.Close()
   179  	_, err := zw.Write(b)
   180  	return err
   181  }
   182  
   183  // CheckValid tests whether the profile is valid. Checks include, but are
   184  // not limited to:
   185  //   - len(Profile.Sample[n].value) == len(Profile.value_unit)
   186  //   - Sample.id has a corresponding Profile.Location
   187  func (p *Profile) CheckValid() error {
   188  	// Check that sample values are consistent
   189  	sampleLen := len(p.SampleType)
   190  	if sampleLen == 0 && len(p.Sample) != 0 {
   191  		return fmt.Errorf("missing sample type information")
   192  	}
   193  	for _, s := range p.Sample {
   194  		if len(s.Value) != sampleLen {
   195  			return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType))
   196  		}
   197  	}
   198  
   199  	// Check that all mappings/locations/functions are in the tables
   200  	// Check that there are no duplicate ids
   201  	mappings := make(map[uint64]*Mapping, len(p.Mapping))
   202  	for _, m := range p.Mapping {
   203  		if m.ID == 0 {
   204  			return fmt.Errorf("found mapping with reserved ID=0")
   205  		}
   206  		if mappings[m.ID] != nil {
   207  			return fmt.Errorf("multiple mappings with same id: %d", m.ID)
   208  		}
   209  		mappings[m.ID] = m
   210  	}
   211  	functions := make(map[uint64]*Function, len(p.Function))
   212  	for _, f := range p.Function {
   213  		if f.ID == 0 {
   214  			return fmt.Errorf("found function with reserved ID=0")
   215  		}
   216  		if functions[f.ID] != nil {
   217  			return fmt.Errorf("multiple functions with same id: %d", f.ID)
   218  		}
   219  		functions[f.ID] = f
   220  	}
   221  	locations := make(map[uint64]*Location, len(p.Location))
   222  	for _, l := range p.Location {
   223  		if l.ID == 0 {
   224  			return fmt.Errorf("found location with reserved id=0")
   225  		}
   226  		if locations[l.ID] != nil {
   227  			return fmt.Errorf("multiple locations with same id: %d", l.ID)
   228  		}
   229  		locations[l.ID] = l
   230  		if m := l.Mapping; m != nil {
   231  			if m.ID == 0 || mappings[m.ID] != m {
   232  				return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID)
   233  			}
   234  		}
   235  		for _, ln := range l.Line {
   236  			if f := ln.Function; f != nil {
   237  				if f.ID == 0 || functions[f.ID] != f {
   238  					return fmt.Errorf("inconsistent function %p: %d", f, f.ID)
   239  				}
   240  			}
   241  		}
   242  	}
   243  	return nil
   244  }
   245  
   246  // Aggregate merges the locations in the profile into equivalence
   247  // classes preserving the request attributes. It also updates the
   248  // samples to point to the merged locations.
   249  func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error {
   250  	for _, m := range p.Mapping {
   251  		m.HasInlineFrames = m.HasInlineFrames && inlineFrame
   252  		m.HasFunctions = m.HasFunctions && function
   253  		m.HasFilenames = m.HasFilenames && filename
   254  		m.HasLineNumbers = m.HasLineNumbers && linenumber
   255  	}
   256  
   257  	// Aggregate functions
   258  	if !function || !filename {
   259  		for _, f := range p.Function {
   260  			if !function {
   261  				f.Name = ""
   262  				f.SystemName = ""
   263  			}
   264  			if !filename {
   265  				f.Filename = ""
   266  			}
   267  		}
   268  	}
   269  
   270  	// Aggregate locations
   271  	if !inlineFrame || !address || !linenumber {
   272  		for _, l := range p.Location {
   273  			if !inlineFrame && len(l.Line) > 1 {
   274  				l.Line = l.Line[len(l.Line)-1:]
   275  			}
   276  			if !linenumber {
   277  				for i := range l.Line {
   278  					l.Line[i].Line = 0
   279  				}
   280  			}
   281  			if !address {
   282  				l.Address = 0
   283  			}
   284  		}
   285  	}
   286  
   287  	return p.CheckValid()
   288  }
   289  
   290  // Print dumps a text representation of a profile. Intended mainly
   291  // for debugging purposes.
   292  func (p *Profile) String() string {
   293  
   294  	ss := make([]string, 0, len(p.Sample)+len(p.Mapping)+len(p.Location))
   295  	if pt := p.PeriodType; pt != nil {
   296  		ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
   297  	}
   298  	ss = append(ss, fmt.Sprintf("Period: %d", p.Period))
   299  	if p.TimeNanos != 0 {
   300  		ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos)))
   301  	}
   302  	if p.DurationNanos != 0 {
   303  		ss = append(ss, fmt.Sprintf("Duration: %v", time.Duration(p.DurationNanos)))
   304  	}
   305  
   306  	ss = append(ss, "Samples:")
   307  	var sh1 string
   308  	for _, s := range p.SampleType {
   309  		sh1 = sh1 + fmt.Sprintf("%s/%s ", s.Type, s.Unit)
   310  	}
   311  	ss = append(ss, strings.TrimSpace(sh1))
   312  	for _, s := range p.Sample {
   313  		var sv string
   314  		for _, v := range s.Value {
   315  			sv = fmt.Sprintf("%s %10d", sv, v)
   316  		}
   317  		sv = sv + ": "
   318  		for _, l := range s.Location {
   319  			sv = sv + fmt.Sprintf("%d ", l.ID)
   320  		}
   321  		ss = append(ss, sv)
   322  		const labelHeader = "                "
   323  		if len(s.Label) > 0 {
   324  			ls := labelHeader
   325  			for k, v := range s.Label {
   326  				ls = ls + fmt.Sprintf("%s:%v ", k, v)
   327  			}
   328  			ss = append(ss, ls)
   329  		}
   330  		if len(s.NumLabel) > 0 {
   331  			ls := labelHeader
   332  			for k, v := range s.NumLabel {
   333  				ls = ls + fmt.Sprintf("%s:%v ", k, v)
   334  			}
   335  			ss = append(ss, ls)
   336  		}
   337  	}
   338  
   339  	ss = append(ss, "Locations")
   340  	for _, l := range p.Location {
   341  		locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address)
   342  		if m := l.Mapping; m != nil {
   343  			locStr = locStr + fmt.Sprintf("M=%d ", m.ID)
   344  		}
   345  		if len(l.Line) == 0 {
   346  			ss = append(ss, locStr)
   347  		}
   348  		for li := range l.Line {
   349  			lnStr := "??"
   350  			if fn := l.Line[li].Function; fn != nil {
   351  				lnStr = fmt.Sprintf("%s %s:%d s=%d",
   352  					fn.Name,
   353  					fn.Filename,
   354  					l.Line[li].Line,
   355  					fn.StartLine)
   356  				if fn.Name != fn.SystemName {
   357  					lnStr = lnStr + "(" + fn.SystemName + ")"
   358  				}
   359  			}
   360  			ss = append(ss, locStr+lnStr)
   361  			// Do not print location details past the first line
   362  			locStr = "             "
   363  		}
   364  	}
   365  
   366  	ss = append(ss, "Mappings")
   367  	for _, m := range p.Mapping {
   368  		bits := ""
   369  		if m.HasFunctions {
   370  			bits += "[FN]"
   371  		}
   372  		if m.HasFilenames {
   373  			bits += "[FL]"
   374  		}
   375  		if m.HasLineNumbers {
   376  			bits += "[LN]"
   377  		}
   378  		if m.HasInlineFrames {
   379  			bits += "[IN]"
   380  		}
   381  		ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s",
   382  			m.ID,
   383  			m.Start, m.Limit, m.Offset,
   384  			m.File,
   385  			m.BuildID,
   386  			bits))
   387  	}
   388  
   389  	return strings.Join(ss, "\n") + "\n"
   390  }
   391  
   392  // Merge adds profile p adjusted by ratio r into profile p. Profiles
   393  // must be compatible (same Type and SampleType).
   394  // TODO(rsilvera): consider normalizing the profiles based on the
   395  // total samples collected.
   396  func (p *Profile) Merge(pb *Profile, r float64) error {
   397  	if err := p.Compatible(pb); err != nil {
   398  		return err
   399  	}
   400  
   401  	pb = pb.Copy()
   402  
   403  	// Keep the largest of the two periods.
   404  	if pb.Period > p.Period {
   405  		p.Period = pb.Period
   406  	}
   407  
   408  	p.DurationNanos += pb.DurationNanos
   409  
   410  	p.Mapping = append(p.Mapping, pb.Mapping...)
   411  	for i, m := range p.Mapping {
   412  		m.ID = uint64(i + 1)
   413  	}
   414  	p.Location = append(p.Location, pb.Location...)
   415  	for i, l := range p.Location {
   416  		l.ID = uint64(i + 1)
   417  	}
   418  	p.Function = append(p.Function, pb.Function...)
   419  	for i, f := range p.Function {
   420  		f.ID = uint64(i + 1)
   421  	}
   422  
   423  	if r != 1.0 {
   424  		for _, s := range pb.Sample {
   425  			for i, v := range s.Value {
   426  				s.Value[i] = int64((float64(v) * r))
   427  			}
   428  		}
   429  	}
   430  	p.Sample = append(p.Sample, pb.Sample...)
   431  	return p.CheckValid()
   432  }
   433  
   434  // Compatible determines if two profiles can be compared/merged.
   435  // returns nil if the profiles are compatible; otherwise an error with
   436  // details on the incompatibility.
   437  func (p *Profile) Compatible(pb *Profile) error {
   438  	if !compatibleValueTypes(p.PeriodType, pb.PeriodType) {
   439  		return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType)
   440  	}
   441  
   442  	if len(p.SampleType) != len(pb.SampleType) {
   443  		return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
   444  	}
   445  
   446  	for i := range p.SampleType {
   447  		if !compatibleValueTypes(p.SampleType[i], pb.SampleType[i]) {
   448  			return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType)
   449  		}
   450  	}
   451  
   452  	return nil
   453  }
   454  
   455  // HasFunctions determines if all locations in this profile have
   456  // symbolized function information.
   457  func (p *Profile) HasFunctions() bool {
   458  	for _, l := range p.Location {
   459  		if l.Mapping == nil || !l.Mapping.HasFunctions {
   460  			return false
   461  		}
   462  	}
   463  	return true
   464  }
   465  
   466  // HasFileLines determines if all locations in this profile have
   467  // symbolized file and line number information.
   468  func (p *Profile) HasFileLines() bool {
   469  	for _, l := range p.Location {
   470  		if l.Mapping == nil || (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) {
   471  			return false
   472  		}
   473  	}
   474  	return true
   475  }
   476  
   477  func compatibleValueTypes(v1, v2 *ValueType) bool {
   478  	if v1 == nil || v2 == nil {
   479  		return true // No grounds to disqualify.
   480  	}
   481  	return v1.Type == v2.Type && v1.Unit == v2.Unit
   482  }
   483  
   484  // Copy makes a fully independent copy of a profile.
   485  func (p *Profile) Copy() *Profile {
   486  	p.preEncode()
   487  	b := marshal(p)
   488  
   489  	pp := &Profile{}
   490  	if err := unmarshal(b, pp); err != nil {
   491  		panic(err)
   492  	}
   493  	if err := pp.postDecode(); err != nil {
   494  		panic(err)
   495  	}
   496  
   497  	return pp
   498  }
   499  
   500  // Demangler maps symbol names to a human-readable form. This may
   501  // include C++ demangling and additional simplification. Names that
   502  // are not demangled may be missing from the resulting map.
   503  type Demangler func(name []string) (map[string]string, error)
   504  
   505  // Demangle attempts to demangle and optionally simplify any function
   506  // names referenced in the profile. It works on a best-effort basis:
   507  // it will silently preserve the original names in case of any errors.
   508  func (p *Profile) Demangle(d Demangler) error {
   509  	// Collect names to demangle.
   510  	var names []string
   511  	for _, fn := range p.Function {
   512  		names = append(names, fn.SystemName)
   513  	}
   514  
   515  	// Update profile with demangled names.
   516  	demangled, err := d(names)
   517  	if err != nil {
   518  		return err
   519  	}
   520  	for _, fn := range p.Function {
   521  		if dd, ok := demangled[fn.SystemName]; ok {
   522  			fn.Name = dd
   523  		}
   524  	}
   525  	return nil
   526  }
   527  
   528  // Empty reports whether the profile contains no samples.
   529  func (p *Profile) Empty() bool {
   530  	return len(p.Sample) == 0
   531  }
   532  
   533  // Scale multiplies all sample values in a profile by a constant.
   534  func (p *Profile) Scale(ratio float64) {
   535  	if ratio == 1 {
   536  		return
   537  	}
   538  	ratios := make([]float64, len(p.SampleType))
   539  	for i := range p.SampleType {
   540  		ratios[i] = ratio
   541  	}
   542  	p.ScaleN(ratios)
   543  }
   544  
   545  // ScaleN multiplies each sample values in a sample by a different amount.
   546  func (p *Profile) ScaleN(ratios []float64) error {
   547  	if len(p.SampleType) != len(ratios) {
   548  		return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType))
   549  	}
   550  	allOnes := true
   551  	for _, r := range ratios {
   552  		if r != 1 {
   553  			allOnes = false
   554  			break
   555  		}
   556  	}
   557  	if allOnes {
   558  		return nil
   559  	}
   560  	for _, s := range p.Sample {
   561  		for i, v := range s.Value {
   562  			if ratios[i] != 1 {
   563  				s.Value[i] = int64(float64(v) * ratios[i])
   564  			}
   565  		}
   566  	}
   567  	return nil
   568  }
   569  

View as plain text