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