github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/debug/triggering_profile.go (about) 1 // Copyright (c) 2020 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package debug 22 23 import ( 24 "bytes" 25 "errors" 26 "fmt" 27 "os" 28 "runtime/pprof" 29 "text/template" 30 "time" 31 32 "github.com/m3db/m3/src/x/instrument" 33 34 "go.uber.org/zap" 35 ) 36 37 const ( 38 // ContinuousCPUProfileName is the name of continuous CPU profile. 39 ContinuousCPUProfileName = "cpu" 40 ) 41 42 var ( 43 defaultConditional = func() bool { return true } 44 defaultInterval = time.Second 45 errNoFilePathTemplate = errors.New("no file path template") 46 errNoProfileName = errors.New("no profile name") 47 errNoInstrumentOptions = errors.New("no instrument options") 48 errAlreadyOpen = errors.New("already open") 49 errNotOpen = errors.New("not open") 50 ) 51 52 // ContinuousFileProfile is a profile that runs continously 53 // to a file using a template for a file name. 54 type ContinuousFileProfile struct { 55 filePathTemplate *template.Template 56 profileName string 57 profileDuration time.Duration 58 profileDebug int 59 conditional func() bool 60 interval time.Duration 61 62 logger *zap.Logger 63 64 closeCh chan struct{} 65 } 66 67 // ContinuousFileProfileOptions is a set of continuous file profile options. 68 type ContinuousFileProfileOptions struct { 69 FilePathTemplate string 70 ProfileName string 71 ProfileDuration time.Duration 72 ProfileDebug int 73 Conditional func() bool 74 Interval time.Duration 75 InstrumentOptions instrument.Options 76 } 77 78 // ContinuousFileProfilePathParams is the params used to construct 79 // a file path. 80 type ContinuousFileProfilePathParams struct { 81 ProfileName string 82 UnixTime int64 83 } 84 85 // NewContinuousFileProfile returns a new continuous file profile. 86 func NewContinuousFileProfile( 87 opts ContinuousFileProfileOptions, 88 ) (*ContinuousFileProfile, error) { 89 if opts.FilePathTemplate == "" { 90 return nil, errNoFilePathTemplate 91 } 92 if opts.ProfileName == "" { 93 return nil, errNoProfileName 94 } 95 if opts.Conditional == nil { 96 opts.Conditional = defaultConditional 97 } 98 if opts.Interval == 0 { 99 opts.Interval = defaultInterval 100 } 101 if opts.InstrumentOptions == nil { 102 return nil, errNoInstrumentOptions 103 } 104 105 tmpl, err := template.New("fileName").Parse(opts.FilePathTemplate) 106 if err != nil { 107 return nil, err 108 } 109 110 return &ContinuousFileProfile{ 111 filePathTemplate: tmpl, 112 profileName: opts.ProfileName, 113 profileDuration: opts.ProfileDuration, 114 profileDebug: opts.ProfileDebug, 115 conditional: opts.Conditional, 116 interval: opts.Interval, 117 logger: opts.InstrumentOptions.Logger(), 118 }, nil 119 } 120 121 // Start will start the continuous file profile. 122 func (c *ContinuousFileProfile) Start() error { 123 if c.closeCh != nil { 124 return errAlreadyOpen 125 } 126 127 c.closeCh = make(chan struct{}) 128 go c.run() 129 return nil 130 } 131 132 // Stop will stop the continuous file profile. 133 func (c *ContinuousFileProfile) Stop() error { 134 if c.closeCh == nil { 135 return errNotOpen 136 } 137 138 close(c.closeCh) 139 c.closeCh = nil 140 141 return nil 142 } 143 144 func (c *ContinuousFileProfile) run() { 145 closeCh := c.closeCh 146 ticker := time.NewTicker(c.interval) 147 defer ticker.Stop() 148 149 for { 150 select { 151 case <-closeCh: 152 return 153 case <-ticker.C: 154 if !c.conditional() { 155 continue 156 } 157 158 err := c.profile() 159 if err != nil { 160 c.logger.Error("continuous profile error", 161 zap.String("name", c.profileName), 162 zap.Int("debug", c.profileDebug), 163 zap.Duration("interval", c.interval), 164 zap.Error(err)) 165 } 166 } 167 } 168 } 169 170 func (c *ContinuousFileProfile) profile() error { 171 filePathBuffer := bytes.NewBuffer(nil) 172 filePathParams := ContinuousFileProfilePathParams{ 173 ProfileName: c.profileName, 174 UnixTime: time.Now().Unix(), 175 } 176 err := c.filePathTemplate.Execute(filePathBuffer, filePathParams) 177 if err != nil { 178 return err 179 } 180 181 w, err := os.Create(filePathBuffer.String()) 182 if err != nil { 183 return err 184 } 185 186 success := false 187 defer func() { 188 if !success { 189 _ = w.Close() 190 } 191 }() 192 193 switch c.profileName { 194 case ContinuousCPUProfileName: 195 if err := pprof.StartCPUProfile(w); err != nil { 196 return err 197 } 198 time.Sleep(c.profileDuration) 199 pprof.StopCPUProfile() 200 default: 201 p := pprof.Lookup(c.profileName) 202 if p == nil { 203 return fmt.Errorf("unknown profile: %s", c.profileName) 204 } 205 if err := p.WriteTo(w, c.profileDebug); err != nil { 206 return err 207 } 208 } 209 210 success = true 211 return w.Close() 212 }