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