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

View as plain text