Source file src/net/http/pprof/pprof.go

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

View as plain text