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