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