1// Copyright 2010 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package pprof serves via its HTTP server runtime profiling data
6// in the format expected by the pprof visualization tool.
7//
8// The package is typically only imported for the side effect of
9// registering its HTTP handlers.
10// The handled paths all begin with /debug/pprof/.
11// As of Go 1.22, all the paths must be requested with GET.
12//
13// To use pprof, link this package into your program:
14//
15//	import _ "net/http/pprof"
16//
17// If your application is not already running an http server, you
18// need to start one. Add "net/http" and "log" to your imports and
19// the following code to your main function:
20//
21//	go func() {
22//		log.Println(http.ListenAndServe("localhost:6060", nil))
23//	}()
24//
25// By default, all the profiles listed in [runtime/pprof.Profile] are
26// available (via [Handler]), in addition to the [Cmdline], [Profile], [Symbol],
27// and [Trace] profiles defined in this package.
28// If you are not using DefaultServeMux, you will have to register handlers
29// with the mux you are using.
30//
31// # Parameters
32//
33// Parameters can be passed via GET query params:
34//
35//   - debug=N (all profiles): response format: N = 0: binary (default), N > 0: plaintext
36//   - gc=N (heap profile): N > 0: run a garbage collection cycle before profiling
37//   - seconds=N (allocs, block, goroutine, heap, mutex, threadcreate profiles): return a delta profile
38//   - seconds=N (cpu (profile), trace profiles): profile for the given duration
39//
40// # Usage examples
41//
42// Use the pprof tool to look at the heap profile:
43//
44//	go tool pprof http://localhost:6060/debug/pprof/heap
45//
46// Or to look at a 30-second CPU profile:
47//
48//	go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
49//
50// Or to look at the goroutine blocking profile, after calling
51// [runtime.SetBlockProfileRate] in your program:
52//
53//	go tool pprof http://localhost:6060/debug/pprof/block
54//
55// Or to look at the holders of contended mutexes, after calling
56// [runtime.SetMutexProfileFraction] in your program:
57//
58//	go tool pprof http://localhost:6060/debug/pprof/mutex
59//
60// The package also exports a handler that serves execution trace data
61// for the "go tool trace" command. To collect a 5-second execution trace:
62//
63//	curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
64//	go tool trace trace.out
65//
66// To view all available profiles, open http://localhost:6060/debug/pprof/
67// in your browser.
68//
69// For a study of the facility in action, visit
70// https://blog.golang.org/2011/06/profiling-go-programs.html.
71package pprof
72
73import (
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
95func 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// Cmdline responds with the running program's
108// command line, with arguments separated by NUL bytes.
109// The package initialization registers it as /debug/pprof/cmdline.
110func 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
116func sleep(r *http.Request, d time.Duration) {
117	select {
118	case <-time.After(d):
119	case <-r.Context().Done():
120	}
121}
122
123func 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
133func 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// Profile responds with the pprof-formatted cpu profile.
142// Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified.
143// The package initialization registers it as /debug/pprof/profile.
144func 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	// Set Content Type assuming StartCPUProfile will work,
154	// because if it does it starts writing.
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		// StartCPUProfile failed, so no writes yet.
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// Trace responds with the execution trace in binary form.
168// Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.
169// The package initialization registers it as /debug/pprof/trace.
170func 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	// Set Content Type assuming trace.Start will work,
180	// because if it does it starts writing.
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		// trace.Start failed, so no writes yet.
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// Symbol looks up the program counters listed in the request,
194// responding with a table mapping program counters to function names.
195// The package initialization registers it as /debug/pprof/symbol.
196func 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	// We have to read the whole POST body before
201	// writing any output. Buffer the output here.
202	var buf bytes.Buffer
203
204	// We don't know how many symbols we have, but we
205	// do have symbol information. Pprof only cares whether
206	// this number is 0 (no symbols available) or > 0.
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] // trim +
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		// Wait until here to check for err; the last
230		// symbol will have an err because it doesn't end in +.
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// Handler returns an HTTP handler that serves the named profile.
243// Available profiles can be found in [runtime/pprof.Profile].
244func Handler(name string) http.Handler {
245	return handler(name)
246}
247
248type handler string
249
250func (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
275func (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	// 'name' should be a key in profileSupportsDelta.
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 { // TODO: what's a good status code for canceled requests? 400?
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 // set since we don't know what profile.Merge set for TimeNanos.
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
339func 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
353var profileSupportsDelta = map[handler]bool{
354	"allocs":       true,
355	"block":        true,
356	"goroutine":    true,
357	"heap":         true,
358	"mutex":        true,
359	"threadcreate": true,
360}
361
362var 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
374type profileEntry struct {
375	Name  string
376	Href  string
377	Desc  string
378	Count int
379}
380
381// Index responds with the pprof-formatted profile named by the request.
382// For example, "/debug/pprof/heap" serves the "heap" profile.
383// Index responds to a request for "/debug/pprof/" with an HTML page
384// listing the available profiles.
385func 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	// Adding other profiles exposed from within this package
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
424func 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>
441Types 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>
455Profile 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