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