1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 package pprof
72
73 import (
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 "slices"
90 "strconv"
91 "strings"
92 "time"
93 )
94
95 func 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
108
109
110 func 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
116 func sleep(r *http.Request, d time.Duration) {
117 select {
118 case <-time.After(d):
119 case <-r.Context().Done():
120 }
121 }
122
123 func 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
133 func 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
142
143
144 func 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
154
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
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
168
169
170 func 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
180
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
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
194
195
196 func 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
201
202 var buf bytes.Buffer
203
204
205
206
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]
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
230
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
243
244 func Handler(name string) http.Handler {
245 return handler(name)
246 }
247
248 type handler string
249
250 func (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
275 func (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
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 {
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
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
339 func 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
353 var profileSupportsDelta = map[handler]bool{
354 "allocs": true,
355 "block": true,
356 "goroutineleak": true,
357 "goroutine": true,
358 "heap": true,
359 "mutex": true,
360 "threadcreate": true,
361 }
362
363 var profileDescriptions = map[string]string{
364 "allocs": "A sampling of all past memory allocations",
365 "block": "Stack traces that led to blocking on synchronization primitives",
366 "cmdline": "The command line invocation of the current program",
367 "goroutine": "Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
368 "heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
369 "mutex": "Stack traces of holders of contended mutexes",
370 "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.",
371 "symbol": "Maps given program counters to function names. Counters can be specified in a GET raw query or POST body, multiple counters are separated by '+'.",
372 "threadcreate": "Stack traces that led to the creation of new OS threads",
373 "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.",
374 "goroutineleak": "Stack traces of all leaked goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
375 }
376
377 type profileEntry struct {
378 Name string
379 Href string
380 Desc string
381 Count int
382 }
383
384
385
386
387
388 func Index(w http.ResponseWriter, r *http.Request) {
389 if name, found := strings.CutPrefix(r.URL.Path, "/debug/pprof/"); found {
390 if name != "" {
391 handler(name).ServeHTTP(w, r)
392 return
393 }
394 }
395
396 w.Header().Set("X-Content-Type-Options", "nosniff")
397 w.Header().Set("Content-Type", "text/html; charset=utf-8")
398
399 var profiles []profileEntry
400 for _, p := range pprof.Profiles() {
401 profiles = append(profiles, profileEntry{
402 Name: p.Name(),
403 Href: p.Name(),
404 Desc: profileDescriptions[p.Name()],
405 Count: p.Count(),
406 })
407 }
408
409
410 for _, p := range []string{"cmdline", "profile", "symbol", "trace"} {
411 profiles = append(profiles, profileEntry{
412 Name: p,
413 Href: p,
414 Desc: profileDescriptions[p],
415 })
416 }
417
418 slices.SortFunc(profiles, func(a, b profileEntry) int {
419 return strings.Compare(a.Name, b.Name)
420 })
421
422 if err := indexTmplExecute(w, profiles); err != nil {
423 log.Print(err)
424 }
425 }
426
427 func indexTmplExecute(w io.Writer, profiles []profileEntry) error {
428 var b bytes.Buffer
429 b.WriteString(`<html>
430 <head>
431 <title>/debug/pprof/</title>
432 <style>
433 .profile-name{
434 display:inline-block;
435 width:6rem;
436 }
437 </style>
438 </head>
439 <body>
440 /debug/pprof/
441 <br>
442 <p>Set debug=1 as a query parameter to export in legacy text format</p>
443 <br>
444 Types of profiles available:
445 <table>
446 <thead><td>Count</td><td>Profile</td></thead>
447 `)
448
449 for _, profile := range profiles {
450 link := &url.URL{Path: profile.Href, RawQuery: "debug=1"}
451 fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(profile.Name))
452 }
453
454 b.WriteString(`</table>
455 <a href="goroutine?debug=2">full goroutine stack dump</a>
456 <br>
457 <p>
458 Profile Descriptions:
459 <ul>
460 `)
461 for _, profile := range profiles {
462 fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(profile.Name), html.EscapeString(profile.Desc))
463 }
464 b.WriteString(`</ul>
465 </p>
466 </body>
467 </html>`)
468
469 _, err := w.Write(b.Bytes())
470 return err
471 }
472
View as plain text