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

View as plain text