github.com/instana/go-sensor@v1.62.2-0.20240520081010-4919868049e1/autoprofile/internal/cpu_sampler.go (about)

     1  // (c) Copyright IBM Corp. 2021
     2  // (c) Copyright Instana Inc. 2020
     3  
     4  package internal
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"runtime/pprof"
    10  	"time"
    11  
    12  	"github.com/instana/go-sensor/autoprofile/internal/pprof/profile"
    13  )
    14  
    15  // CPUSampler collects information about CPU usage
    16  type CPUSampler struct {
    17  	top       *CallSite
    18  	buf       *bytes.Buffer
    19  	startNano int64
    20  }
    21  
    22  // NewCPUSampler initializes a new CPI sampler
    23  func NewCPUSampler() *CPUSampler {
    24  	return &CPUSampler{}
    25  }
    26  
    27  // Reset resets the state of a CPUProfiler, starting a new call tree. It does not
    28  // terminate the profiling, so the gathered profile will make up a new call tree.
    29  func (cs *CPUSampler) Reset() {
    30  	cs.top = NewCallSite("", "", 0)
    31  }
    32  
    33  // Start enables the collection of CPU usage data
    34  func (cs *CPUSampler) Start() error {
    35  	if cs.buf != nil {
    36  		return nil
    37  	}
    38  
    39  	cs.buf = bytes.NewBuffer(nil)
    40  	cs.startNano = time.Now().UnixNano()
    41  
    42  	if err := pprof.StartCPUProfile(cs.buf); err != nil {
    43  		return err
    44  	}
    45  
    46  	return nil
    47  }
    48  
    49  // Stop terminates the collection of CPU usage data and records the collected profile
    50  func (cs *CPUSampler) Stop() error {
    51  	if cs.buf == nil {
    52  		return nil
    53  	}
    54  
    55  	pprof.StopCPUProfile()
    56  
    57  	p, err := cs.collectProfile()
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	if p == nil {
    63  		return errors.New("no profile returned")
    64  	}
    65  
    66  	if uerr := cs.updateCPUProfile(p); uerr != nil {
    67  		return uerr
    68  	}
    69  
    70  	return nil
    71  }
    72  
    73  // Profile returns the recorder profile
    74  func (cs *CPUSampler) Profile(duration int64, timespan int64) (*Profile, error) {
    75  	roots := make([]*CallSite, 0)
    76  	for _, child := range cs.top.children {
    77  		roots = append(roots, child)
    78  	}
    79  	p := NewProfile(CategoryCPU, TypeCPUUsage, UnitMillisecond, roots, duration, timespan)
    80  
    81  	return p, nil
    82  }
    83  
    84  func (cs *CPUSampler) updateCPUProfile(p *profile.Profile) error {
    85  	samplesIndex := -1
    86  	cpuIndex := -1
    87  	for i, s := range p.SampleType {
    88  		if s.Type == "samples" {
    89  			samplesIndex = i
    90  		} else if s.Type == "cpu" {
    91  			cpuIndex = i
    92  		}
    93  	}
    94  
    95  	if samplesIndex == -1 || cpuIndex == -1 {
    96  		return errors.New("Unrecognized profile data")
    97  	}
    98  
    99  	// build call graph
   100  	for _, s := range p.Sample {
   101  		if shouldSkipStack(s) {
   102  			continue
   103  		}
   104  
   105  		stackSamples := s.Value[samplesIndex]
   106  		stackDuration := float64(s.Value[cpuIndex])
   107  
   108  		current := cs.top
   109  		for i := len(s.Location) - 1; i >= 0; i-- {
   110  			l := s.Location[i]
   111  			funcName, fileName, fileLine := readFuncInfo(l)
   112  
   113  			current = current.FindOrAddChild(funcName, fileName, fileLine)
   114  		}
   115  
   116  		current.Increment(stackDuration, stackSamples)
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  func (cs *CPUSampler) collectProfile() (*profile.Profile, error) {
   123  	defer func() {
   124  		cs.buf = nil
   125  	}()
   126  
   127  	p, err := profile.Parse(cs.buf)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if p.TimeNanos == 0 {
   133  		p.TimeNanos = cs.startNano
   134  	}
   135  
   136  	if p.DurationNanos == 0 {
   137  		p.DurationNanos = time.Now().UnixNano() - cs.startNano
   138  	}
   139  
   140  	if err := symbolizeProfile(p); err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	if err := p.CheckValid(); err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	return p, nil
   149  }
   150  
   151  func readFuncInfo(l *profile.Location) (funcName string, fileName string, fileLine int64) {
   152  	for li := range l.Line {
   153  		if fn := l.Line[li].Function; fn != nil {
   154  			return fn.Name, fn.Filename, l.Line[li].Line
   155  		}
   156  	}
   157  
   158  	return "", "", 0
   159  }