github.com/Kolosok86/http@v0.1.2/pprof/pprof.go (about)

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