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/goexperiment"
81 "internal/profile"
82 "io"
83 "log"
84 "net/http"
85 "net/url"
86 "os"
87 "runtime"
88 "runtime/pprof"
89 "runtime/trace"
90 "slices"
91 "strconv"
92 "strings"
93 "time"
94 )
95
96 func init() {
97 prefix := ""
98 if godebug.New("httpmuxgo121").Value() != "1" {
99 prefix = "GET "
100 }
101 http.HandleFunc(prefix+"/debug/pprof/", Index)
102 http.HandleFunc(prefix+"/debug/pprof/cmdline", Cmdline)
103 http.HandleFunc(prefix+"/debug/pprof/profile", Profile)
104 http.HandleFunc(prefix+"/debug/pprof/symbol", Symbol)
105 http.HandleFunc(prefix+"/debug/pprof/trace", Trace)
106 }
107
108
109
110
111 func Cmdline(w http.ResponseWriter, r *http.Request) {
112 w.Header().Set("X-Content-Type-Options", "nosniff")
113 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
114 fmt.Fprint(w, strings.Join(os.Args, "\x00"))
115 }
116
117 func sleep(r *http.Request, d time.Duration) {
118 select {
119 case <-time.After(d):
120 case <-r.Context().Done():
121 }
122 }
123
124 func configureWriteDeadline(w http.ResponseWriter, r *http.Request, seconds float64) {
125 srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server)
126 if ok && srv.WriteTimeout > 0 {
127 timeout := srv.WriteTimeout + time.Duration(seconds*float64(time.Second))
128
129 rc := http.NewResponseController(w)
130 rc.SetWriteDeadline(time.Now().Add(timeout))
131 }
132 }
133
134 func serveError(w http.ResponseWriter, status int, txt string) {
135 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
136 w.Header().Set("X-Go-Pprof", "1")
137 w.Header().Del("Content-Disposition")
138 w.WriteHeader(status)
139 fmt.Fprintln(w, txt)
140 }
141
142
143
144
145 func Profile(w http.ResponseWriter, r *http.Request) {
146 w.Header().Set("X-Content-Type-Options", "nosniff")
147 sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64)
148 if sec <= 0 || err != nil {
149 sec = 30
150 }
151
152 configureWriteDeadline(w, r, float64(sec))
153
154
155
156 w.Header().Set("Content-Type", "application/octet-stream")
157 w.Header().Set("Content-Disposition", `attachment; filename="profile"`)
158 if err := pprof.StartCPUProfile(w); err != nil {
159
160 serveError(w, http.StatusInternalServerError,
161 fmt.Sprintf("Could not enable CPU profiling: %s", err))
162 return
163 }
164 sleep(r, time.Duration(sec)*time.Second)
165 pprof.StopCPUProfile()
166 }
167
168
169
170
171 func Trace(w http.ResponseWriter, r *http.Request) {
172 w.Header().Set("X-Content-Type-Options", "nosniff")
173 sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64)
174 if sec <= 0 || err != nil {
175 sec = 1
176 }
177
178 configureWriteDeadline(w, r, sec)
179
180
181
182 w.Header().Set("Content-Type", "application/octet-stream")
183 w.Header().Set("Content-Disposition", `attachment; filename="trace"`)
184 if err := trace.Start(w); err != nil {
185
186 serveError(w, http.StatusInternalServerError,
187 fmt.Sprintf("Could not enable tracing: %s", err))
188 return
189 }
190 sleep(r, time.Duration(sec*float64(time.Second)))
191 trace.Stop()
192 }
193
194
195
196
197 func Symbol(w http.ResponseWriter, r *http.Request) {
198 w.Header().Set("X-Content-Type-Options", "nosniff")
199 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
200
201
202
203 var buf bytes.Buffer
204
205
206
207
208 fmt.Fprintf(&buf, "num_symbols: 1\n")
209
210 var b *bufio.Reader
211 if r.Method == "POST" {
212 b = bufio.NewReader(r.Body)
213 } else {
214 b = bufio.NewReader(strings.NewReader(r.URL.RawQuery))
215 }
216
217 for {
218 word, err := b.ReadSlice('+')
219 if err == nil {
220 word = word[0 : len(word)-1]
221 }
222 pc, _ := strconv.ParseUint(string(word), 0, 64)
223 if pc != 0 {
224 f := runtime.FuncForPC(uintptr(pc))
225 if f != nil {
226 fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name())
227 }
228 }
229
230
231
232 if err != nil {
233 if err != io.EOF {
234 fmt.Fprintf(&buf, "reading request: %v\n", err)
235 }
236 break
237 }
238 }
239
240 w.Write(buf.Bytes())
241 }
242
243
244
245 func Handler(name string) http.Handler {
246 return handler(name)
247 }
248
249 type handler string
250
251 func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
252 w.Header().Set("X-Content-Type-Options", "nosniff")
253 p := pprof.Lookup(string(name))
254 if p == nil {
255 serveError(w, http.StatusNotFound, "Unknown profile")
256 return
257 }
258 if sec := r.FormValue("seconds"); sec != "" {
259 name.serveDeltaProfile(w, r, p, sec)
260 return
261 }
262 gc, _ := strconv.Atoi(r.FormValue("gc"))
263 if name == "heap" && gc > 0 {
264 runtime.GC()
265 }
266 debug, _ := strconv.Atoi(r.FormValue("debug"))
267 if debug != 0 {
268 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
269 } else {
270 w.Header().Set("Content-Type", "application/octet-stream")
271 w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
272 }
273 p.WriteTo(w, debug)
274 }
275
276 func (name handler) serveDeltaProfile(w http.ResponseWriter, r *http.Request, p *pprof.Profile, secStr string) {
277 sec, err := strconv.ParseInt(secStr, 10, 64)
278 if err != nil || sec <= 0 {
279 serveError(w, http.StatusBadRequest, `invalid value for "seconds" - must be a positive integer`)
280 return
281 }
282
283 if !profileSupportsDelta[name] {
284 serveError(w, http.StatusBadRequest, `"seconds" parameter is not supported for this profile type`)
285 return
286 }
287
288 configureWriteDeadline(w, r, float64(sec))
289
290 debug, _ := strconv.Atoi(r.FormValue("debug"))
291 if debug != 0 {
292 serveError(w, http.StatusBadRequest, "seconds and debug params are incompatible")
293 return
294 }
295 p0, err := collectProfile(p)
296 if err != nil {
297 serveError(w, http.StatusInternalServerError, "failed to collect profile")
298 return
299 }
300
301 t := time.NewTimer(time.Duration(sec) * time.Second)
302 defer t.Stop()
303
304 select {
305 case <-r.Context().Done():
306 err := r.Context().Err()
307 if err == context.DeadlineExceeded {
308 serveError(w, http.StatusRequestTimeout, err.Error())
309 } else {
310 serveError(w, http.StatusInternalServerError, err.Error())
311 }
312 return
313 case <-t.C:
314 }
315
316 p1, err := collectProfile(p)
317 if err != nil {
318 serveError(w, http.StatusInternalServerError, "failed to collect profile")
319 return
320 }
321 ts := p1.TimeNanos
322 dur := p1.TimeNanos - p0.TimeNanos
323
324 p0.Scale(-1)
325
326 p1, err = profile.Merge([]*profile.Profile{p0, p1})
327 if err != nil {
328 serveError(w, http.StatusInternalServerError, "failed to compute delta")
329 return
330 }
331
332 p1.TimeNanos = ts
333 p1.DurationNanos = dur
334
335 w.Header().Set("Content-Type", "application/octet-stream")
336 w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-delta"`, name))
337 p1.Write(w)
338 }
339
340 func collectProfile(p *pprof.Profile) (*profile.Profile, error) {
341 var buf bytes.Buffer
342 if err := p.WriteTo(&buf, 0); err != nil {
343 return nil, err
344 }
345 ts := time.Now().UnixNano()
346 p0, err := profile.Parse(&buf)
347 if err != nil {
348 return nil, err
349 }
350 p0.TimeNanos = ts
351 return p0, nil
352 }
353
354 var profileSupportsDelta = map[handler]bool{
355 "allocs": true,
356 "block": true,
357 "goroutineleak": true,
358 "goroutine": true,
359 "heap": true,
360 "mutex": true,
361 "threadcreate": true,
362 }
363
364 var profileDescriptions = map[string]string{
365 "allocs": "A sampling of all past memory allocations",
366 "block": "Stack traces that led to blocking on synchronization primitives",
367 "cmdline": "The command line invocation of the current program",
368 "goroutine": "Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
369 "heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
370 "mutex": "Stack traces of holders of contended mutexes",
371 "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.",
372 "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 '+'.",
373 "threadcreate": "Stack traces that led to the creation of new OS threads",
374 "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.",
375 }
376
377 func init() {
378 if goexperiment.GoroutineLeakProfile {
379 profileDescriptions["goroutineleak"] = "Stack traces of all leaked goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic."
380 }
381 }
382
383 type profileEntry struct {
384 Name string
385 Href string
386 Desc string
387 Count int
388 }
389
390
391
392
393
394 func Index(w http.ResponseWriter, r *http.Request) {
395 if name, found := strings.CutPrefix(r.URL.Path, "/debug/pprof/"); found {
396 if name != "" {
397 handler(name).ServeHTTP(w, r)
398 return
399 }
400 }
401
402 w.Header().Set("X-Content-Type-Options", "nosniff")
403 w.Header().Set("Content-Type", "text/html; charset=utf-8")
404
405 var profiles []profileEntry
406 for _, p := range pprof.Profiles() {
407 profiles = append(profiles, profileEntry{
408 Name: p.Name(),
409 Href: p.Name(),
410 Desc: profileDescriptions[p.Name()],
411 Count: p.Count(),
412 })
413 }
414
415
416 for _, p := range []string{"cmdline", "profile", "symbol", "trace"} {
417 profiles = append(profiles, profileEntry{
418 Name: p,
419 Href: p,
420 Desc: profileDescriptions[p],
421 })
422 }
423
424 slices.SortFunc(profiles, func(a, b profileEntry) int {
425 return strings.Compare(a.Name, b.Name)
426 })
427
428 if err := indexTmplExecute(w, profiles); err != nil {
429 log.Print(err)
430 }
431 }
432
433 func indexTmplExecute(w io.Writer, profiles []profileEntry) error {
434 var b bytes.Buffer
435 b.WriteString(`<html>
436 <head>
437 <title>/debug/pprof/</title>
438 <style>
439 .profile-name{
440 display:inline-block;
441 width:6rem;
442 }
443 </style>
444 </head>
445 <body>
446 /debug/pprof/
447 <br>
448 <p>Set debug=1 as a query parameter to export in legacy text format</p>
449 <br>
450 Types of profiles available:
451 <table>
452 <thead><td>Count</td><td>Profile</td></thead>
453 `)
454
455 for _, profile := range profiles {
456 link := &url.URL{Path: profile.Href, RawQuery: "debug=1"}
457 fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(profile.Name))
458 }
459
460 b.WriteString(`</table>
461 <a href="goroutine?debug=2">full goroutine stack dump</a>
462 <br>
463 <p>
464 Profile Descriptions:
465 <ul>
466 `)
467 for _, profile := range profiles {
468 fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(profile.Name), html.EscapeString(profile.Desc))
469 }
470 b.WriteString(`</ul>
471 </p>
472 </body>
473 </html>`)
474
475 _, err := w.Write(b.Bytes())
476 return err
477 }
478
View as plain text