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