...

Source file src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go

     1	// Copyright 2017 Google Inc. All Rights Reserved.
     2	//
     3	// Licensed under the Apache License, Version 2.0 (the "License");
     4	// you may not use this file except in compliance with the License.
     5	// You may obtain a copy of the License at
     6	//
     7	//     http://www.apache.org/licenses/LICENSE-2.0
     8	//
     9	// Unless required by applicable law or agreed to in writing, software
    10	// distributed under the License is distributed on an "AS IS" BASIS,
    11	// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12	// See the License for the specific language governing permissions and
    13	// limitations under the License.
    14	
    15	package driver
    16	
    17	import (
    18		"bytes"
    19		"fmt"
    20		"html/template"
    21		"net"
    22		"net/http"
    23		gourl "net/url"
    24		"os"
    25		"os/exec"
    26		"strconv"
    27		"strings"
    28		"time"
    29	
    30		"github.com/google/pprof/internal/graph"
    31		"github.com/google/pprof/internal/plugin"
    32		"github.com/google/pprof/internal/report"
    33		"github.com/google/pprof/profile"
    34	)
    35	
    36	// webInterface holds the state needed for serving a browser based interface.
    37	type webInterface struct {
    38		prof      *profile.Profile
    39		options   *plugin.Options
    40		help      map[string]string
    41		templates *template.Template
    42	}
    43	
    44	func makeWebInterface(p *profile.Profile, opt *plugin.Options) *webInterface {
    45		templates := template.New("templategroup")
    46		addTemplates(templates)
    47		report.AddSourceTemplates(templates)
    48		return &webInterface{
    49			prof:      p,
    50			options:   opt,
    51			help:      make(map[string]string),
    52			templates: templates,
    53		}
    54	}
    55	
    56	// maxEntries is the maximum number of entries to print for text interfaces.
    57	const maxEntries = 50
    58	
    59	// errorCatcher is a UI that captures errors for reporting to the browser.
    60	type errorCatcher struct {
    61		plugin.UI
    62		errors []string
    63	}
    64	
    65	func (ec *errorCatcher) PrintErr(args ...interface{}) {
    66		ec.errors = append(ec.errors, strings.TrimSuffix(fmt.Sprintln(args...), "\n"))
    67		ec.UI.PrintErr(args...)
    68	}
    69	
    70	// webArgs contains arguments passed to templates in webhtml.go.
    71	type webArgs struct {
    72		Title       string
    73		Errors      []string
    74		Total       int64
    75		SampleTypes []string
    76		Legend      []string
    77		Help        map[string]string
    78		Nodes       []string
    79		HTMLBody    template.HTML
    80		TextBody    string
    81		Top         []report.TextItem
    82		FlameGraph  template.JS
    83	}
    84	
    85	func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error {
    86		host, port, err := getHostAndPort(hostport)
    87		if err != nil {
    88			return err
    89		}
    90		interactiveMode = true
    91		ui := makeWebInterface(p, o)
    92		for n, c := range pprofCommands {
    93			ui.help[n] = c.description
    94		}
    95		for n, v := range pprofVariables {
    96			ui.help[n] = v.help
    97		}
    98		ui.help["details"] = "Show information about the profile and this view"
    99		ui.help["graph"] = "Display profile as a directed graph"
   100		ui.help["reset"] = "Show the entire profile"
   101	
   102		server := o.HTTPServer
   103		if server == nil {
   104			server = defaultWebServer
   105		}
   106		args := &plugin.HTTPServerArgs{
   107			Hostport: net.JoinHostPort(host, strconv.Itoa(port)),
   108			Host:     host,
   109			Port:     port,
   110			Handlers: map[string]http.Handler{
   111				"/":           http.HandlerFunc(ui.dot),
   112				"/top":        http.HandlerFunc(ui.top),
   113				"/disasm":     http.HandlerFunc(ui.disasm),
   114				"/source":     http.HandlerFunc(ui.source),
   115				"/peek":       http.HandlerFunc(ui.peek),
   116				"/flamegraph": http.HandlerFunc(ui.flamegraph),
   117			},
   118		}
   119	
   120		url := "http://" + args.Hostport
   121	
   122		o.UI.Print("Serving web UI on ", url)
   123	
   124		if o.UI.WantBrowser() && !disableBrowser {
   125			go openBrowser(url, o)
   126		}
   127		return server(args)
   128	}
   129	
   130	func getHostAndPort(hostport string) (string, int, error) {
   131		host, portStr, err := net.SplitHostPort(hostport)
   132		if err != nil {
   133			return "", 0, fmt.Errorf("could not split http address: %v", err)
   134		}
   135		if host == "" {
   136			host = "localhost"
   137		}
   138		var port int
   139		if portStr == "" {
   140			ln, err := net.Listen("tcp", net.JoinHostPort(host, "0"))
   141			if err != nil {
   142				return "", 0, fmt.Errorf("could not generate random port: %v", err)
   143			}
   144			port = ln.Addr().(*net.TCPAddr).Port
   145			err = ln.Close()
   146			if err != nil {
   147				return "", 0, fmt.Errorf("could not generate random port: %v", err)
   148			}
   149		} else {
   150			port, err = strconv.Atoi(portStr)
   151			if err != nil {
   152				return "", 0, fmt.Errorf("invalid port number: %v", err)
   153			}
   154		}
   155		return host, port, nil
   156	}
   157	func defaultWebServer(args *plugin.HTTPServerArgs) error {
   158		ln, err := net.Listen("tcp", args.Hostport)
   159		if err != nil {
   160			return err
   161		}
   162		isLocal := isLocalhost(args.Host)
   163		handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   164			if isLocal {
   165				// Only allow local clients
   166				host, _, err := net.SplitHostPort(req.RemoteAddr)
   167				if err != nil || !isLocalhost(host) {
   168					http.Error(w, "permission denied", http.StatusForbidden)
   169					return
   170				}
   171			}
   172			h := args.Handlers[req.URL.Path]
   173			if h == nil {
   174				// Fall back to default behavior
   175				h = http.DefaultServeMux
   176			}
   177			h.ServeHTTP(w, req)
   178		})
   179	
   180		// We serve the ui at /ui/ and redirect there from the root. This is done
   181		// to surface any problems with serving the ui at a non-root early. See:
   182		//
   183		// https://github.com/google/pprof/pull/348
   184		mux := http.NewServeMux()
   185		mux.Handle("/ui/", http.StripPrefix("/ui", handler))
   186		mux.Handle("/", redirectWithQuery("/ui"))
   187		s := &http.Server{Handler: mux}
   188		return s.Serve(ln)
   189	}
   190	
   191	func redirectWithQuery(path string) http.HandlerFunc {
   192		return func(w http.ResponseWriter, r *http.Request) {
   193			pathWithQuery := &gourl.URL{Path: path, RawQuery: r.URL.RawQuery}
   194			http.Redirect(w, r, pathWithQuery.String(), http.StatusTemporaryRedirect)
   195		}
   196	}
   197	
   198	func isLocalhost(host string) bool {
   199		for _, v := range []string{"localhost", "127.0.0.1", "[::1]", "::1"} {
   200			if host == v {
   201				return true
   202			}
   203		}
   204		return false
   205	}
   206	
   207	func openBrowser(url string, o *plugin.Options) {
   208		// Construct URL.
   209		u, _ := gourl.Parse(url)
   210		q := u.Query()
   211		for _, p := range []struct{ param, key string }{
   212			{"f", "focus"},
   213			{"s", "show"},
   214			{"sf", "show_from"},
   215			{"i", "ignore"},
   216			{"h", "hide"},
   217			{"si", "sample_index"},
   218		} {
   219			if v := pprofVariables[p.key].value; v != "" {
   220				q.Set(p.param, v)
   221			}
   222		}
   223		u.RawQuery = q.Encode()
   224	
   225		// Give server a little time to get ready.
   226		time.Sleep(time.Millisecond * 500)
   227	
   228		for _, b := range browsers() {
   229			args := strings.Split(b, " ")
   230			if len(args) == 0 {
   231				continue
   232			}
   233			viewer := exec.Command(args[0], append(args[1:], u.String())...)
   234			viewer.Stderr = os.Stderr
   235			if err := viewer.Start(); err == nil {
   236				return
   237			}
   238		}
   239		// No visualizer succeeded, so just print URL.
   240		o.UI.PrintErr(u.String())
   241	}
   242	
   243	func varsFromURL(u *gourl.URL) variables {
   244		vars := pprofVariables.makeCopy()
   245		vars["focus"].value = u.Query().Get("f")
   246		vars["show"].value = u.Query().Get("s")
   247		vars["show_from"].value = u.Query().Get("sf")
   248		vars["ignore"].value = u.Query().Get("i")
   249		vars["hide"].value = u.Query().Get("h")
   250		vars["sample_index"].value = u.Query().Get("si")
   251		return vars
   252	}
   253	
   254	// makeReport generates a report for the specified command.
   255	func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
   256		cmd []string, vars ...string) (*report.Report, []string) {
   257		v := varsFromURL(req.URL)
   258		for i := 0; i+1 < len(vars); i += 2 {
   259			v[vars[i]].value = vars[i+1]
   260		}
   261		catcher := &errorCatcher{UI: ui.options.UI}
   262		options := *ui.options
   263		options.UI = catcher
   264		_, rpt, err := generateRawReport(ui.prof, cmd, v, &options)
   265		if err != nil {
   266			http.Error(w, err.Error(), http.StatusBadRequest)
   267			ui.options.UI.PrintErr(err)
   268			return nil, nil
   269		}
   270		return rpt, catcher.errors
   271	}
   272	
   273	// render generates html using the named template based on the contents of data.
   274	func (ui *webInterface) render(w http.ResponseWriter, tmpl string,
   275		rpt *report.Report, errList, legend []string, data webArgs) {
   276		file := getFromLegend(legend, "File: ", "unknown")
   277		profile := getFromLegend(legend, "Type: ", "unknown")
   278		data.Title = file + " " + profile
   279		data.Errors = errList
   280		data.Total = rpt.Total()
   281		data.SampleTypes = sampleTypes(ui.prof)
   282		data.Legend = legend
   283		data.Help = ui.help
   284		html := &bytes.Buffer{}
   285		if err := ui.templates.ExecuteTemplate(html, tmpl, data); err != nil {
   286			http.Error(w, "internal template error", http.StatusInternalServerError)
   287			ui.options.UI.PrintErr(err)
   288			return
   289		}
   290		w.Header().Set("Content-Type", "text/html")
   291		w.Write(html.Bytes())
   292	}
   293	
   294	// dot generates a web page containing an svg diagram.
   295	func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
   296		rpt, errList := ui.makeReport(w, req, []string{"svg"})
   297		if rpt == nil {
   298			return // error already reported
   299		}
   300	
   301		// Generate dot graph.
   302		g, config := report.GetDOT(rpt)
   303		legend := config.Labels
   304		config.Labels = nil
   305		dot := &bytes.Buffer{}
   306		graph.ComposeDot(dot, g, &graph.DotAttributes{}, config)
   307	
   308		// Convert to svg.
   309		svg, err := dotToSvg(dot.Bytes())
   310		if err != nil {
   311			http.Error(w, "Could not execute dot; may need to install graphviz.",
   312				http.StatusNotImplemented)
   313			ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err)
   314			return
   315		}
   316	
   317		// Get all node names into an array.
   318		nodes := []string{""} // dot starts with node numbered 1
   319		for _, n := range g.Nodes {
   320			nodes = append(nodes, n.Info.Name)
   321		}
   322	
   323		ui.render(w, "graph", rpt, errList, legend, webArgs{
   324			HTMLBody: template.HTML(string(svg)),
   325			Nodes:    nodes,
   326		})
   327	}
   328	
   329	func dotToSvg(dot []byte) ([]byte, error) {
   330		cmd := exec.Command("dot", "-Tsvg")
   331		out := &bytes.Buffer{}
   332		cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr
   333		if err := cmd.Run(); err != nil {
   334			return nil, err
   335		}
   336	
   337		// Fix dot bug related to unquoted amperands.
   338		svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&amp;;"), -1)
   339	
   340		// Cleanup for embedding by dropping stuff before the <svg> start.
   341		if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 {
   342			svg = svg[pos:]
   343		}
   344		return svg, nil
   345	}
   346	
   347	func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
   348		rpt, errList := ui.makeReport(w, req, []string{"top"}, "nodecount", "500")
   349		if rpt == nil {
   350			return // error already reported
   351		}
   352		top, legend := report.TextItems(rpt)
   353		var nodes []string
   354		for _, item := range top {
   355			nodes = append(nodes, item.Name)
   356		}
   357	
   358		ui.render(w, "top", rpt, errList, legend, webArgs{
   359			Top:   top,
   360			Nodes: nodes,
   361		})
   362	}
   363	
   364	// disasm generates a web page containing disassembly.
   365	func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
   366		args := []string{"disasm", req.URL.Query().Get("f")}
   367		rpt, errList := ui.makeReport(w, req, args)
   368		if rpt == nil {
   369			return // error already reported
   370		}
   371	
   372		out := &bytes.Buffer{}
   373		if err := report.PrintAssembly(out, rpt, ui.options.Obj, maxEntries); err != nil {
   374			http.Error(w, err.Error(), http.StatusBadRequest)
   375			ui.options.UI.PrintErr(err)
   376			return
   377		}
   378	
   379		legend := report.ProfileLabels(rpt)
   380		ui.render(w, "plaintext", rpt, errList, legend, webArgs{
   381			TextBody: out.String(),
   382		})
   383	
   384	}
   385	
   386	// source generates a web page containing source code annotated with profile
   387	// data.
   388	func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
   389		args := []string{"weblist", req.URL.Query().Get("f")}
   390		rpt, errList := ui.makeReport(w, req, args)
   391		if rpt == nil {
   392			return // error already reported
   393		}
   394	
   395		// Generate source listing.
   396		var body bytes.Buffer
   397		if err := report.PrintWebList(&body, rpt, ui.options.Obj, maxEntries); err != nil {
   398			http.Error(w, err.Error(), http.StatusBadRequest)
   399			ui.options.UI.PrintErr(err)
   400			return
   401		}
   402	
   403		legend := report.ProfileLabels(rpt)
   404		ui.render(w, "sourcelisting", rpt, errList, legend, webArgs{
   405			HTMLBody: template.HTML(body.String()),
   406		})
   407	}
   408	
   409	// peek generates a web page listing callers/callers.
   410	func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
   411		args := []string{"peek", req.URL.Query().Get("f")}
   412		rpt, errList := ui.makeReport(w, req, args, "lines", "t")
   413		if rpt == nil {
   414			return // error already reported
   415		}
   416	
   417		out := &bytes.Buffer{}
   418		if err := report.Generate(out, rpt, ui.options.Obj); err != nil {
   419			http.Error(w, err.Error(), http.StatusBadRequest)
   420			ui.options.UI.PrintErr(err)
   421			return
   422		}
   423	
   424		legend := report.ProfileLabels(rpt)
   425		ui.render(w, "plaintext", rpt, errList, legend, webArgs{
   426			TextBody: out.String(),
   427		})
   428	}
   429	
   430	// getFromLegend returns the suffix of an entry in legend that starts
   431	// with param.  It returns def if no such entry is found.
   432	func getFromLegend(legend []string, param, def string) string {
   433		for _, s := range legend {
   434			if strings.HasPrefix(s, param) {
   435				return s[len(param):]
   436			}
   437		}
   438		return def
   439	}
   440	

View as plain text