github.com/stealthrocket/wzprof@v0.2.1-0.20230830205924-5fa86be5e5b3/pprof.go (about)

     1  package wzprof
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"html"
     7  	"io"
     8  	"net/http"
     9  	httpprof "net/http/pprof"
    10  	"net/url"
    11  	"runtime/pprof"
    12  	"sort"
    13  	"strings"
    14  
    15  	"github.com/google/pprof/profile"
    16  )
    17  
    18  // Copyright (c) 2009 The Go Authors. All rights reserved.
    19  //
    20  // Redistribution and use in source and binary forms, with or without
    21  // modification, are permitted provided that the following conditions are
    22  // met:
    23  //
    24  //    * Redistributions of source code must retain the above copyright
    25  // notice, this list of conditions and the following disclaimer.
    26  //    * Redistributions in binary form must reproduce the above
    27  // copyright notice, this list of conditions and the following disclaimer
    28  // in the documentation and/or other materials provided with the
    29  // distribution.
    30  //    * Neither the name of Google Inc. nor the names of its
    31  // contributors may be used to endorse or promote products derived from
    32  // this software without specific prior written permission.
    33  //
    34  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    35  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    36  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    37  // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    38  // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    39  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    40  // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    41  // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    42  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    43  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    44  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    45  
    46  func serveProfile(w http.ResponseWriter, prof *profile.Profile) {
    47  	h := w.Header()
    48  	h.Set("X-Content-Type-Options", "nosniff")
    49  	h.Set("Content-Type", "application/octet-stream")
    50  	h.Set("Content-Disposition", `attachment; filename="profile"`)
    51  	if err := prof.Write(w); err != nil {
    52  		serveError(w, http.StatusInternalServerError, err.Error())
    53  	}
    54  }
    55  
    56  func serveError(w http.ResponseWriter, status int, txt string) {
    57  	h := w.Header()
    58  	h.Set("X-Content-Type-Options", "nosniff")
    59  	h.Set("X-Go-Pprof", "1")
    60  	h.Set("Content-Type", "text/plain; charset=utf-8")
    61  	h.Del("Content-Disposition")
    62  	w.WriteHeader(status)
    63  	fmt.Fprintln(w, txt)
    64  }
    65  
    66  type profileEntry struct {
    67  	Name    string
    68  	Href    string
    69  	Desc    string
    70  	Debug   int
    71  	Count   int
    72  	Handler http.Handler
    73  }
    74  
    75  func sortProfiles(entries []profileEntry) {
    76  	sort.Slice(entries, func(i, j int) bool {
    77  		return entries[i].Name < entries[j].Name
    78  	})
    79  }
    80  
    81  // Handler returns a http handler which responds with the pprof-formatted
    82  // profile named by the request. For example, "/debug/pprof/heap" serves the
    83  // "heap" profile.
    84  //
    85  // Handler responds to a request for "/debug/pprof/" with an HTML page listing
    86  // the available profiles.
    87  func Handler(sampleRate float64, profilers ...Profiler) http.Handler {
    88  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    89  		var guest, host []profileEntry
    90  
    91  		for _, p := range profilers {
    92  			guest = append(guest, profileEntry{
    93  				Name:    p.Name(),
    94  				Href:    p.Name(),
    95  				Desc:    p.Desc(),
    96  				Count:   p.Count(),
    97  				Handler: p.NewHandler(sampleRate),
    98  			})
    99  		}
   100  
   101  		// Add host profiling debug entries.
   102  		host = append(host, profileEntry{
   103  			Name:    "cmdline",
   104  			Href:    "cmdline",
   105  			Desc:    profileDescriptions["cmdline"],
   106  			Handler: http.HandlerFunc(httpprof.Cmdline),
   107  			Debug:   1,
   108  		})
   109  
   110  		host = append(host, profileEntry{
   111  			Name:    "profile",
   112  			Href:    "profile",
   113  			Desc:    profileDescriptions["profile"],
   114  			Handler: http.HandlerFunc(httpprof.Profile),
   115  			Debug:   1,
   116  		})
   117  
   118  		host = append(host, profileEntry{
   119  			Name:    "trace",
   120  			Href:    "trace",
   121  			Desc:    profileDescriptions["trace"],
   122  			Handler: http.HandlerFunc(httpprof.Trace),
   123  			Debug:   1,
   124  		})
   125  
   126  		for _, p := range pprof.Profiles() {
   127  			host = append(host, profileEntry{
   128  				Name:    p.Name(),
   129  				Href:    p.Name(),
   130  				Desc:    profileDescriptions[p.Name()],
   131  				Count:   p.Count(),
   132  				Handler: httpprof.Handler(p.Name()),
   133  				Debug:   1,
   134  			})
   135  		}
   136  
   137  		p := pprof.Lookup("goroutine")
   138  		host = append(host, profileEntry{
   139  			Name:    "full goroutine stack dump",
   140  			Href:    p.Name(),
   141  			Count:   p.Count(),
   142  			Handler: httpprof.Handler(p.Name()),
   143  			Debug:   2,
   144  		})
   145  
   146  		if href, found := strings.CutPrefix(r.URL.Path, "/debug/pprof/"); found {
   147  			var entries []profileEntry
   148  			_, queryHost := r.URL.Query()["host"]
   149  			if queryHost {
   150  				entries = host
   151  			} else {
   152  				entries = guest
   153  			}
   154  			for _, entry := range entries {
   155  				if entry.Href == href {
   156  					entry.Handler.ServeHTTP(w, r)
   157  					return
   158  				}
   159  			}
   160  		}
   161  
   162  		sortProfiles(guest)
   163  		sortProfiles(host)
   164  
   165  		h := w.Header()
   166  		h.Set("X-Content-Type-Options", "nosniff")
   167  		h.Set("Content-Type", "text/html; charset=utf-8")
   168  
   169  		if err := indexTmplExecute(w, guest, host); err != nil {
   170  			serveError(w, http.StatusInternalServerError, err.Error())
   171  		}
   172  	})
   173  }
   174  
   175  func indexTmplExecute(w io.Writer, guest, host []profileEntry) error {
   176  	var b bytes.Buffer
   177  	b.WriteString(`<html>
   178  <head>
   179  <title>/debug/pprof</title>
   180  <style>
   181  .profile-name{
   182  	display:inline-block;
   183  	width:6rem;
   184  }
   185  </style>
   186  </head>
   187  <body>
   188  /debug/pprof
   189  <br>
   190  <p>Set debug=1 as a query parameter to export in legacy text format (host only)</p>
   191  <br>
   192  Types of profiles available:
   193  <table>
   194  <thead><td>Count</td><td>Profile (guest)</td></thead>
   195  `)
   196  
   197  	for _, profile := range guest {
   198  		link := &url.URL{Path: profile.Href}
   199  		name := profile.Name
   200  		fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(name))
   201  	}
   202  
   203  	b.WriteString(`</table>
   204  <table>
   205  <thead><td>Count</td><td>Profile (host)</td></thead>
   206  `)
   207  
   208  	for _, profile := range host {
   209  		link := &url.URL{Path: profile.Href, RawQuery: fmt.Sprintf("host&debug=%d", profile.Debug)}
   210  		name := profile.Name
   211  		fmt.Fprintf(&b, "<tr><td>%d</td><td><a href='%s'>%s</a></td></tr>\n", profile.Count, link, html.EscapeString(name))
   212  	}
   213  
   214  	b.WriteString(`</table>
   215  	<br>
   216  	<p>
   217  	Profile Descriptions:
   218  	<ul>
   219  	`)
   220  
   221  	descriptionsByName := make(map[string]string, len(guest)+len(host))
   222  	for _, profiles := range [][]profileEntry{guest, host} {
   223  		for _, profile := range profiles {
   224  			if profile.Desc != "" {
   225  				descriptionsByName[profile.Name] = profile.Desc
   226  			}
   227  		}
   228  	}
   229  	descriptions := make([][2]string, 0, len(descriptionsByName))
   230  	for name, desc := range descriptionsByName {
   231  		descriptions = append(descriptions, [2]string{name, desc})
   232  	}
   233  	sort.Slice(descriptions, func(i, j int) bool {
   234  		return descriptions[i][0] < descriptions[j][0]
   235  	})
   236  	for _, desc := range descriptions {
   237  		fmt.Fprintf(&b, "<li><div class=profile-name>%s: </div> %s</li>\n", html.EscapeString(desc[0]), html.EscapeString(desc[1]))
   238  	}
   239  	b.WriteString(`</ul>
   240  	</p>
   241  	</body>
   242  	</html>`)
   243  
   244  	_, err := w.Write(b.Bytes())
   245  	return err
   246  }
   247  
   248  var profileDescriptions = map[string]string{
   249  	"allocs":       "A sampling of all past memory allocations",
   250  	"block":        "Stack traces that led to blocking on synchronization primitives",
   251  	"cmdline":      "The command line invocation of the current program",
   252  	"goroutine":    "Stack traces of all current goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic.",
   253  	"heap":         "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
   254  	"mutex":        "Stack traces of holders of contended mutexes",
   255  	"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.",
   256  	"threadcreate": "Stack traces that led to the creation of new OS threads",
   257  	"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.",
   258  }