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