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