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