github.com/Kolosok86/http@v0.1.2/internal/profile/profile.go (about)

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