go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/runtime/profiling/profiler.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package profiling provides a tool to profile various aspects of the process. 16 package profiling 17 18 import ( 19 "flag" 20 "fmt" 21 "net" 22 "net/http" 23 httpProf "net/http/pprof" 24 "os" 25 "path/filepath" 26 "runtime" 27 "runtime/pprof" 28 "runtime/trace" 29 "sync/atomic" 30 "time" 31 32 "go.chromium.org/luci/common/clock" 33 "go.chromium.org/luci/common/errors" 34 "go.chromium.org/luci/common/logging" 35 ) 36 37 // Profiler helps setup and manage profiling 38 type Profiler struct { 39 // BindHTTP, if not empty, is the HTTP address to bind to. 40 // 41 // Can also be configured with "-profile-bind-http" flag. 42 BindHTTP string 43 44 // Dir, if set, is the path where profiling data will be written to. 45 // 46 // Can also be configured with "-profile-output-dir" flag. 47 Dir string 48 49 // ProfileCPU, if true, indicates that the profiler should profile the CPU. 50 // 51 // Requires Dir to be set, since it's where the profiler output is dumped. 52 // 53 // Can also be set with "-profile-cpu". 54 ProfileCPU bool 55 56 // ProfileTrace, if true, indicates that the profiler should enable 57 // runtime tracing. 58 // 59 // Requires Dir to be set, since it's where the profiler output is dumped. 60 // 61 // Can also be set with "-profile-trace". 62 ProfileTrace bool 63 64 // ProfileHeap, if true, indicates that the profiler should profile heap 65 // allocations. 66 // 67 // Requires Dir to be set, since it's where the profiler output is dumped. 68 // 69 // Can also be set with "-profile-heap". 70 ProfileHeap bool 71 72 // ProfileHeapFrequency, if set non-zero, instructs the profiler to 73 // periodically dump heap profiler snapshots. 74 // 75 // Requires Dir to be set, since it's where the profiler output is dumped. 76 // 77 // Can also be set with "-profile-heap-frequency". 78 ProfileHeapFrequency time.Duration 79 80 // Logger, if not nil, will be used to log events and errors. If nil, no 81 // logging will be used. 82 Logger logging.Logger 83 // Clock is the clock instance to use. If nil, the system clock will be used. 84 Clock clock.Clock 85 86 // listener is the active listener instance. It is set when Start is called. 87 listener net.Listener 88 89 // pathCounter is an atomic counter used to ensure non-conflicting paths. 90 pathCounter uint32 91 92 // profilingCPU is true if 'Start' successfully launched CPU profiling. 93 profilingCPU bool 94 95 // profilingTrace is true if 'Start' successfully launched runtime tracing. 96 profilingTrace bool 97 } 98 99 // AddFlags adds command line flags to common Profiler fields. 100 func (p *Profiler) AddFlags(fs *flag.FlagSet) { 101 fs.StringVar(&p.BindHTTP, "profile-bind-http", "", 102 "If specified, run a runtime profiler HTTP server bound to this [address][:port].") 103 fs.StringVar(&p.Dir, "profile-output-dir", "", 104 "If specified, allow generation of profiling artifacts, which will be written here.") 105 fs.BoolVar(&p.ProfileCPU, "profile-cpu", false, "If specified, enables CPU profiling.") 106 fs.BoolVar(&p.ProfileTrace, "profile-trace", false, "If specified, enables runtime tracing.") 107 fs.BoolVar(&p.ProfileHeap, "profile-heap", false, "If specified, enables heap profiling.") 108 fs.DurationVar(&p.ProfileHeapFrequency, "profile-heap-frequency", 0, "If specified non-zero, enables periodic heap profiler snapshots dump.") 109 } 110 111 // Start starts the Profiler's configured operations. On success, returns a 112 // function that can be called to shutdown the profiling server. 113 // 114 // Calling Stop is not necessary, but will enable end-of-operation profiling 115 // to be gathered. 116 func (p *Profiler) Start() error { 117 if p.Dir == "" { 118 if p.ProfileCPU { 119 return errors.New("-profile-cpu requires -profile-output-dir to be set") 120 } 121 if p.ProfileTrace { 122 return errors.New("-profile-trace requires -profile-output-dir to be set") 123 } 124 if p.ProfileHeap { 125 return errors.New("-profile-heap requires -profile-output-dir to be set") 126 } 127 128 if p.ProfileHeapFrequency > 0 { 129 return errors.New("-profile-heap-frequency requires -profile-output-dir to be set") 130 } 131 } 132 133 if p.ProfileHeapFrequency < 0 { 134 return errors.New("-profile-heap-frequency should be positive if set") 135 } 136 137 if p.ProfileHeapFrequency > 0 && !p.ProfileHeap { 138 return errors.New("-profile-heap-frequency requires -profile-heap") 139 } 140 141 if p.ProfileCPU { 142 out, err := os.Create(p.generateOutPath("cpu")) 143 if err != nil { 144 return errors.Annotate(err, "failed to create CPU profile output file").Err() 145 } 146 pprof.StartCPUProfile(out) 147 p.profilingCPU = true 148 } 149 150 if p.ProfileTrace { 151 out, err := os.Create(p.generateOutPath("trace")) 152 if err != nil { 153 return errors.Annotate(err, "failed to create runtime trace output file").Err() 154 } 155 trace.Start(out) 156 p.profilingTrace = true 157 } 158 159 if p.ProfileHeapFrequency > 0 { 160 go func() { 161 for { 162 time.Sleep(p.ProfileHeapFrequency) 163 if err := p.dumpHeapProfile(); err != nil { 164 p.getLogger().Errorf("Error dump heap profile: %v", err) 165 } 166 } 167 }() 168 } 169 170 if p.BindHTTP != "" { 171 if err := p.startHTTP(); err != nil { 172 return errors.Annotate(err, "failed to start HTTP server").Err() 173 } 174 } 175 176 return nil 177 } 178 179 func (p *Profiler) startHTTP() error { 180 // Register paths: https://golang.org/src/net/http/pprof/pprof.go 181 router := http.NewServeMux() 182 router.HandleFunc("/debug/pprof/", httpProf.Index) 183 router.HandleFunc("/debug/pprof/cmdline", httpProf.Cmdline) 184 router.HandleFunc("/debug/pprof/profile", httpProf.Profile) 185 router.HandleFunc("/debug/pprof/symbol", httpProf.Symbol) 186 router.HandleFunc("/debug/pprof/trace", httpProf.Trace) 187 for _, p := range pprof.Profiles() { 188 name := p.Name() 189 router.Handle(fmt.Sprintf("/debug/pprof/%s", name), httpProf.Handler(name)) 190 } 191 192 // Bind to our profiling port. 193 l, err := net.Listen("tcp4", p.BindHTTP) 194 if err != nil { 195 return errors.Annotate(err, "failed to bind to TCP4 address: %q", p.BindHTTP).Err() 196 } 197 198 server := http.Server{ 199 Handler: router, 200 } 201 go func() { 202 if err := server.Serve(l); err != nil { 203 p.getLogger().Errorf("Error serving profile HTTP: %s", err) 204 } 205 }() 206 return nil 207 } 208 209 // Stop stops the Profiler's operations. 210 func (p *Profiler) Stop() { 211 if p.profilingCPU { 212 pprof.StopCPUProfile() 213 p.profilingCPU = false 214 } 215 216 if p.profilingTrace { 217 trace.Stop() 218 p.profilingTrace = false 219 } 220 221 if p.listener != nil { 222 if err := p.listener.Close(); err != nil { 223 p.getLogger().Warningf("Failed to stop profile HTTP server: %s", err) 224 } 225 p.listener = nil 226 } 227 228 // Take one final snapshot. 229 p.DumpSnapshot() 230 } 231 232 // DumpSnapshot dumps a profile snapshot to the configured output directory. If 233 // no output directory is configured, nothing will happen. 234 func (p *Profiler) DumpSnapshot() error { 235 if p.Dir == "" { 236 return nil 237 } 238 239 if p.ProfileHeap { 240 if err := p.dumpHeapProfile(); err != nil { 241 return errors.Annotate(err, "failed to dump heap profile").Err() 242 } 243 } 244 245 return nil 246 } 247 248 func (p *Profiler) dumpHeapProfile() error { 249 fd, err := os.Create(p.generateOutPath("memory")) 250 if err != nil { 251 return errors.Annotate(err, "failed to create output file").Err() 252 } 253 defer fd.Close() 254 255 // Get up-to-date statistics. 256 runtime.GC() 257 if err := pprof.WriteHeapProfile(fd); err != nil { 258 return errors.Annotate(err, "failed to write heap profile").Err() 259 } 260 return nil 261 } 262 263 func (p *Profiler) generateOutPath(base string) string { 264 clk := p.Clock 265 if clk == nil { 266 clk = clock.GetSystemClock() 267 } 268 now := clk.Now() 269 counter := atomic.AddUint32(&p.pathCounter, 1) - 1 270 return filepath.Join(p.Dir, fmt.Sprintf("%s_%d_%d.prof", base, now.Unix(), counter)) 271 } 272 273 func (p *Profiler) getLogger() logging.Logger { 274 if p.Logger != nil { 275 return p.Logger 276 } 277 return logging.Null 278 }