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  //
    12  // To use pprof, link this package into your program:
    13  //
    14  //	import _ "net/http/pprof"
    15  //
    16  // If your application is not already running an http server, you
    17  // need to start one. Add "net/http" and "log" to your imports and
    18  // the following code to your main function:
    19  //
    20  //	go func() {
    21  //		log.Println(http.ListenAndServe("localhost:6060", nil))
    22  //	}()
    23  //
    24  // By default, all the profiles listed in [runtime/pprof.Profile] are
    25  // available (via [Handler]), in addition to the [Cmdline], [Profile], [Symbol],
    26  // and [Trace] profiles defined in this package.
    27  // If you are not using DefaultServeMux, you will have to register handlers
    28  // with the mux you are using.
    29  //
    30  // # Parameters
    31  //
    32  // Parameters can be passed via GET query params:
    33  //
    34  //   - debug=N (all profiles): response format: N = 0: binary (default), N > 0: plaintext
    35  //   - gc=N (heap profile): N > 0: run a garbage collection cycle before profiling
    36  //   - seconds=N (allocs, block, goroutine, heap, mutex, threadcreate profiles): return a delta profile
    37  //   - seconds=N (cpu (profile), trace profiles): profile for the given duration
    38  //
    39  // # Usage examples
    40  //
    41  // Use the pprof tool to look at the heap profile:
    42  //
    43  //	go tool pprof http://localhost:6060/debug/pprof/heap
    44  //
    45  // Or to look at a 30-second CPU profile:
    46  //
    47  //	go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
    48  //
    49  // Or to look at the goroutine blocking profile, after calling
    50  // [runtime.SetBlockProfileRate] in your program:
    51  //
    52  //	go tool pprof http://localhost:6060/debug/pprof/block
    53  //
    54  // Or to look at the holders of contended mutexes, after calling
    55  // [runtime.SetMutexProfileFraction] in your program:
    56  //
    57  //	go tool pprof http://localhost:6060/debug/pprof/mutex
    58  //
    59  // The package also exports a handler that serves execution trace data
    60  // for the "go tool trace" command. To collect a 5-second execution trace:
    61  //
    62  //	curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
    63  //	go tool trace trace.out
    64  //
    65  // To view all available profiles, open http://localhost:6060/debug/pprof/
    66  // in your browser.
    67  //
    68  // For a study of the facility in action, visit
    69  // https://blog.golang.org/2011/06/profiling-go-programs.html.
    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  // Cmdline responds with the running program's
   102  // command line, with arguments separated by NUL bytes.
   103  // The package initialization registers it as /debug/pprof/cmdline.
   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  // Profile responds with the pprof-formatted cpu profile.
   131  // Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified.
   132  // The package initialization registers it as /debug/pprof/profile.
   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  	// Set Content Type assuming StartCPUProfile will work,
   146  	// because if it does it starts writing.
   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  		// StartCPUProfile failed, so no writes yet.
   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  // Trace responds with the execution trace in binary form.
   160  // Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.
   161  // The package initialization registers it as /debug/pprof/trace.
   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  	// Set Content Type assuming trace.Start will work,
   175  	// because if it does it starts writing.
   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  		// trace.Start failed, so no writes yet.
   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  // Symbol looks up the program counters listed in the request,
   189  // responding with a table mapping program counters to function names.
   190  // The package initialization registers it as /debug/pprof/symbol.
   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  	// We have to read the whole POST body before
   196  	// writing any output. Buffer the output here.
   197  	var buf bytes.Buffer
   198  
   199  	// We don't know how many symbols we have, but we
   200  	// do have symbol information. Pprof only cares whether
   201  	// this number is 0 (no symbols available) or > 0.
   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] // trim +
   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  		// Wait until here to check for err; the last
   225  		// symbol will have an err because it doesn't end in +.
   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  // Handler returns an HTTP handler that serves the named profile.
   238  // Available profiles can be found in [runtime/pprof.Profile].
   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  	// 'name' should be a key in profileSupportsDelta.
   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 { // TODO: what's a good status code for canceled requests? 400?
   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 // set since we don't know what profile.Merge set for TimeNanos.
   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  // Index responds with the pprof-formatted profile named by the request.
   378  // For example, "/debug/pprof/heap" serves the "heap" profile.
   379  // Index responds to a request for "/debug/pprof/" with an HTML page
   380  // listing the available profiles.
   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  	// Adding other profiles exposed from within this package
   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