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 "goroutine": true,
357 "heap": true,
358 "mutex": true,
359 "threadcreate": true,
360 }
361
362 var 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 "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 '+'.",
371 "threadcreate": "Stack traces that led to the creation of new OS threads",
372 "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.",
373 }
374
375 type profileEntry struct {
376 Name string
377 Href string
378 Desc string
379 Count int
380 }
381
382
383
384
385
386 func Index(w http.ResponseWriter, r *http.Request) {
387 if name, found := strings.CutPrefix(r.URL.Path, "/debug/pprof/"); found {
388 if name != "" {
389 handler(name).ServeHTTP(w, r)
390 return
391 }
392 }
393
394 w.Header().Set("X-Content-Type-Options", "nosniff")
395 w.Header().Set("Content-Type", "text/html; charset=utf-8")
396
397 var profiles []profileEntry
398 for _, p := range pprof.Profiles() {
399 profiles = append(profiles, profileEntry{
400 Name: p.Name(),
401 Href: p.Name(),
402 Desc: profileDescriptions[p.Name()],
403 Count: p.Count(),
404 })
405 }
406
407
408 for _, p := range []string{"cmdline", "profile", "symbol", "trace"} {
409 profiles = append(profiles, profileEntry{
410 Name: p,
411 Href: p,
412 Desc: profileDescriptions[p],
413 })
414 }
415
416 slices.SortFunc(profiles, func(a, b profileEntry) int {
417 return strings.Compare(a.Name, b.Name)
418 })
419
420 if err := indexTmplExecute(w, profiles); err != nil {
421 log.Print(err)
422 }
423 }
424
425 func indexTmplExecute(w io.Writer, profiles []profileEntry) error {
426 var b bytes.Buffer
427 b.WriteString(`<html>
428 <head>
429 <title>/debug/pprof/</title>
430 <style>
431 .profile-name{
432 display:inline-block;
433 width:6rem;
434 }
435 </style>
436 </head>
437 <body>
438 /debug/pprof/
439 <br>
440 <p>Set debug=1 as a query parameter to export in legacy text format</p>
441 <br>
442 Types of profiles available:
443 <table>
444 <thead><td>Count</td><td>Profile</td></thead>
445 `)
446
447 for _, profile := range profiles {
448 link := &url.URL{Path: profile.Href, RawQuery: "debug=1"}
449 fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(profile.Name))
450 }
451
452 b.WriteString(`</table>
453 <a href="goroutine?debug=2">full goroutine stack dump</a>
454 <br>
455 <p>
456 Profile Descriptions:
457 <ul>
458 `)
459 for _, profile := range profiles {
460 fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(profile.Name), html.EscapeString(profile.Desc))
461 }
462 b.WriteString(`</ul>
463 </p>
464 </body>
465 </html>`)
466
467 _, err := w.Write(b.Bytes())
468 return err
469 }
470
View as plain text