github.com/dorkamotorka/go/src@v0.0.0-20230614113921-187095f0e316/net/http/pprof/pprof.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package pprof serves via its HTTP server runtime profiling data 6 // in the format expected by the pprof visualization tool. 7 // 8 // The package is typically only imported for the side effect of 9 // registering its HTTP handlers. 10 // The handled paths all begin with /debug/pprof/. 11 // 12 // To use pprof, link this package into your program: 13 // 14 // import _ "net/http/pprof" 15 // 16 // If your application is not already running an http server, you 17 // need to start one. Add "net/http" and "log" to your imports and 18 // the following code to your main function: 19 // 20 // go func() { 21 // log.Println(http.ListenAndServe("localhost:6060", nil)) 22 // }() 23 // 24 // By default, all the profiles listed in [runtime/pprof.Profile] are 25 // available (via [Handler]), in addition to the [Cmdline], [Profile], [Symbol], 26 // and [Trace] profiles defined in this package. 27 // If you are not using DefaultServeMux, you will have to register handlers 28 // with the mux you are using. 29 // 30 // # Parameters 31 // 32 // Parameters can be passed via GET query params: 33 // 34 // - debug=N (all profiles): response format: N = 0: binary (default), N > 0: plaintext 35 // - gc=N (heap profile): N > 0: run a garbage collection cycle before profiling 36 // - seconds=N (allocs, block, goroutine, heap, mutex, threadcreate profiles): return a delta profile 37 // - seconds=N (cpu (profile), trace profiles): profile for the given duration 38 // 39 // # Usage examples 40 // 41 // Use the pprof tool to look at the heap profile: 42 // 43 // go tool pprof http://localhost:6060/debug/pprof/heap 44 // 45 // Or to look at a 30-second CPU profile: 46 // 47 // go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 48 // 49 // Or to look at the goroutine blocking profile, after calling 50 // runtime.SetBlockProfileRate in your program: 51 // 52 // go tool pprof http://localhost:6060/debug/pprof/block 53 // 54 // Or to look at the holders of contended mutexes, after calling 55 // runtime.SetMutexProfileFraction in your program: 56 // 57 // go tool pprof http://localhost:6060/debug/pprof/mutex 58 // 59 // The package also exports a handler that serves execution trace data 60 // for the "go tool trace" command. To collect a 5-second execution trace: 61 // 62 // curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5 63 // go tool trace trace.out 64 // 65 // To view all available profiles, open http://localhost:6060/debug/pprof/ 66 // in your browser. 67 // 68 // For a study of the facility in action, visit 69 // https://blog.golang.org/2011/06/profiling-go-programs.html. 70 package pprof 71 72 import ( 73 "bufio" 74 "bytes" 75 "context" 76 "fmt" 77 "html" 78 "internal/profile" 79 "io" 80 "log" 81 "net/http" 82 "net/url" 83 "os" 84 "runtime" 85 "runtime/pprof" 86 "runtime/trace" 87 "sort" 88 "strconv" 89 "strings" 90 "time" 91 ) 92 93 func init() { 94 http.HandleFunc("/debug/pprof/", Index) 95 http.HandleFunc("/debug/pprof/cmdline", Cmdline) 96 http.HandleFunc("/debug/pprof/profile", Profile) 97 http.HandleFunc("/debug/pprof/symbol", Symbol) 98 http.HandleFunc("/debug/pprof/trace", Trace) 99 } 100 101 // Cmdline responds with the running program's 102 // command line, with arguments separated by NUL bytes. 103 // The package initialization registers it as /debug/pprof/cmdline. 104 func Cmdline(w http.ResponseWriter, r *http.Request) { 105 w.Header().Set("X-Content-Type-Options", "nosniff") 106 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 107 fmt.Fprint(w, strings.Join(os.Args, "\x00")) 108 } 109 110 func sleep(r *http.Request, d time.Duration) { 111 select { 112 case <-time.After(d): 113 case <-r.Context().Done(): 114 } 115 } 116 117 func durationExceedsWriteTimeout(r *http.Request, seconds float64) bool { 118 srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server) 119 return ok && srv.WriteTimeout != 0 && seconds >= srv.WriteTimeout.Seconds() 120 } 121 122 func serveError(w http.ResponseWriter, status int, txt string) { 123 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 124 w.Header().Set("X-Go-Pprof", "1") 125 w.Header().Del("Content-Disposition") 126 w.WriteHeader(status) 127 fmt.Fprintln(w, txt) 128 } 129 130 // Profile responds with the pprof-formatted cpu profile. 131 // Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified. 132 // The package initialization registers it as /debug/pprof/profile. 133 func Profile(w http.ResponseWriter, r *http.Request) { 134 w.Header().Set("X-Content-Type-Options", "nosniff") 135 sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64) 136 if sec <= 0 || err != nil { 137 sec = 30 138 } 139 140 if durationExceedsWriteTimeout(r, float64(sec)) { 141 serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout") 142 return 143 } 144 145 // Set Content Type assuming StartCPUProfile will work, 146 // because if it does it starts writing. 147 w.Header().Set("Content-Type", "application/octet-stream") 148 w.Header().Set("Content-Disposition", `attachment; filename="profile"`) 149 if err := pprof.StartCPUProfile(w); err != nil { 150 // StartCPUProfile failed, so no writes yet. 151 serveError(w, http.StatusInternalServerError, 152 fmt.Sprintf("Could not enable CPU profiling: %s", err)) 153 return 154 } 155 sleep(r, time.Duration(sec)*time.Second) 156 pprof.StopCPUProfile() 157 } 158 159 // Trace responds with the execution trace in binary form. 160 // Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified. 161 // The package initialization registers it as /debug/pprof/trace. 162 func Trace(w http.ResponseWriter, r *http.Request) { 163 w.Header().Set("X-Content-Type-Options", "nosniff") 164 sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64) 165 if sec <= 0 || err != nil { 166 sec = 1 167 } 168 169 if durationExceedsWriteTimeout(r, sec) { 170 serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout") 171 return 172 } 173 174 // Set Content Type assuming trace.Start will work, 175 // because if it does it starts writing. 176 w.Header().Set("Content-Type", "application/octet-stream") 177 w.Header().Set("Content-Disposition", `attachment; filename="trace"`) 178 if err := trace.Start(w); err != nil { 179 // trace.Start failed, so no writes yet. 180 serveError(w, http.StatusInternalServerError, 181 fmt.Sprintf("Could not enable tracing: %s", err)) 182 return 183 } 184 sleep(r, time.Duration(sec*float64(time.Second))) 185 trace.Stop() 186 } 187 188 // Symbol looks up the program counters listed in the request, 189 // responding with a table mapping program counters to function names. 190 // The package initialization registers it as /debug/pprof/symbol. 191 func Symbol(w http.ResponseWriter, r *http.Request) { 192 w.Header().Set("X-Content-Type-Options", "nosniff") 193 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 194 195 // We have to read the whole POST body before 196 // writing any output. Buffer the output here. 197 var buf bytes.Buffer 198 199 // We don't know how many symbols we have, but we 200 // do have symbol information. Pprof only cares whether 201 // this number is 0 (no symbols available) or > 0. 202 fmt.Fprintf(&buf, "num_symbols: 1\n") 203 204 var b *bufio.Reader 205 if r.Method == "POST" { 206 b = bufio.NewReader(r.Body) 207 } else { 208 b = bufio.NewReader(strings.NewReader(r.URL.RawQuery)) 209 } 210 211 for { 212 word, err := b.ReadSlice('+') 213 if err == nil { 214 word = word[0 : len(word)-1] // trim + 215 } 216 pc, _ := strconv.ParseUint(string(word), 0, 64) 217 if pc != 0 { 218 f := runtime.FuncForPC(uintptr(pc)) 219 if f != nil { 220 fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name()) 221 } 222 } 223 224 // Wait until here to check for err; the last 225 // symbol will have an err because it doesn't end in +. 226 if err != nil { 227 if err != io.EOF { 228 fmt.Fprintf(&buf, "reading request: %v\n", err) 229 } 230 break 231 } 232 } 233 234 w.Write(buf.Bytes()) 235 } 236 237 // Handler returns an HTTP handler that serves the named profile. 238 // Available profiles can be found in [runtime/pprof.Profile]. 239 func Handler(name string) http.Handler { 240 return handler(name) 241 } 242 243 type handler string 244 245 func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 246 w.Header().Set("X-Content-Type-Options", "nosniff") 247 p := pprof.Lookup(string(name)) 248 if p == nil { 249 serveError(w, http.StatusNotFound, "Unknown profile") 250 return 251 } 252 if sec := r.FormValue("seconds"); sec != "" { 253 name.serveDeltaProfile(w, r, p, sec) 254 return 255 } 256 gc, _ := strconv.Atoi(r.FormValue("gc")) 257 if name == "heap" && gc > 0 { 258 runtime.GC() 259 } 260 debug, _ := strconv.Atoi(r.FormValue("debug")) 261 if debug != 0 { 262 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 263 } else { 264 w.Header().Set("Content-Type", "application/octet-stream") 265 w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name)) 266 } 267 p.WriteTo(w, debug) 268 } 269 270 func (name handler) serveDeltaProfile(w http.ResponseWriter, r *http.Request, p *pprof.Profile, secStr string) { 271 sec, err := strconv.ParseInt(secStr, 10, 64) 272 if err != nil || sec <= 0 { 273 serveError(w, http.StatusBadRequest, `invalid value for "seconds" - must be a positive integer`) 274 return 275 } 276 if !profileSupportsDelta[name] { 277 serveError(w, http.StatusBadRequest, `"seconds" parameter is not supported for this profile type`) 278 return 279 } 280 // 'name' should be a key in profileSupportsDelta. 281 if durationExceedsWriteTimeout(r, float64(sec)) { 282 serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout") 283 return 284 } 285 debug, _ := strconv.Atoi(r.FormValue("debug")) 286 if debug != 0 { 287 serveError(w, http.StatusBadRequest, "seconds and debug params are incompatible") 288 return 289 } 290 p0, err := collectProfile(p) 291 if err != nil { 292 serveError(w, http.StatusInternalServerError, "failed to collect profile") 293 return 294 } 295 296 t := time.NewTimer(time.Duration(sec) * time.Second) 297 defer t.Stop() 298 299 select { 300 case <-r.Context().Done(): 301 err := r.Context().Err() 302 if err == context.DeadlineExceeded { 303 serveError(w, http.StatusRequestTimeout, err.Error()) 304 } else { // TODO: what's a good status code for canceled requests? 400? 305 serveError(w, http.StatusInternalServerError, err.Error()) 306 } 307 return 308 case <-t.C: 309 } 310 311 p1, err := collectProfile(p) 312 if err != nil { 313 serveError(w, http.StatusInternalServerError, "failed to collect profile") 314 return 315 } 316 ts := p1.TimeNanos 317 dur := p1.TimeNanos - p0.TimeNanos 318 319 p0.Scale(-1) 320 321 p1, err = profile.Merge([]*profile.Profile{p0, p1}) 322 if err != nil { 323 serveError(w, http.StatusInternalServerError, "failed to compute delta") 324 return 325 } 326 327 p1.TimeNanos = ts // set since we don't know what profile.Merge set for TimeNanos. 328 p1.DurationNanos = dur 329 330 w.Header().Set("Content-Type", "application/octet-stream") 331 w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-delta"`, name)) 332 p1.Write(w) 333 } 334 335 func collectProfile(p *pprof.Profile) (*profile.Profile, error) { 336 var buf bytes.Buffer 337 if err := p.WriteTo(&buf, 0); err != nil { 338 return nil, err 339 } 340 ts := time.Now().UnixNano() 341 p0, err := profile.Parse(&buf) 342 if err != nil { 343 return nil, err 344 } 345 p0.TimeNanos = ts 346 return p0, nil 347 } 348 349 var profileSupportsDelta = map[handler]bool{ 350 "allocs": true, 351 "block": true, 352 "goroutine": true, 353 "heap": true, 354 "mutex": true, 355 "threadcreate": true, 356 } 357 358 var profileDescriptions = map[string]string{ 359 "allocs": "A sampling of all past memory allocations", 360 "block": "Stack traces that led to blocking on synchronization primitives", 361 "cmdline": "The command line invocation of the current program", 362 "goroutine": "Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.", 363 "heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.", 364 "mutex": "Stack traces of holders of contended mutexes", 365 "profile": "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.", 366 "threadcreate": "Stack traces that led to the creation of new OS threads", 367 "trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.", 368 } 369 370 type profileEntry struct { 371 Name string 372 Href string 373 Desc string 374 Count int 375 } 376 377 // Index responds with the pprof-formatted profile named by the request. 378 // For example, "/debug/pprof/heap" serves the "heap" profile. 379 // Index responds to a request for "/debug/pprof/" with an HTML page 380 // listing the available profiles. 381 func Index(w http.ResponseWriter, r *http.Request) { 382 if name, found := strings.CutPrefix(r.URL.Path, "/debug/pprof/"); found { 383 if name != "" { 384 handler(name).ServeHTTP(w, r) 385 return 386 } 387 } 388 389 w.Header().Set("X-Content-Type-Options", "nosniff") 390 w.Header().Set("Content-Type", "text/html; charset=utf-8") 391 392 var profiles []profileEntry 393 for _, p := range pprof.Profiles() { 394 profiles = append(profiles, profileEntry{ 395 Name: p.Name(), 396 Href: p.Name(), 397 Desc: profileDescriptions[p.Name()], 398 Count: p.Count(), 399 }) 400 } 401 402 // Adding other profiles exposed from within this package 403 for _, p := range []string{"cmdline", "profile", "trace"} { 404 profiles = append(profiles, profileEntry{ 405 Name: p, 406 Href: p, 407 Desc: profileDescriptions[p], 408 }) 409 } 410 411 sort.Slice(profiles, func(i, j int) bool { 412 return profiles[i].Name < profiles[j].Name 413 }) 414 415 if err := indexTmplExecute(w, profiles); err != nil { 416 log.Print(err) 417 } 418 } 419 420 func indexTmplExecute(w io.Writer, profiles []profileEntry) error { 421 var b bytes.Buffer 422 b.WriteString(`<html> 423 <head> 424 <title>/debug/pprof/</title> 425 <style> 426 .profile-name{ 427 display:inline-block; 428 width:6rem; 429 } 430 </style> 431 </head> 432 <body> 433 /debug/pprof/ 434 <br> 435 <p>Set debug=1 as a query parameter to export in legacy text format</p> 436 <br> 437 Types of profiles available: 438 <table> 439 <thead><td>Count</td><td>Profile</td></thead> 440 `) 441 442 for _, profile := range profiles { 443 link := &url.URL{Path: profile.Href, RawQuery: "debug=1"} 444 fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(profile.Name)) 445 } 446 447 b.WriteString(`</table> 448 <a href="goroutine?debug=2">full goroutine stack dump</a> 449 <br> 450 <p> 451 Profile Descriptions: 452 <ul> 453 `) 454 for _, profile := range profiles { 455 fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(profile.Name), html.EscapeString(profile.Desc)) 456 } 457 b.WriteString(`</ul> 458 </p> 459 </body> 460 </html>`) 461 462 _, err := w.Write(b.Bytes()) 463 return err 464 }