blob: 4ae1b289d82e1561b5a715b02fc17848e4532b36 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package pproflib defines a client-side proxy and server-side implementation
// of the v.io/v23/services/pprof interface.
//
// It is functionally equivalent to http://golang.org/pkg/net/http/pprof/,
// except that the data comes from a remote vanadium server, and the handlers
// are not registered in DefaultServeMux.
package pproflib
import (
"bufio"
"fmt"
"html/template"
"io"
"net/http"
"strconv"
"strings"
"v.io/v23/context"
"v.io/v23/services/pprof"
"v.io/v23/vtrace"
)
// PprofProxy returns an http.Handler implements to serve profile information
// of a remote process with the vanadium object name 'name'.
//
// The handler assumes that it is serving paths under "pathPrefix".
func PprofProxy(ctx *context.T, pathPrefix, name string) http.Handler {
return &proxy{
ctx: ctx,
name: name,
pathPrefix: pathPrefix,
}
}
type proxy struct {
ctx *context.T
name string
pathPrefix string
}
func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch path := p.canonicalPath(r); path {
case "/":
http.Redirect(w, r, r.URL.Path+"/pprof/", http.StatusTemporaryRedirect)
case "/pprof/cmdline":
p.cmdLine(w, r)
case "/pprof/profile":
p.profile(w, r)
case "/pprof/symbol":
p.symbol(w, r)
default:
if strings.HasPrefix(path, "/pprof/") || path == "/pprof" {
p.index(w, r)
} else {
http.NotFound(w, r)
}
}
}
func (p *proxy) canonicalPath(r *http.Request) string {
return strings.TrimPrefix(r.URL.Path, p.pathPrefix)
}
func replyUnavailable(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, "%v", err)
}
// index serves an html page for pprof/ and, indirectly, raw data for pprof/*
func (p *proxy) index(w http.ResponseWriter, r *http.Request) {
if path := p.canonicalPath(r); strings.HasPrefix(path, "/pprof/") {
name := strings.TrimPrefix(path, "/pprof/")
if name != "" {
p.sendProfile(name, w, r)
return
}
}
c := pprof.PProfClient(p.name)
ctx, _ := vtrace.WithNewTrace(p.ctx)
profiles, err := c.Profiles(ctx)
if err != nil {
replyUnavailable(w, err)
return
}
if err := indexTmpl.Execute(w, profiles); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "%v", err)
return
}
}
// sendProfile sends the requested profile as a response.
func (p *proxy) sendProfile(name string, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
debug, _ := strconv.Atoi(r.FormValue("debug"))
c := pprof.PProfClient(p.name)
ctx, _ := vtrace.WithNewTrace(p.ctx)
prof, err := c.Profile(ctx, name, int32(debug))
if err != nil {
replyUnavailable(w, err)
return
}
iterator := prof.RecvStream()
for iterator.Advance() {
_, err := w.Write(iterator.Value())
if err != nil {
return
}
}
if err := iterator.Err(); err != nil {
replyUnavailable(w, err)
return
}
if err := prof.Finish(); err != nil {
replyUnavailable(w, err)
return
}
}
// profile replies with a CPU profile.
func (p *proxy) profile(w http.ResponseWriter, r *http.Request) {
sec, _ := strconv.ParseUint(r.FormValue("seconds"), 10, 64)
if sec == 0 {
sec = 30
}
w.Header().Set("Content-Type", "application/octet-stream")
c := pprof.PProfClient(p.name)
ctx, _ := vtrace.WithNewTrace(p.ctx)
prof, err := c.CpuProfile(ctx, int32(sec))
if err != nil {
replyUnavailable(w, err)
return
}
iterator := prof.RecvStream()
for iterator.Advance() {
_, err := w.Write(iterator.Value())
if err != nil {
return
}
}
if err := iterator.Err(); err != nil {
replyUnavailable(w, err)
return
}
if err := prof.Finish(); err != nil {
replyUnavailable(w, err)
return
}
}
// cmdLine replies with the command-line arguments of the process.
func (p *proxy) cmdLine(w http.ResponseWriter, r *http.Request) {
c := pprof.PProfClient(p.name)
ctx, _ := vtrace.WithNewTrace(p.ctx)
cmdline, err := c.CmdLine(ctx)
if err != nil {
replyUnavailable(w, err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, strings.Join(cmdline, "\x00"))
}
// symbol replies with a map of program counters to object name.
func (p *proxy) symbol(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
var b *bufio.Reader
if r.Method == "POST" {
b = bufio.NewReader(r.Body)
} else {
b = bufio.NewReader(strings.NewReader(r.URL.RawQuery))
}
pcList := []uint64{}
for {
word, err := b.ReadSlice('+')
if err == nil {
word = word[0 : len(word)-1] // trim +
}
pc, _ := strconv.ParseUint(string(word), 0, 64)
if pc != 0 {
pcList = append(pcList, pc)
}
if err != nil {
if err != io.EOF {
replyUnavailable(w, err)
return
}
break
}
}
c := pprof.PProfClient(p.name)
ctx, _ := vtrace.WithNewTrace(p.ctx)
pcMap, err := c.Symbol(ctx, pcList)
if err != nil {
replyUnavailable(w, err)
return
}
if len(pcMap) != len(pcList) {
replyUnavailable(w, fmt.Errorf("received the wrong number of results. Got %d, want %d", len(pcMap), len(pcList)))
return
}
// The pprof tool always wants num_symbols to be non-zero, even if no
// symbols are returned.. The actual value doesn't matter.
fmt.Fprintf(w, "num_symbols: 1\n")
for i, v := range pcMap {
if len(v) > 0 {
fmt.Fprintf(w, "%#x %s\n", pcList[i], v)
}
}
}
var indexTmpl = template.Must(template.New("index").Parse(`<html>
<head>
<title>/pprof/</title>
</head>
/pprof/<br>
<br>
<body>
profiles:<br>
<table>
{{range .}}<tr><td align=right><td><a href="/pprof/{{.}}?debug=1">{{.}}</a>
{{end}}
</table>
<br>
<a href="/pprof/goroutine?debug=2">full goroutine stack dump</a><br>
</body>
</html>
`))