1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package driver
16
17 import (
18 "bytes"
19 "fmt"
20 "html/template"
21 "io"
22 "net"
23 "net/http"
24 gourl "net/url"
25 "os"
26 "os/exec"
27 "strconv"
28 "strings"
29 "time"
30
31 "github.com/google/pprof/internal/graph"
32 "github.com/google/pprof/internal/measurement"
33 "github.com/google/pprof/internal/plugin"
34 "github.com/google/pprof/internal/report"
35 "github.com/google/pprof/profile"
36 )
37
38
39 type webInterface struct {
40 prof *profile.Profile
41 copier profileCopier
42 options *plugin.Options
43 help map[string]string
44 settingsFile string
45 }
46
47 func makeWebInterface(p *profile.Profile, copier profileCopier, opt *plugin.Options) (*webInterface, error) {
48 settingsFile, err := settingsFileName()
49 if err != nil {
50 return nil, err
51 }
52 return &webInterface{
53 prof: p,
54 copier: copier,
55 options: opt,
56 help: make(map[string]string),
57 settingsFile: settingsFile,
58 }, nil
59 }
60
61
62 const maxEntries = 50
63
64
65 type errorCatcher struct {
66 plugin.UI
67 errors []string
68 }
69
70 func (ec *errorCatcher) PrintErr(args ...interface{}) {
71 ec.errors = append(ec.errors, strings.TrimSuffix(fmt.Sprintln(args...), "\n"))
72 ec.UI.PrintErr(args...)
73 }
74
75
76 type webArgs struct {
77 Title string
78 Errors []string
79 Total int64
80 SampleTypes []string
81 Legend []string
82 DocURL string
83 Standalone bool
84 Help map[string]string
85 Nodes []string
86 HTMLBody template.HTML
87 TextBody string
88 Top []report.TextItem
89 Listing report.WebListData
90 FlameGraph template.JS
91 Stacks template.JS
92 Configs []configMenuEntry
93 UnitDefs []measurement.UnitType
94 }
95
96 func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, disableBrowser bool) error {
97 host, port, err := getHostAndPort(hostport)
98 if err != nil {
99 return err
100 }
101 interactiveMode = true
102 copier := makeProfileCopier(p)
103 ui, err := makeWebInterface(p, copier, o)
104 if err != nil {
105 return err
106 }
107 for n, c := range pprofCommands {
108 ui.help[n] = c.description
109 }
110 for n, help := range configHelp {
111 ui.help[n] = help
112 }
113 ui.help["details"] = "Show information about the profile and this view"
114 ui.help["graph"] = "Display profile as a directed graph"
115 ui.help["flamegraph"] = "Display profile as a flame graph"
116 ui.help["reset"] = "Show the entire profile"
117 ui.help["save_config"] = "Save current settings"
118
119 server := o.HTTPServer
120 if server == nil {
121 server = defaultWebServer
122 }
123 args := &plugin.HTTPServerArgs{
124 Hostport: net.JoinHostPort(host, strconv.Itoa(port)),
125 Host: host,
126 Port: port,
127 Handlers: map[string]http.Handler{
128 "/": http.HandlerFunc(ui.dot),
129 "/top": http.HandlerFunc(ui.top),
130 "/disasm": http.HandlerFunc(ui.disasm),
131 "/source": http.HandlerFunc(ui.source),
132 "/peek": http.HandlerFunc(ui.peek),
133 "/flamegraph": http.HandlerFunc(ui.stackView),
134 "/flamegraph2": redirectWithQuery("flamegraph", http.StatusMovedPermanently),
135 "/flamegraphold": redirectWithQuery("flamegraph", http.StatusMovedPermanently),
136 "/saveconfig": http.HandlerFunc(ui.saveConfig),
137 "/deleteconfig": http.HandlerFunc(ui.deleteConfig),
138 "/download": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
139 w.Header().Set("Content-Type", "application/vnd.google.protobuf+gzip")
140 w.Header().Set("Content-Disposition", "attachment;filename=profile.pb.gz")
141 p.Write(w)
142 }),
143 },
144 }
145
146 url := "http://" + args.Hostport
147
148 o.UI.Print("Serving web UI on ", url)
149
150 if o.UI.WantBrowser() && !disableBrowser {
151 go openBrowser(url, o)
152 }
153 return server(args)
154 }
155
156 func getHostAndPort(hostport string) (string, int, error) {
157 host, portStr, err := net.SplitHostPort(hostport)
158 if err != nil {
159 return "", 0, fmt.Errorf("could not split http address: %v", err)
160 }
161 if host == "" {
162 host = "localhost"
163 }
164 var port int
165 if portStr == "" {
166 ln, err := net.Listen("tcp", net.JoinHostPort(host, "0"))
167 if err != nil {
168 return "", 0, fmt.Errorf("could not generate random port: %v", err)
169 }
170 port = ln.Addr().(*net.TCPAddr).Port
171 err = ln.Close()
172 if err != nil {
173 return "", 0, fmt.Errorf("could not generate random port: %v", err)
174 }
175 } else {
176 port, err = strconv.Atoi(portStr)
177 if err != nil {
178 return "", 0, fmt.Errorf("invalid port number: %v", err)
179 }
180 }
181 return host, port, nil
182 }
183 func defaultWebServer(args *plugin.HTTPServerArgs) error {
184 ln, err := net.Listen("tcp", args.Hostport)
185 if err != nil {
186 return err
187 }
188 isLocal := isLocalhost(args.Host)
189 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
190 if isLocal {
191
192 host, _, err := net.SplitHostPort(req.RemoteAddr)
193 if err != nil || !isLocalhost(host) {
194 http.Error(w, "permission denied", http.StatusForbidden)
195 return
196 }
197 }
198 h := args.Handlers[req.URL.Path]
199 if h == nil {
200
201 h = http.DefaultServeMux
202 }
203 h.ServeHTTP(w, req)
204 })
205
206
207
208
209
210 mux := http.NewServeMux()
211 mux.Handle("/ui/", http.StripPrefix("/ui", handler))
212 mux.Handle("/", redirectWithQuery("/ui", http.StatusTemporaryRedirect))
213 s := &http.Server{Handler: mux}
214 return s.Serve(ln)
215 }
216
217
218
219
220
221 func redirectWithQuery(path string, code int) http.HandlerFunc {
222 return func(w http.ResponseWriter, r *http.Request) {
223 pathWithQuery := &gourl.URL{Path: path, RawQuery: r.URL.RawQuery}
224 w.Header().Set("Location", pathWithQuery.String())
225 w.WriteHeader(code)
226 }
227 }
228
229 func isLocalhost(host string) bool {
230 for _, v := range []string{"localhost", "127.0.0.1", "[::1]", "::1"} {
231 if host == v {
232 return true
233 }
234 }
235 return false
236 }
237
238 func openBrowser(url string, o *plugin.Options) {
239
240 baseURL, _ := gourl.Parse(url)
241 current := currentConfig()
242 u, _ := current.makeURL(*baseURL)
243
244
245 time.Sleep(time.Millisecond * 500)
246
247 for _, b := range browsers() {
248 args := strings.Split(b, " ")
249 if len(args) == 0 {
250 continue
251 }
252 viewer := exec.Command(args[0], append(args[1:], u.String())...)
253 viewer.Stderr = os.Stderr
254 if err := viewer.Start(); err == nil {
255 return
256 }
257 }
258
259 o.UI.PrintErr(u.String())
260 }
261
262
263
264 func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
265 cmd []string, configEditor func(*config)) (*report.Report, []string) {
266 cfg := currentConfig()
267 if err := cfg.applyURL(req.URL.Query()); err != nil {
268 http.Error(w, err.Error(), http.StatusBadRequest)
269 ui.options.UI.PrintErr(err)
270 return nil, nil
271 }
272 if configEditor != nil {
273 configEditor(&cfg)
274 }
275 catcher := &errorCatcher{UI: ui.options.UI}
276 options := *ui.options
277 options.UI = catcher
278 _, rpt, err := generateRawReport(ui.copier.newCopy(), cmd, cfg, &options)
279 if err != nil {
280 http.Error(w, err.Error(), http.StatusBadRequest)
281 ui.options.UI.PrintErr(err)
282 return nil, nil
283 }
284 return rpt, catcher.errors
285 }
286
287
288 func renderHTML(dst io.Writer, tmpl string, rpt *report.Report, errList, legend []string, data webArgs) error {
289 file := getFromLegend(legend, "File: ", "unknown")
290 profile := getFromLegend(legend, "Type: ", "unknown")
291 data.Title = file + " " + profile
292 data.Errors = errList
293 data.Total = rpt.Total()
294 data.DocURL = rpt.DocURL()
295 data.Legend = legend
296 return getHTMLTemplates().ExecuteTemplate(dst, tmpl, data)
297 }
298
299
300 func (ui *webInterface) render(w http.ResponseWriter, req *http.Request, tmpl string,
301 rpt *report.Report, errList, legend []string, data webArgs) {
302 data.SampleTypes = sampleTypes(ui.prof)
303 data.Help = ui.help
304 data.Configs = configMenu(ui.settingsFile, *req.URL)
305 html := &bytes.Buffer{}
306 if err := renderHTML(html, tmpl, rpt, errList, legend, data); err != nil {
307 http.Error(w, "internal template error", http.StatusInternalServerError)
308 ui.options.UI.PrintErr(err)
309 return
310 }
311 w.Header().Set("Content-Type", "text/html")
312 w.Write(html.Bytes())
313 }
314
315
316 func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
317 rpt, errList := ui.makeReport(w, req, []string{"svg"}, nil)
318 if rpt == nil {
319 return
320 }
321
322
323 g, config := report.GetDOT(rpt)
324 legend := config.Labels
325 config.Labels = nil
326 dot := &bytes.Buffer{}
327 graph.ComposeDot(dot, g, &graph.DotAttributes{}, config)
328
329
330 svg, err := dotToSvg(dot.Bytes())
331 if err != nil {
332 http.Error(w, "Could not execute dot; may need to install graphviz.",
333 http.StatusNotImplemented)
334 ui.options.UI.PrintErr("Failed to execute dot. Is Graphviz installed?\n", err)
335 return
336 }
337
338
339 nodes := []string{""}
340 for _, n := range g.Nodes {
341 nodes = append(nodes, n.Info.Name)
342 }
343
344 ui.render(w, req, "graph", rpt, errList, legend, webArgs{
345 HTMLBody: template.HTML(string(svg)),
346 Nodes: nodes,
347 })
348 }
349
350 func dotToSvg(dot []byte) ([]byte, error) {
351 cmd := exec.Command("dot", "-Tsvg")
352 out := &bytes.Buffer{}
353 cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(dot), out, os.Stderr
354 if err := cmd.Run(); err != nil {
355 return nil, err
356 }
357
358
359 svg := bytes.Replace(out.Bytes(), []byte("&;"), []byte("&;"), -1)
360
361
362 if pos := bytes.Index(svg, []byte("<svg")); pos >= 0 {
363 svg = svg[pos:]
364 }
365 return svg, nil
366 }
367
368 func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
369 rpt, errList := ui.makeReport(w, req, []string{"top"}, func(cfg *config) {
370 cfg.NodeCount = 500
371 })
372 if rpt == nil {
373 return
374 }
375 top, legend := report.TextItems(rpt)
376 var nodes []string
377 for _, item := range top {
378 nodes = append(nodes, item.Name)
379 }
380
381 ui.render(w, req, "top", rpt, errList, legend, webArgs{
382 Top: top,
383 Nodes: nodes,
384 })
385 }
386
387
388 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
389 args := []string{"disasm", req.URL.Query().Get("f")}
390 rpt, errList := ui.makeReport(w, req, args, nil)
391 if rpt == nil {
392 return
393 }
394
395 out := &bytes.Buffer{}
396 if err := report.PrintAssembly(out, rpt, ui.options.Obj, maxEntries); err != nil {
397 http.Error(w, err.Error(), http.StatusBadRequest)
398 ui.options.UI.PrintErr(err)
399 return
400 }
401
402 legend := report.ProfileLabels(rpt)
403 ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{
404 TextBody: out.String(),
405 })
406
407 }
408
409
410
411 func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
412 args := []string{"weblist", req.URL.Query().Get("f")}
413 rpt, errList := ui.makeReport(w, req, args, nil)
414 if rpt == nil {
415 return
416 }
417
418
419 listing, err := report.MakeWebList(rpt, ui.options.Obj, maxEntries)
420 if err != nil {
421 http.Error(w, err.Error(), http.StatusBadRequest)
422 ui.options.UI.PrintErr(err)
423 return
424 }
425
426 legend := report.ProfileLabels(rpt)
427 ui.render(w, req, "sourcelisting", rpt, errList, legend, webArgs{
428 Listing: listing,
429 })
430 }
431
432
433 func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
434 args := []string{"peek", req.URL.Query().Get("f")}
435 rpt, errList := ui.makeReport(w, req, args, func(cfg *config) {
436 cfg.Granularity = "lines"
437 })
438 if rpt == nil {
439 return
440 }
441
442 out := &bytes.Buffer{}
443 if err := report.Generate(out, rpt, ui.options.Obj); err != nil {
444 http.Error(w, err.Error(), http.StatusBadRequest)
445 ui.options.UI.PrintErr(err)
446 return
447 }
448
449 legend := report.ProfileLabels(rpt)
450 ui.render(w, req, "plaintext", rpt, errList, legend, webArgs{
451 TextBody: out.String(),
452 })
453 }
454
455
456 func (ui *webInterface) saveConfig(w http.ResponseWriter, req *http.Request) {
457 if err := setConfig(ui.settingsFile, *req.URL); err != nil {
458 http.Error(w, err.Error(), http.StatusBadRequest)
459 ui.options.UI.PrintErr(err)
460 return
461 }
462 }
463
464
465 func (ui *webInterface) deleteConfig(w http.ResponseWriter, req *http.Request) {
466 name := req.URL.Query().Get("config")
467 if err := removeConfig(ui.settingsFile, name); err != nil {
468 http.Error(w, err.Error(), http.StatusBadRequest)
469 ui.options.UI.PrintErr(err)
470 return
471 }
472 }
473
474
475
476 func getFromLegend(legend []string, param, def string) string {
477 for _, s := range legend {
478 if strings.HasPrefix(s, param) {
479 return s[len(param):]
480 }
481 }
482 return def
483 }
484
View as plain text