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  	"sort"
    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  	"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  
   374  type 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.
   385  func 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  
   424  func 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>
   441  Types 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>
   455  Profile 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  

View as plain text