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