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://blog.golang.org/2011/06/profiling-go-programs.html.
    71  package pprof
    72  
    73  import (
    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  	"slices"
    90  	"strconv"
    91  	"strings"
    92  	"time"
    93  )
    94  
    95  func 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.
   110  func 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  
   116  func sleep(r *http.Request, d time.Duration) {
   117  	select {
   118  	case <-time.After(d):
   119  	case <-r.Context().Done():
   120  	}
   121  }
   122  
   123  func 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  
   133  func 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.
   144  func 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.
   170  func 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.
   196  func 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].
   244  func Handler(name string) http.Handler {
   245  	return handler(name)
   246  }
   247  
   248  type handler string
   249  
   250  func (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  
   275  func (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  
   339  func 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  
   353  var profileSupportsDelta = map[handler]bool{
   354  	"allocs":       true,
   355  	"block":        true,
   356  	"goroutine":    true,
   357  	"heap":         true,
   358  	"mutex":        true,
   359  	"threadcreate": true,
   360  }
   361  
   362  var 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  	"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 '+'.",
   371  	"threadcreate": "Stack traces that led to the creation of new OS threads",
   372  	"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.",
   373  }
   374  
   375  type profileEntry struct {
   376  	Name  string
   377  	Href  string
   378  	Desc  string
   379  	Count int
   380  }
   381  
   382  // Index responds with the pprof-formatted profile named by the request.
   383  // For example, "/debug/pprof/heap" serves the "heap" profile.
   384  // Index responds to a request for "/debug/pprof/" with an HTML page
   385  // listing the available profiles.
   386  func Index(w http.ResponseWriter, r *http.Request) {
   387  	if name, found := strings.CutPrefix(r.URL.Path, "/debug/pprof/"); found {
   388  		if name != "" {
   389  			handler(name).ServeHTTP(w, r)
   390  			return
   391  		}
   392  	}
   393  
   394  	w.Header().Set("X-Content-Type-Options", "nosniff")
   395  	w.Header().Set("Content-Type", "text/html; charset=utf-8")
   396  
   397  	var profiles []profileEntry
   398  	for _, p := range pprof.Profiles() {
   399  		profiles = append(profiles, profileEntry{
   400  			Name:  p.Name(),
   401  			Href:  p.Name(),
   402  			Desc:  profileDescriptions[p.Name()],
   403  			Count: p.Count(),
   404  		})
   405  	}
   406  
   407  	// Adding other profiles exposed from within this package
   408  	for _, p := range []string{"cmdline", "profile", "symbol", "trace"} {
   409  		profiles = append(profiles, profileEntry{
   410  			Name: p,
   411  			Href: p,
   412  			Desc: profileDescriptions[p],
   413  		})
   414  	}
   415  
   416  	slices.SortFunc(profiles, func(a, b profileEntry) int {
   417  		return strings.Compare(a.Name, b.Name)
   418  	})
   419  
   420  	if err := indexTmplExecute(w, profiles); err != nil {
   421  		log.Print(err)
   422  	}
   423  }
   424  
   425  func indexTmplExecute(w io.Writer, profiles []profileEntry) error {
   426  	var b bytes.Buffer
   427  	b.WriteString(`<html>
   428  <head>
   429  <title>/debug/pprof/</title>
   430  <style>
   431  .profile-name{
   432  	display:inline-block;
   433  	width:6rem;
   434  }
   435  </style>
   436  </head>
   437  <body>
   438  /debug/pprof/
   439  <br>
   440  <p>Set debug=1 as a query parameter to export in legacy text format</p>
   441  <br>
   442  Types of profiles available:
   443  <table>
   444  <thead><td>Count</td><td>Profile</td></thead>
   445  `)
   446  
   447  	for _, profile := range profiles {
   448  		link := &url.URL{Path: profile.Href, RawQuery: "debug=1"}
   449  		fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(profile.Name))
   450  	}
   451  
   452  	b.WriteString(`</table>
   453  <a href="goroutine?debug=2">full goroutine stack dump</a>
   454  <br>
   455  <p>
   456  Profile Descriptions:
   457  <ul>
   458  `)
   459  	for _, profile := range profiles {
   460  		fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(profile.Name), html.EscapeString(profile.Desc))
   461  	}
   462  	b.WriteString(`</ul>
   463  </p>
   464  </body>
   465  </html>`)
   466  
   467  	_, err := w.Write(b.Bytes())
   468  	return err
   469  }
   470  

View as plain text