go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/pprof/internal/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 internal
     6  
     7  // This code is copied from go/src/net/http/pprof/pprof.go at revision
     8  // 01c144c410b09d8b56d40e7e9c54fface204aa29 of the official Golang repo.
     9  //
    10  // Modifications:
    11  //  * Different package name, no package doc.
    12  //  * Removed init() section, since we don't want to autoregister routes in the
    13  //    default router (that's the main reason we are copy-pasting code instead of
    14  //    importing the package).
    15  //  * Modified Index() to put '&tok=...' into URLs.
    16  
    17  import (
    18  	"bufio"
    19  	"bytes"
    20  	"fmt"
    21  	"html/template"
    22  	"io"
    23  	"log"
    24  	"net/http"
    25  	"os"
    26  	"runtime"
    27  	"runtime/pprof"
    28  	"runtime/trace"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  )
    33  
    34  // Cmdline responds with the running program's
    35  // command line, with arguments separated by NUL bytes.
    36  // The package initialization registers it as /debug/pprof/cmdline.
    37  func Cmdline(w http.ResponseWriter, r *http.Request) {
    38  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    39  	fmt.Fprintf(w, strings.Join(os.Args, "\x00"))
    40  }
    41  
    42  func sleep(w http.ResponseWriter, d time.Duration) {
    43  	var clientGone <-chan bool
    44  	if cn, ok := w.(http.CloseNotifier); ok {
    45  		clientGone = cn.CloseNotify()
    46  	}
    47  	select {
    48  	case <-time.After(d):
    49  	case <-clientGone:
    50  	}
    51  }
    52  
    53  func durationExceedsWriteTimeout(r *http.Request, seconds float64) bool {
    54  	srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server)
    55  	return ok && srv.WriteTimeout != 0 && seconds >= srv.WriteTimeout.Seconds()
    56  }
    57  
    58  // Profile responds with the pprof-formatted cpu profile.
    59  // The package initialization registers it as /debug/pprof/profile.
    60  func Profile(w http.ResponseWriter, r *http.Request) {
    61  	sec, _ := strconv.ParseInt(r.FormValue("seconds"), 10, 64)
    62  	if sec == 0 {
    63  		sec = 30
    64  	}
    65  
    66  	if durationExceedsWriteTimeout(r, float64(sec)) {
    67  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    68  		w.Header().Set("X-Go-Pprof", "1")
    69  		w.WriteHeader(http.StatusBadRequest)
    70  		fmt.Fprintln(w, "profile duration exceeds server's WriteTimeout")
    71  		return
    72  	}
    73  
    74  	// Set Content Type assuming StartCPUProfile will work,
    75  	// because if it does it starts writing.
    76  	w.Header().Set("Content-Type", "application/octet-stream")
    77  	if err := pprof.StartCPUProfile(w); err != nil {
    78  		// StartCPUProfile failed, so no writes yet.
    79  		// Can change header back to text content
    80  		// and send error code.
    81  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    82  		w.Header().Set("X-Go-Pprof", "1")
    83  		w.WriteHeader(http.StatusInternalServerError)
    84  		fmt.Fprintf(w, "Could not enable CPU profiling: %s\n", err)
    85  		return
    86  	}
    87  	sleep(w, time.Duration(sec)*time.Second)
    88  	pprof.StopCPUProfile()
    89  }
    90  
    91  // Trace responds with the execution trace in binary form.
    92  // Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.
    93  // The package initialization registers it as /debug/pprof/trace.
    94  func Trace(w http.ResponseWriter, r *http.Request) {
    95  	sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64)
    96  	if sec <= 0 || err != nil {
    97  		sec = 1
    98  	}
    99  
   100  	if durationExceedsWriteTimeout(r, sec) {
   101  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   102  		w.Header().Set("X-Go-Pprof", "1")
   103  		w.WriteHeader(http.StatusBadRequest)
   104  		fmt.Fprintln(w, "profile duration exceeds server's WriteTimeout")
   105  		return
   106  	}
   107  
   108  	// Set Content Type assuming trace.Start will work,
   109  	// because if it does it starts writing.
   110  	w.Header().Set("Content-Type", "application/octet-stream")
   111  	if err := trace.Start(w); err != nil {
   112  		// trace.Start failed, so no writes yet.
   113  		// Can change header back to text content and send error code.
   114  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   115  		w.Header().Set("X-Go-Pprof", "1")
   116  		w.WriteHeader(http.StatusInternalServerError)
   117  		fmt.Fprintf(w, "Could not enable tracing: %s\n", err)
   118  		return
   119  	}
   120  	sleep(w, time.Duration(sec*float64(time.Second)))
   121  	trace.Stop()
   122  }
   123  
   124  // Symbol looks up the program counters listed in the request,
   125  // responding with a table mapping program counters to function names.
   126  // The package initialization registers it as /debug/pprof/symbol.
   127  func Symbol(w http.ResponseWriter, r *http.Request) {
   128  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   129  
   130  	// We have to read the whole POST body before
   131  	// writing any output. Buffer the output here.
   132  	var buf bytes.Buffer
   133  
   134  	// We don't know how many symbols we have, but we
   135  	// do have symbol information. Pprof only cares whether
   136  	// this number is 0 (no symbols available) or > 0.
   137  	fmt.Fprintf(&buf, "num_symbols: 1\n")
   138  
   139  	var b *bufio.Reader
   140  	if r.Method == "POST" {
   141  		b = bufio.NewReader(r.Body)
   142  	} else {
   143  		b = bufio.NewReader(strings.NewReader(r.URL.RawQuery))
   144  	}
   145  
   146  	for {
   147  		word, err := b.ReadSlice('+')
   148  		if err == nil {
   149  			word = word[0 : len(word)-1] // trim +
   150  		}
   151  		pc, _ := strconv.ParseUint(string(word), 0, 64)
   152  		if pc != 0 {
   153  			f := runtime.FuncForPC(uintptr(pc))
   154  			if f != nil {
   155  				fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name())
   156  			}
   157  		}
   158  
   159  		// Wait until here to check for err; the last
   160  		// symbol will have an err because it doesn't end in +.
   161  		if err != nil {
   162  			if err != io.EOF {
   163  				fmt.Fprintf(&buf, "reading request: %v\n", err)
   164  			}
   165  			break
   166  		}
   167  	}
   168  
   169  	w.Write(buf.Bytes())
   170  }
   171  
   172  // Handler returns an HTTP handler that serves the named profile.
   173  func Handler(name string) http.Handler {
   174  	return handler(name)
   175  }
   176  
   177  type handler string
   178  
   179  func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   180  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   181  	debug, _ := strconv.Atoi(r.FormValue("debug"))
   182  	p := pprof.Lookup(string(name))
   183  	if p == nil {
   184  		w.WriteHeader(404)
   185  		fmt.Fprintf(w, "Unknown profile: %s\n", name)
   186  		return
   187  	}
   188  	gc, _ := strconv.Atoi(r.FormValue("gc"))
   189  	if name == "heap" && gc > 0 {
   190  		runtime.GC()
   191  	}
   192  	p.WriteTo(w, debug)
   193  }
   194  
   195  // Index responds with the pprof-formatted profile named by the request.
   196  // For example, "/debug/pprof/heap" serves the "heap" profile.
   197  // Index responds to a request for "/debug/pprof/" with an HTML page
   198  // listing the available profiles.
   199  func Index(w http.ResponseWriter, r *http.Request) {
   200  	if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
   201  		name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/")
   202  		if name != "" {
   203  			handler(name).ServeHTTP(w, r)
   204  			return
   205  		}
   206  	}
   207  
   208  	// [BEGIN modification]
   209  	err := indexTmpl.Execute(w, map[string]any{
   210  		"Tok":      r.FormValue("tok"),
   211  		"Profiles": pprof.Profiles(),
   212  	})
   213  	if err != nil {
   214  		log.Print(err)
   215  	}
   216  	// [END modification]
   217  }
   218  
   219  var indexTmpl = template.Must(template.New("index").Parse(`<html>
   220  <head>
   221  <title>/debug/pprof/</title>
   222  </head>
   223  <body>
   224  /debug/pprof/<br>
   225  <br>
   226  profiles:<br>
   227  <table>
   228  {{/* [BEGIN modification] */}}
   229  {{range .Profiles}}
   230  <tr><td align=right>{{.Count}}<td><a href="{{.Name}}?debug=1&tok={{$.Tok}}">{{.Name}}</a>
   231  {{end}}
   232  </table>
   233  <br>
   234  <a href="goroutine?debug=2&tok={{.Tok}}">full goroutine stack dump</a><br>
   235  {{/* [END modification] */}}
   236  </body>
   237  </html>
   238  `))