go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/pprof/internal/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 internal 6 7 // This code is copied from go/src/net/http/pprof/pprof.go at revision 8 // 01c144c410b09d8b56d40e7e9c54fface204aa29 of the official Golang repo. 9 // 10 // Modifications: 11 // * Different package name, no package doc. 12 // * Removed init() section, since we don't want to autoregister routes in the 13 // default router (that's the main reason we are copy-pasting code instead of 14 // importing the package). 15 // * Modified Index() to put '&tok=...' into URLs. 16 17 import ( 18 "bufio" 19 "bytes" 20 "fmt" 21 "html/template" 22 "io" 23 "log" 24 "net/http" 25 "os" 26 "runtime" 27 "runtime/pprof" 28 "runtime/trace" 29 "strconv" 30 "strings" 31 "time" 32 ) 33 34 // Cmdline responds with the running program's 35 // command line, with arguments separated by NUL bytes. 36 // The package initialization registers it as /debug/pprof/cmdline. 37 func Cmdline(w http.ResponseWriter, r *http.Request) { 38 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 39 fmt.Fprintf(w, strings.Join(os.Args, "\x00")) 40 } 41 42 func sleep(w http.ResponseWriter, d time.Duration) { 43 var clientGone <-chan bool 44 if cn, ok := w.(http.CloseNotifier); ok { 45 clientGone = cn.CloseNotify() 46 } 47 select { 48 case <-time.After(d): 49 case <-clientGone: 50 } 51 } 52 53 func durationExceedsWriteTimeout(r *http.Request, seconds float64) bool { 54 srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server) 55 return ok && srv.WriteTimeout != 0 && seconds >= srv.WriteTimeout.Seconds() 56 } 57 58 // Profile responds with the pprof-formatted cpu profile. 59 // The package initialization registers it as /debug/pprof/profile. 60 func Profile(w http.ResponseWriter, r *http.Request) { 61 sec, _ := strconv.ParseInt(r.FormValue("seconds"), 10, 64) 62 if sec == 0 { 63 sec = 30 64 } 65 66 if durationExceedsWriteTimeout(r, float64(sec)) { 67 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 68 w.Header().Set("X-Go-Pprof", "1") 69 w.WriteHeader(http.StatusBadRequest) 70 fmt.Fprintln(w, "profile duration exceeds server's WriteTimeout") 71 return 72 } 73 74 // Set Content Type assuming StartCPUProfile will work, 75 // because if it does it starts writing. 76 w.Header().Set("Content-Type", "application/octet-stream") 77 if err := pprof.StartCPUProfile(w); err != nil { 78 // StartCPUProfile failed, so no writes yet. 79 // Can change header back to text content 80 // and send error code. 81 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 82 w.Header().Set("X-Go-Pprof", "1") 83 w.WriteHeader(http.StatusInternalServerError) 84 fmt.Fprintf(w, "Could not enable CPU profiling: %s\n", err) 85 return 86 } 87 sleep(w, time.Duration(sec)*time.Second) 88 pprof.StopCPUProfile() 89 } 90 91 // Trace responds with the execution trace in binary form. 92 // Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified. 93 // The package initialization registers it as /debug/pprof/trace. 94 func Trace(w http.ResponseWriter, r *http.Request) { 95 sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64) 96 if sec <= 0 || err != nil { 97 sec = 1 98 } 99 100 if durationExceedsWriteTimeout(r, sec) { 101 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 102 w.Header().Set("X-Go-Pprof", "1") 103 w.WriteHeader(http.StatusBadRequest) 104 fmt.Fprintln(w, "profile duration exceeds server's WriteTimeout") 105 return 106 } 107 108 // Set Content Type assuming trace.Start will work, 109 // because if it does it starts writing. 110 w.Header().Set("Content-Type", "application/octet-stream") 111 if err := trace.Start(w); err != nil { 112 // trace.Start failed, so no writes yet. 113 // Can change header back to text content and send error code. 114 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 115 w.Header().Set("X-Go-Pprof", "1") 116 w.WriteHeader(http.StatusInternalServerError) 117 fmt.Fprintf(w, "Could not enable tracing: %s\n", err) 118 return 119 } 120 sleep(w, time.Duration(sec*float64(time.Second))) 121 trace.Stop() 122 } 123 124 // Symbol looks up the program counters listed in the request, 125 // responding with a table mapping program counters to function names. 126 // The package initialization registers it as /debug/pprof/symbol. 127 func Symbol(w http.ResponseWriter, r *http.Request) { 128 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 129 130 // We have to read the whole POST body before 131 // writing any output. Buffer the output here. 132 var buf bytes.Buffer 133 134 // We don't know how many symbols we have, but we 135 // do have symbol information. Pprof only cares whether 136 // this number is 0 (no symbols available) or > 0. 137 fmt.Fprintf(&buf, "num_symbols: 1\n") 138 139 var b *bufio.Reader 140 if r.Method == "POST" { 141 b = bufio.NewReader(r.Body) 142 } else { 143 b = bufio.NewReader(strings.NewReader(r.URL.RawQuery)) 144 } 145 146 for { 147 word, err := b.ReadSlice('+') 148 if err == nil { 149 word = word[0 : len(word)-1] // trim + 150 } 151 pc, _ := strconv.ParseUint(string(word), 0, 64) 152 if pc != 0 { 153 f := runtime.FuncForPC(uintptr(pc)) 154 if f != nil { 155 fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name()) 156 } 157 } 158 159 // Wait until here to check for err; the last 160 // symbol will have an err because it doesn't end in +. 161 if err != nil { 162 if err != io.EOF { 163 fmt.Fprintf(&buf, "reading request: %v\n", err) 164 } 165 break 166 } 167 } 168 169 w.Write(buf.Bytes()) 170 } 171 172 // Handler returns an HTTP handler that serves the named profile. 173 func Handler(name string) http.Handler { 174 return handler(name) 175 } 176 177 type handler string 178 179 func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 180 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 181 debug, _ := strconv.Atoi(r.FormValue("debug")) 182 p := pprof.Lookup(string(name)) 183 if p == nil { 184 w.WriteHeader(404) 185 fmt.Fprintf(w, "Unknown profile: %s\n", name) 186 return 187 } 188 gc, _ := strconv.Atoi(r.FormValue("gc")) 189 if name == "heap" && gc > 0 { 190 runtime.GC() 191 } 192 p.WriteTo(w, debug) 193 } 194 195 // Index responds with the pprof-formatted profile named by the request. 196 // For example, "/debug/pprof/heap" serves the "heap" profile. 197 // Index responds to a request for "/debug/pprof/" with an HTML page 198 // listing the available profiles. 199 func Index(w http.ResponseWriter, r *http.Request) { 200 if strings.HasPrefix(r.URL.Path, "/debug/pprof/") { 201 name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/") 202 if name != "" { 203 handler(name).ServeHTTP(w, r) 204 return 205 } 206 } 207 208 // [BEGIN modification] 209 err := indexTmpl.Execute(w, map[string]any{ 210 "Tok": r.FormValue("tok"), 211 "Profiles": pprof.Profiles(), 212 }) 213 if err != nil { 214 log.Print(err) 215 } 216 // [END modification] 217 } 218 219 var indexTmpl = template.Must(template.New("index").Parse(`<html> 220 <head> 221 <title>/debug/pprof/</title> 222 </head> 223 <body> 224 /debug/pprof/<br> 225 <br> 226 profiles:<br> 227 <table> 228 {{/* [BEGIN modification] */}} 229 {{range .Profiles}} 230 <tr><td align=right>{{.Count}}<td><a href="{{.Name}}?debug=1&tok={{$.Tok}}">{{.Name}}</a> 231 {{end}} 232 </table> 233 <br> 234 <a href="goroutine?debug=2&tok={{.Tok}}">full goroutine stack dump</a><br> 235 {{/* [END modification] */}} 236 </body> 237 </html> 238 `))