github.com/waldiirawan/apm-agent-go/v2@v2.2.2/profiling.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package apm // import "github.com/waldiirawan/apm-agent-go/v2" 19 20 import ( 21 "bytes" 22 "context" 23 "io" 24 "runtime/pprof" 25 "time" 26 27 "github.com/pkg/errors" 28 ) 29 30 type profilingState struct { 31 profileType string 32 profileStart func(io.Writer) error 33 profileStop func() 34 sender profileSender 35 36 interval time.Duration 37 duration time.Duration // not relevant to all profiles 38 39 timer *time.Timer 40 timerStart time.Time 41 buf bytes.Buffer 42 finished chan struct{} 43 } 44 45 // newCPUProfilingState calls newProfilingState with the 46 // profiler type set to "cpu", and using pprof.StartCPUProfile 47 // and pprof.StopCPUProfile. 48 func newCPUProfilingState(sender profileSender) *profilingState { 49 return newProfilingState("cpu", pprof.StartCPUProfile, pprof.StopCPUProfile, sender) 50 } 51 52 // newHeapProfilingState calls newProfilingState with the 53 // profiler type set to "heap", and using pprof.Lookup("heap").WriteTo(writer, 0). 54 func newHeapProfilingState(sender profileSender) *profilingState { 55 return newLookupProfilingState("heap", sender) 56 } 57 58 func newLookupProfilingState(name string, sender profileSender) *profilingState { 59 profileStart := func(w io.Writer) error { 60 profile := pprof.Lookup(name) 61 if profile == nil { 62 return errors.Errorf("no profile called %q", name) 63 } 64 return profile.WriteTo(w, 0) 65 } 66 return newProfilingState("heap", profileStart, func() {}, sender) 67 } 68 69 // newProfilingState returns a new profilingState, 70 // with its timer stopped. The timer may be started 71 // by calling profilingState.updateConfig. 72 func newProfilingState( 73 profileType string, 74 profileStart func(io.Writer) error, 75 profileStop func(), 76 sender profileSender, 77 ) *profilingState { 78 state := &profilingState{ 79 profileType: profileType, 80 profileStart: profileStart, 81 profileStop: profileStop, 82 sender: sender, 83 timer: time.NewTimer(0), 84 finished: make(chan struct{}, 1), 85 } 86 if !state.timer.Stop() { 87 <-state.timer.C 88 } 89 return state 90 } 91 92 func (state *profilingState) updateConfig(interval, duration time.Duration) { 93 if state.sender == nil { 94 // No profile sender, no point in starting a timer. 95 return 96 } 97 state.duration = duration 98 if state.interval == interval { 99 return 100 } 101 if state.timerStart.IsZero() { 102 state.interval = interval 103 state.resetTimer() 104 } 105 // TODO(axw) handle extending/cutting short running timers once 106 // it is possible to dynamically control profiling configuration. 107 } 108 109 func (state *profilingState) resetTimer() { 110 if state.interval != 0 { 111 state.timer.Reset(state.interval) 112 state.timerStart = time.Now() 113 } else { 114 state.timerStart = time.Time{} 115 } 116 } 117 118 // start spawns a goroutine that will capture a profile, send it using state.sender, 119 // and finally signal state.finished. 120 // 121 // start will return immediately after spawning the goroutine. 122 func (state *profilingState) start(ctx context.Context, logger Logger, metadata io.Reader) { 123 // The state.duration field may be updated after the goroutine starts, 124 // by the caller, so it must be read outside the goroutine. 125 duration := state.duration 126 go func() { 127 defer func() { state.finished <- struct{}{} }() 128 if err := state.profile(ctx, duration); err != nil { 129 if logger != nil && ctx.Err() == nil { 130 logger.Errorf("%s", err) 131 } 132 return 133 } 134 // TODO(axw) backoff like SendStream requests 135 if err := state.sender.SendProfile(ctx, metadata, &state.buf); err != nil { 136 if logger != nil && ctx.Err() == nil { 137 logger.Errorf("failed to send %s profile: %s", state.profileType, err) 138 } 139 return 140 } 141 if logger != nil { 142 logger.Debugf("sent %s profile", state.profileType) 143 } 144 }() 145 } 146 147 func (state *profilingState) profile(ctx context.Context, duration time.Duration) error { 148 state.buf.Reset() 149 if err := state.profileStart(&state.buf); err != nil { 150 return errors.Wrapf(err, "failed to start %s profile", state.profileType) 151 } 152 defer state.profileStop() 153 154 if duration > 0 { 155 timer := time.NewTimer(duration) 156 defer timer.Stop() 157 select { 158 case <-ctx.Done(): 159 return ctx.Err() 160 case <-timer.C: 161 } 162 } 163 return nil 164 } 165 166 type profileSender interface { 167 SendProfile(ctx context.Context, metadata io.Reader, profile ...io.Reader) error 168 }