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