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 }