| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | package driver |
| |
|
| | import ( |
| | "bytes" |
| | "fmt" |
| | "html/template" |
| | "io" |
| | "maps" |
| | "net" |
| | "net/http" |
| | gourl "net/url" |
| | "os" |
| | "os/exec" |
| | "slices" |
| | "strconv" |
| | "strings" |
| | "time" |
| |
|
| | "github.com/google/pprof/internal/graph" |
| | "github.com/google/pprof/internal/measurement" |
| | "github.com/google/pprof/internal/plugin" |
| | "github.com/google/pprof/internal/report" |
| | "github.com/google/pprof/profile" |
| | ) |
| |
|
| | |
| | type webInterface struct { |
| | prof *profile.Profile |
| | copier profileCopier |
| | options *plugin.Options |
| | help map[string]string |
| | settingsFile string |
| | } |
| |
|
| | func makeWebInterface(p *profile.Profile, copier profileCopier, opt *plugin.Options) (*webInterface, error) { |
| | settingsFile, err := settingsFileName() |
| | if err != nil { |
| | return nil, err |
| | } |
| | return &webInterface{ |
| | prof: p, |
| | copier: copier, |
| | options: opt, |
| | help: make(map[string]string), |
| | settingsFile: settingsFile, |
| | }, nil |
| | } |
| |
|
| | |
| | const maxEntries = 50 |
| |
|
| | |
| | type errorCatcher struct { |
| | plugin.UI |
| | errors []string |
| | } |
| |
|
| | func (ec *errorCatcher) PrintErr(args ...interface{}) { |
| | ec.errors = append(ec.errors, strings.TrimSuffix(fmt.Sprintln(args...), "\n")) |
| | ec.UI.PrintErr(args...) |
| | } |
| |
|
| | |
| | type webArgs struct { |
| | Title string |
| | Errors []string |
| | Total int64 |
| | SampleTypes []string |
| | Legend []string |
| | DocURL string |
| | Standalone bool |
| | Help map[string]string |
| | Nodes []string |
| | HTMLBody template.HTML |
| | TextBody string |
| | Top []report.TextItem |
| | Listing report.WebListData |
| | FlameGraph template.JS |
| | Stacks template.JS |
| | Configs []configMenuEntry |
| | UnitDefs []measurement.UnitType |
| | } |
| |
|
| | func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error { |
| | host, port, err := getHostAndPort(hostport) |
| | if err != nil { |
| | return err |
| | } |
| | interactiveMode = true |
| | copier := makeProfileCopier(p) |
| | ui, err := makeWebInterface(p, copier, o) |
| | if err != nil { |
| | return err |
| | } |
| | for n, c := range pprofCommands { |
| | ui.help[n] = c.description |
| | } |
| | maps.Copy(ui.help, configHelp) |
| | ui.help["details"] = "Show information about the profile and this view" |
| | ui.help["graph"] = "Display profile as a directed graph" |
| | ui.help["flamegraph"] = "Display profile as a flame graph" |
| | ui.help["reset"] = "Show the entire profile" |
| | ui.help["save_config"] = "Save current settings" |
| |
|
| | server := o.HTTPServer |
| | if server == nil { |
| | server = defaultWebServer |
| | } |
| | args := &plugin.HTTPServerArgs{ |
| | Hostport: net.JoinHostPort(host, strconv.Itoa(port)), |
| | Host: host, |
| | Port: port, |
| | Handlers: map[string]http.Handler{ |
| | "/": redirectWithQuery("flamegraph", http.StatusMovedPermanently), |
| | "/graph": http.HandlerFunc(ui.dot), |
| | "/top": http.HandlerFunc(ui.top), |
| | "/disasm": http.HandlerFunc(ui.disasm), |
| | "/source": http.HandlerFunc(ui.source), |
| | "/peek": http.HandlerFunc(ui.peek), |
| | "/flamegraph": http.HandlerFunc(ui.stackView), |
| | "/saveconfig": http.HandlerFunc(ui.saveConfig), |
| | "/deleteconfig": http.HandlerFunc(ui.deleteConfig), |
| | "/download": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| | w.Header().Set("Content-Type", "application/vnd.google.protobuf+gzip") |
| | w.Header().Set("Content-Disposition", "attachment;filename=profile.pb.gz") |
| | p.Write(w) |
| | }), |
| | |
| | "/flamegraph2": redirectWithQuery("flamegraph", http.StatusMovedPermanently), |
| | "/flamegraphold": redirectWithQuery("flamegraph", http.StatusMovedPermanently), |
| | }, |
| | } |
| |
|
| | url := "http://" + args.Hostport |
| |
|
| | o.UI.Print("Serving web UI on ", url) |
| |
|
| | if o.UI.WantBrowser() && !disableBrowser { |
| | go openBrowser(url, o) |
| | } |
| | return server(args) |
| | } |
| |
|
| | func getHostAndPort(hostport string) (string, int, error) { |
| | host, portStr, err := net.SplitHostPort(hostport) |
| | if err != nil { |
| | return "", 0, fmt.Errorf("could not split http address: %v", err) |
| | } |
| | if host == "" { |
| | host = "localhost" |
| | } |
| | var port int |
| | if portStr == "" { |
| | ln, err := net.Listen("tcp", net.JoinHostPort(host, "0")) |
| | if err != nil { |
| | return "", 0, fmt.Errorf("could not generate random port: %v", err) |
| | } |
| | port = ln.Addr().(*net.TCPAddr).Port |
| | err = ln.Close() |
| | if err != nil { |
| | return "", 0, fmt.Errorf("could not generate random port: %v", err) |
| | } |
| | } else { |
| | port, err = strconv.Atoi(portStr) |
| | if err != nil { |
| | return "", 0, fmt.Errorf("invalid port number: %v", err) |
| | } |
| | } |
| | return host, port, nil |
| | } |
| | func defaultWebServer(args *plugin.HTTPServerArgs) error { |
| | ln, err := net.Listen("tcp", args.Hostport) |
| | if err != nil { |
| | return err |
| | } |
| | isLocal := isLocalhost(args.Host) |
| | handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| | if isLocal { |
| | |
| | host, _, err := net.SplitHostPort(req.RemoteAddr) |
| | if err != nil || !isLocalhost(host) { |
| | http.Error(w, "permission denied", http.StatusForbidden) |
| | return |
| | } |
| | } |
| | h := args.Handlers[req.URL.Path] |
| | if h == nil { |
| | |
| | h = http.DefaultServeMux |
| | } |
| | h.ServeHTTP(w, req) |
| | }) |
| |
|
| | |
| | |
| | |
| | |
| | mux := http.NewServeMux() |
| | mux.Handle("/ui/", http.StripPrefix("/ui", handler)) |
| | mux.Handle("/", redirectWithQuery("/ui", http.StatusTemporaryRedirect)) |
| | s := &http.Server{Handler: mux} |
| | return s.Serve(ln) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func redirectWithQuery(path string, code int) http.HandlerFunc { |
| | return func(w http.ResponseWriter, r *http.Request) { |
| | pathWithQuery := &gourl.URL{Path: path, RawQuery: r.URL.RawQuery} |
| | w.Header().Set("Location", pathWithQuery.String()) |
| | w.WriteHeader(code) |
| | } |
| | } |
| |
|
| | func isLocalhost(host string) bool { |
| | return slices.Contains([]string{"localhost", "127.0.0.1", "[::1]", "::1"}, host) |
| | } |
| |
|
| | func openBrowser(url string, o *plugin.Options) { |
| | |
| | baseURL, _ := gourl.Parse(url) |
| | current := currentConfig() |
| | u, _ := current.makeURL(*baseURL) |
| |
|
| | |
| | time.Sleep(time.Millisecond * 500) |
| |
|
| | for _, b := range browsers() { |
| | args := strings.Split(b, " ") |
| | if len(args) == 0 { |
| | continue |
| | } |
| | viewer := exec.Command(args[0], append(args[1:], u.String())...) |
| | viewer.Stderr = os.Stderr |
| | if err := viewer.Start(); err == nil { |
| | return |
| | } |
| | } |
| | |
| | o.UI.PrintErr(u.String()) |
| | } |
| |
|
| | |
| | |
| | func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request, |
| | cmd []string, configEditor func(*config)) (*report.Report, []string) { |
| | cfg := currentConfig() |
| | if err := cfg.applyURL(req.URL.Query()); err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return nil, nil |
| | } |
| | if configEditor != nil { |
| | configEditor(&cfg) |
| | } |
| | catcher := &errorCatcher{UI: ui.options.UI} |
| | options := *ui.options |
| | options.UI = catcher |
| | _, rpt, err := generateRawReport(ui.copier.newCopy(), cmd, cfg, &options) |
| | if err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return nil, nil |
| | } |
| | return rpt, catcher.errors |
| | } |
| |
|
| | |
| | func renderHTML(dst io.Writer, tmpl string, rpt *report.Report, errList, legend []string, data webArgs) error { |
| | file := getFromLegend(legend, "File: ", "unknown") |
| | profile := getFromLegend(legend, "Type: ", "unknown") |
| | data.Title = file + " " + profile |
| | data.Errors = errList |
| | data.Total = rpt.Total() |
| | data.DocURL = rpt.DocURL() |
| | data.Legend = legend |
| | return getHTMLTemplates().ExecuteTemplate(dst, tmpl, data) |
| | } |
| |
|
| | |
| | func (ui *webInterface) render(w http.ResponseWriter, req *http.Request, tmpl string, |
| | rpt *report.Report, errList, legend []string, data webArgs) { |
| | data.SampleTypes = sampleTypes(ui.prof) |
| | data.Help = ui.help |
| | data.Configs = configMenu(ui.settingsFile, *req.URL) |
| | html := &bytes.Buffer{} |
| | if err := renderHTML(html, tmpl, rpt, errList, legend, data); err != nil { |
| | http.Error(w, "internal template error", http.StatusInternalServerError) |
| | ui.options.UI.PrintErr(err) |
| | return |
| | } |
| | w.Header().Set("Content-Type", "text/html") |
| | w.Write(html.Bytes()) |
| | } |
| |
|
| | |
| | func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) { |
| | rpt, errList := ui.makeReport(w, req, []string{"svg"}, nil) |
| | if rpt == nil { |
| | return |
| | } |
| |
|
| | |
| | g, config := report.GetDOT(rpt) |
| | legend := config.Labels |
| | config.Labels = nil |
| | dot := &bytes.Buffer{} |
| | graph.ComposeDot(dot, g, &graph.DotAttributes{}, config) |
| |
|
| | |
| | svg, err := dotToSvg(dot.Bytes()) |
| | if err != nil { |
| | http.Error(w, "Could not execute dot; may need to install graphviz.", |
| | http.StatusNotImplemented) |
| | ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err) |
| | return |
| | } |
| |
|
| | |
| | nodes := []string{""} |
| | for _, n := range g.Nodes { |
| | nodes = append(nodes, n.Info.Name) |
| | } |
| |
|
| | ui.render(w, req, "graph", rpt, errList, legend, webArgs{ |
| | HTMLBody: template.HTML(string(svg)), |
| | Nodes: nodes, |
| | }) |
| | } |
| |
|
| | func dotToSvg(dot []byte) ([]byte, error) { |
| | cmd := exec.Command("dot", "-Tsvg") |
| | out := &bytes.Buffer{} |
| | cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr |
| | if err := cmd.Run(); err != nil { |
| | return nil, err |
| | } |
| |
|
| | |
| | svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&;"), -1) |
| |
|
| | |
| | if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 { |
| | svg = svg[pos:] |
| | } |
| | return svg, nil |
| | } |
| |
|
| | func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) { |
| | rpt, errList := ui.makeReport(w, req, []string{"top"}, func(cfg *config) { |
| | cfg.NodeCount = 500 |
| | }) |
| | if rpt == nil { |
| | return |
| | } |
| | top, legend := report.TextItems(rpt) |
| | var nodes []string |
| | for _, item := range top { |
| | nodes = append(nodes, item.Name) |
| | } |
| |
|
| | ui.render(w, req, "top", rpt, errList, legend, webArgs{ |
| | Top: top, |
| | Nodes: nodes, |
| | }) |
| | } |
| |
|
| | |
| | func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) { |
| | args := []string{"disasm", req.URL.Query().Get("f")} |
| | rpt, errList := ui.makeReport(w, req, args, nil) |
| | if rpt == nil { |
| | return |
| | } |
| |
|
| | out := &bytes.Buffer{} |
| | if err := report.PrintAssembly(out, rpt, ui.options.Obj, maxEntries); err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return |
| | } |
| |
|
| | legend := report.ProfileLabels(rpt) |
| | ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{ |
| | TextBody: out.String(), |
| | }) |
| |
|
| | } |
| |
|
| | |
| | |
| | func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) { |
| | args := []string{"weblist", req.URL.Query().Get("f")} |
| | rpt, errList := ui.makeReport(w, req, args, nil) |
| | if rpt == nil { |
| | return |
| | } |
| |
|
| | |
| | listing, err := report.MakeWebList(rpt, ui.options.Obj, maxEntries) |
| | if err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return |
| | } |
| |
|
| | legend := report.ProfileLabels(rpt) |
| | ui.render(w, req, "sourcelisting", rpt, errList, legend, webArgs{ |
| | Listing: listing, |
| | }) |
| | } |
| |
|
| | |
| | func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) { |
| | args := []string{"peek", req.URL.Query().Get("f")} |
| | rpt, errList := ui.makeReport(w, req, args, func(cfg *config) { |
| | cfg.Granularity = "lines" |
| | }) |
| | if rpt == nil { |
| | return |
| | } |
| |
|
| | out := &bytes.Buffer{} |
| | if err := report.Generate(out, rpt, ui.options.Obj); err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return |
| | } |
| |
|
| | legend := report.ProfileLabels(rpt) |
| | ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{ |
| | TextBody: out.String(), |
| | }) |
| | } |
| |
|
| | |
| | func (ui *webInterface) saveConfig(w http.ResponseWriter, req *http.Request) { |
| | if err := setConfig(ui.settingsFile, *req.URL); err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return |
| | } |
| | } |
| |
|
| | |
| | func (ui *webInterface) deleteConfig(w http.ResponseWriter, req *http.Request) { |
| | name := req.URL.Query().Get("config") |
| | if err := removeConfig(ui.settingsFile, name); err != nil { |
| | http.Error(w, err.Error(), http.StatusBadRequest) |
| | ui.options.UI.PrintErr(err) |
| | return |
| | } |
| | } |
| |
|
| | |
| | |
| | func getFromLegend(legend []string, param, def string) string { |
| | for _, s := range legend { |
| | if strings.HasPrefix(s, param) { |
| | return s[len(param):] |
| | } |
| | } |
| | return def |
| | } |
| |
|