瀏覽代碼

Added peek command to web interface. (#161)

Detailed list of changes:
* Since the list of actions is now long, move it into
  an action box that is displayed only when applicable.
* Display help messages when hovering over action buttons.
* Hitting return in the search box invokes "focus" action.
* Cleaned up stale css entries.

I have attached one of the resulting pages to see what things look like easily.  It is static, so won't allow navigation to other pages, but should be interactive enough.

[benchcpu.zip](https://github.com/google/pprof/files/1151238/benchcpu.zip)
Sanjay Ghemawat 7 年之前
父節點
當前提交
7209e76b00
共有 3 個檔案被更改,包括 115 行新增69 行删除
  1. 82
    60
      internal/driver/webhtml.go
  2. 21
    4
      internal/driver/webui.go
  3. 12
    5
      internal/driver/webui_test.go

+ 82
- 60
internal/driver/webhtml.go 查看文件

32
   width: 100%;
32
   width: 100%;
33
   overflow: hidden;
33
   overflow: hidden;
34
 }
34
 }
35
-h1 {
36
-  font-weight: normal;
37
-  font-size: 24px;
38
-  padding: 0em;
39
-  margin-top: 5px;
40
-  margin-bottom: 5px;
41
-}
42
 #page {
35
 #page {
43
   display: flex;
36
   display: flex;
44
   flex-direction: column;
37
   flex-direction: column;
48
   min-width: 100%;
41
   min-width: 100%;
49
   margin: 0px;
42
   margin: 0px;
50
 }
43
 }
51
-#header {
52
-  flex: 0 0 auto;
53
-  width: 100%;
54
-}
55
-#leftbuttons {
56
-  float: left;
57
-}
58
-#rightbuttons {
59
-  float: right;
60
-  display: table-cell;
61
-  vertical-align: middle;
62
-}
63
-#rightbuttons label {
64
-  vertical-align: middle;
65
-}
66
-#scale {
67
-  vertical-align: middle;
68
-}
69
 #graph {
44
 #graph {
70
   flex: 1 1 auto;
45
   flex: 1 1 auto;
71
   overflow: hidden;
46
   overflow: hidden;
73
 svg {
48
 svg {
74
   width: 100%;
49
   width: 100%;
75
   height: auto;
50
   height: auto;
76
-  border: 1px solid black;
77
 }
51
 }
78
 button {
52
 button {
79
   margin-top: 5px;
53
   margin-top: 5px;
80
   margin-bottom: 5px;
54
   margin-bottom: 5px;
81
 }
55
 }
82
-#reset, #scale {
56
+#reset {
83
   margin-left: 10px;
57
   margin-left: 10px;
84
 }
58
 }
85
 #detailtext {
59
 #detailtext {
91
   box-shadow: 2px 2px 2px 0px #aaa;
65
   box-shadow: 2px 2px 2px 0px #aaa;
92
   z-index: 1;
66
   z-index: 1;
93
 }
67
 }
68
+#actionbox {
69
+  display: none;
70
+  position: fixed;
71
+  background-color: #ffffff;
72
+  border: 1px solid black;
73
+  box-shadow: 2px 2px 2px 0px #aaa;
74
+  top: 20px;
75
+  right: 20px;
76
+  z-index: 1;
77
+}
78
+.actionhdr {
79
+  background-color: #ddd;
80
+  width: 100%;
81
+  border-bottom: 1px solid black;
82
+  border-top: 1px solid black;
83
+  font-size: 14pt;
84
+}
85
+#actionbox > button {
86
+  display: block;
87
+  width: 100%;
88
+  margin: 0px;
89
+  text-align: left;
90
+  padding-left: 0.5em;
91
+  background-color: #fff;
92
+  border: none;
93
+  font-size: 12pt;
94
+}
95
+#actionbox > button:hover {
96
+  background-color: #ddd;
97
+}
98
+#home {
99
+  font-size: 20pt;
100
+  padding-left: 0.5em;
101
+  padding-right: 0.5em;
102
+}
94
 </style>
103
 </style>
95
 </head>
104
 </head>
96
 <body>
105
 <body>
97
-<h1>{{.Title}}</h1>
98
-<div id="page">
99
 
106
 
100
-<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
101
-
102
-<div id="header">
103
-<div id="leftbuttons">
104
 <button id="details">&#x25b7; Details</button>
107
 <button id="details">&#x25b7; Details</button>
105
 <div id="detailtext">
108
 <div id="detailtext">
106
 {{range .Legend}}<div>{{.}}</div>{{end}}
109
 {{range .Legend}}<div>{{.}}</div>{{end}}
107
 </div>
110
 </div>
108
-<button id="list">List</button>
109
-<button id="disasm">Disasm</button>
110
-<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
111
-<button id="focus">Focus</button>
112
-<button id="ignore">Ignore</button>
113
-<button id="hide">Hide</button>
114
-<button id="show">Show</button>
111
+
115
 <button id="reset">Reset</button>
112
 <button id="reset">Reset</button>
116
-</div>
117
-<div id="rightbuttons">
118
-</div>
119
-</div>
113
+
114
+<span id="home">{{.Title}}</span>
115
+
116
+<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
117
+
118
+<div id="page">
119
+
120
+<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
120
 
121
 
121
 <div id="graph">
122
 <div id="graph">
123
+
124
+<div id="actionbox">
125
+<div class="actionhdr">Refine graph</div>
126
+<button title="{{.Help.focus}}" id="focus">Focus</button>
127
+<button title="{{.Help.ignore}}" id="ignore">Ignore</button>
128
+<button title="{{.Help.hide}}" id="hide">Hide</button>
129
+<button title="{{.Help.show}}" id="show">Show</button>
130
+<div class="actionhdr">Show Functions</div>
131
+<button title="{{.Help.peek}}" id="peek">Peek</button>
132
+<button title="{{.Help.list}}" id="list">List</button>
133
+<button title="{{.Help.disasm}}" id="disasm">Disassemble</button>
134
+</div>
135
+
122
 {{.Svg}}
136
 {{.Svg}}
123
 </div>
137
 </div>
124
 
138
 
340
   // Elements
354
   // Elements
341
   const detailsButton = document.getElementById("details")
355
   const detailsButton = document.getElementById("details")
342
   const detailsText = document.getElementById("detailtext")
356
   const detailsText = document.getElementById("detailtext")
357
+  const actionBox = document.getElementById("actionbox")
343
   const listButton = document.getElementById("list")
358
   const listButton = document.getElementById("list")
344
   const disasmButton = document.getElementById("disasm")
359
   const disasmButton = document.getElementById("disasm")
345
   const resetButton = document.getElementById("reset")
360
   const resetButton = document.getElementById("reset")
361
+  const peekButton = document.getElementById("peek")
346
   const focusButton = document.getElementById("focus")
362
   const focusButton = document.getElementById("focus")
347
   const showButton = document.getElementById("show")
363
   const showButton = document.getElementById("show")
348
   const ignoreButton = document.getElementById("ignore")
364
   const ignoreButton = document.getElementById("ignore")
351
   const graph0 = document.getElementById("graph0")
367
   const graph0 = document.getElementById("graph0")
352
   const svg = graph0.parentElement
368
   const svg = graph0.parentElement
353
 
369
 
354
-  let currentRe = ""
370
+  let regexpActive = false
355
   let selected = new Map()
371
   let selected = new Map()
356
   let origFill = new Map()
372
   let origFill = new Map()
357
   let searchAlarm = null
373
   let searchAlarm = null
370
   function handleReset() { window.location.href = "/" }
386
   function handleReset() { window.location.href = "/" }
371
   function handleList() { navigate("/weblist", "f", true) }
387
   function handleList() { navigate("/weblist", "f", true) }
372
   function handleDisasm() { navigate("/disasm", "f", true) }
388
   function handleDisasm() { navigate("/disasm", "f", true) }
389
+  function handlePeek() { navigate("/peek", "f", true) }
373
   function handleFocus() { navigate("/", "f", false) }
390
   function handleFocus() { navigate("/", "f", false) }
374
   function handleShow() { navigate("/", "s", false) }
391
   function handleShow() { navigate("/", "s", false) }
375
   function handleIgnore() { navigate("/", "i", false) }
392
   function handleIgnore() { navigate("/", "i", false) }
376
   function handleHide() { navigate("/", "h", false) }
393
   function handleHide() { navigate("/", "h", false) }
377
 
394
 
395
+  function handleKey(e) {
396
+    if (e.keyCode != 13) return
397
+    handleFocus()
398
+    e.preventDefault()
399
+  }
400
+
378
   function handleSearch() {
401
   function handleSearch() {
379
-    // Delay processing so a flurry of key strokes is handled once.
402
+    // Delay expensive processing so a flurry of key strokes is handled once.
380
     if (searchAlarm != null) {
403
     if (searchAlarm != null) {
381
       clearTimeout(searchAlarm)
404
       clearTimeout(searchAlarm)
382
     }
405
     }
383
-    searchAlarm = setTimeout(doSearch, 300)
406
+    searchAlarm = setTimeout(selectMatching, 300)
407
+
408
+    regexpActive = true
409
+    updateButtons()
384
   }
410
   }
385
 
411
 
386
-  function doSearch() {
412
+  function selectMatching() {
387
     searchAlarm = null
413
     searchAlarm = null
388
     let re = null
414
     let re = null
389
     if (search.value != "") {
415
     if (search.value != "") {
394
         return
420
         return
395
       }
421
       }
396
     }
422
     }
397
-    currentRe = search.value
398
 
423
 
399
     function match(text) {
424
     function match(text) {
400
       return re != null && re.test(text)
425
       return re != null && re.test(text)
425
     if (!elem) return
450
     if (!elem) return
426
 
451
 
427
     // Disable regexp mode.
452
     // Disable regexp mode.
428
-    currentRe = ""
453
+    regexpActive = false
429
 
454
 
430
     const n = nodeId(elem)
455
     const n = nodeId(elem)
431
     if (n < 0) return
456
     if (n < 0) return
485
   function navigate(path, param, newWindow) {
510
   function navigate(path, param, newWindow) {
486
     // The selection can be in one of two modes: regexp-based or
511
     // The selection can be in one of two modes: regexp-based or
487
     // list-based.  Construct regular expression depending on mode.
512
     // list-based.  Construct regular expression depending on mode.
488
-    let re = currentRe
489
-    if (re == "") {
513
+    let re = regexpActive ? search.value : ""
514
+    if (!regexpActive) {
490
       selected.forEach(function(v, key) {
515
       selected.forEach(function(v, key) {
491
         if (re != "") re += "|"
516
         if (re != "") re += "|"
492
         re += nodes[key]
517
         re += nodes[key]
517
   }
542
   }
518
 
543
 
519
   function updateButtons() {
544
   function updateButtons() {
520
-    const enable = (currentRe != "" || selected.size != 0)
545
+    const enable = (search.value != "" || selected.size != 0)
521
     if (buttonsEnabled == enable) return
546
     if (buttonsEnabled == enable) return
522
     buttonsEnabled = enable
547
     buttonsEnabled = enable
523
-    listButton.disabled = !enable
524
-    disasmButton.disabled = !enable
525
-    focusButton.disabled = !enable
526
-    showButton.disabled = !enable
527
-    ignoreButton.disabled = !enable
528
-    hideButton.disabled = !enable
548
+    actionBox.style.display = enable ? "block" : "none"
529
   }
549
   }
530
 
550
 
531
   // Initialize button states
551
   // Initialize button states
536
   
556
   
537
   function bindButtons(evt) {
557
   function bindButtons(evt) {
538
     detailsButton.addEventListener(evt, handleDetails)
558
     detailsButton.addEventListener(evt, handleDetails)
559
+    resetButton.addEventListener(evt, handleReset)
539
     listButton.addEventListener(evt, handleList)
560
     listButton.addEventListener(evt, handleList)
540
     disasmButton.addEventListener(evt, handleDisasm)
561
     disasmButton.addEventListener(evt, handleDisasm)
541
-    resetButton.addEventListener(evt, handleReset)
562
+    peekButton.addEventListener(evt, handlePeek)
542
     focusButton.addEventListener(evt, handleFocus)
563
     focusButton.addEventListener(evt, handleFocus)
543
     showButton.addEventListener(evt, handleShow)
564
     showButton.addEventListener(evt, handleShow)
544
     ignoreButton.addEventListener(evt, handleIgnore)
565
     ignoreButton.addEventListener(evt, handleIgnore)
547
   bindButtons("click")
568
   bindButtons("click")
548
   bindButtons("touchstart")
569
   bindButtons("touchstart")
549
   search.addEventListener("input", handleSearch)
570
   search.addEventListener("input", handleSearch)
571
+  search.addEventListener("keydown", handleKey)
550
 }
572
 }
551
 
573
 
552
 dotviewer({{.Nodes}})
574
 dotviewer({{.Nodes}})

+ 21
- 4
internal/driver/webui.go 查看文件

38
 type webInterface struct {
38
 type webInterface struct {
39
 	prof    *profile.Profile
39
 	prof    *profile.Profile
40
 	options *plugin.Options
40
 	options *plugin.Options
41
+	help    map[string]string
41
 }
42
 }
42
 
43
 
43
 // errorCatcher is a UI that captures errors for reporting to the browser.
44
 // errorCatcher is a UI that captures errors for reporting to the browser.
56
 	ui := &webInterface{
57
 	ui := &webInterface{
57
 		prof:    p,
58
 		prof:    p,
58
 		options: o,
59
 		options: o,
60
+		help:    make(map[string]string),
61
+	}
62
+	for n, c := range pprofCommands {
63
+		ui.help[n] = c.description
64
+	}
65
+	for n, v := range pprofVariables {
66
+		ui.help[n] = v.help
59
 	}
67
 	}
60
 
68
 
61
 	ln, url, isLocal, err := newListenerAndURL(hostport)
69
 	ln, url, isLocal, err := newListenerAndURL(hostport)
74
 	mux.Handle("/", wrap(http.HandlerFunc(ui.dot)))
82
 	mux.Handle("/", wrap(http.HandlerFunc(ui.dot)))
75
 	mux.Handle("/disasm", wrap(http.HandlerFunc(ui.disasm)))
83
 	mux.Handle("/disasm", wrap(http.HandlerFunc(ui.disasm)))
76
 	mux.Handle("/weblist", wrap(http.HandlerFunc(ui.weblist)))
84
 	mux.Handle("/weblist", wrap(http.HandlerFunc(ui.weblist)))
85
+	mux.Handle("/peek", wrap(http.HandlerFunc(ui.peek)))
77
 
86
 
78
 	s := &http.Server{Handler: mux}
87
 	s := &http.Server{Handler: mux}
79
 	go openBrowser(url, o)
88
 	go openBrowser(url, o)
204
 		Svg    template.HTML
213
 		Svg    template.HTML
205
 		Legend []string
214
 		Legend []string
206
 		Nodes  []string
215
 		Nodes  []string
216
+		Help   map[string]string
207
 	}{
217
 	}{
208
 		Title:  file + " " + profile,
218
 		Title:  file + " " + profile,
209
 		Errors: catcher.errors,
219
 		Errors: catcher.errors,
210
 		Svg:    template.HTML(string(svg)),
220
 		Svg:    template.HTML(string(svg)),
211
 		Legend: legend,
221
 		Legend: legend,
212
 		Nodes:  nodes,
222
 		Nodes:  nodes,
223
+		Help:   ui.help,
213
 	}
224
 	}
214
 	html := &bytes.Buffer{}
225
 	html := &bytes.Buffer{}
215
 	if err := graphTemplate.Execute(html, data); err != nil {
226
 	if err := graphTemplate.Execute(html, data); err != nil {
241
 
252
 
242
 // disasm generates a web page containing disassembly.
253
 // disasm generates a web page containing disassembly.
243
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
254
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
244
-	ui.output(w, req, "disasm", "text/plain")
255
+	ui.output(w, req, "disasm", "text/plain", pprofVariables.makeCopy())
245
 }
256
 }
246
 
257
 
247
 // weblist generates a web page containing disassembly.
258
 // weblist generates a web page containing disassembly.
248
 func (ui *webInterface) weblist(w http.ResponseWriter, req *http.Request) {
259
 func (ui *webInterface) weblist(w http.ResponseWriter, req *http.Request) {
249
-	ui.output(w, req, "weblist", "text/html")
260
+	ui.output(w, req, "weblist", "text/html", pprofVariables.makeCopy())
261
+}
262
+
263
+// peek generates a web page listing callers/callers.
264
+func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
265
+	vars := pprofVariables.makeCopy()
266
+	vars.set("lines", "t") // Switch to line granularity
267
+	ui.output(w, req, "peek", "text/plain", vars)
250
 }
268
 }
251
 
269
 
252
 // output generates a webpage that contains the output of the specified pprof cmd.
270
 // output generates a webpage that contains the output of the specified pprof cmd.
253
-func (ui *webInterface) output(w http.ResponseWriter, req *http.Request, cmd, ctype string) {
271
+func (ui *webInterface) output(w http.ResponseWriter, req *http.Request, cmd, ctype string, vars variables) {
254
 	focus := req.URL.Query().Get("f")
272
 	focus := req.URL.Query().Get("f")
255
 	if focus == "" {
273
 	if focus == "" {
256
 		fmt.Fprintln(w, "no argument supplied for "+cmd)
274
 		fmt.Fprintln(w, "no argument supplied for "+cmd)
263
 	options.UI = catcher
281
 	options.UI = catcher
264
 
282
 
265
 	args := []string{cmd, focus}
283
 	args := []string{cmd, focus}
266
-	vars := pprofVariables.makeCopy()
267
 	_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
284
 	_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
268
 	if err != nil {
285
 	if err != nil {
269
 		http.Error(w, err.Error(), http.StatusBadRequest)
286
 		http.Error(w, err.Error(), http.StatusBadRequest)

+ 12
- 5
internal/driver/webui_test.go 查看文件

22
 	"net/url"
22
 	"net/url"
23
 	"os/exec"
23
 	"os/exec"
24
 	"regexp"
24
 	"regexp"
25
-	"strings"
26
 	"testing"
25
 	"testing"
27
 
26
 
28
 	"github.com/google/pprof/internal/plugin"
27
 	"github.com/google/pprof/internal/plugin"
31
 
30
 
32
 func TestWebInterface(t *testing.T) {
31
 func TestWebInterface(t *testing.T) {
33
 	prof := makeFakeProfile()
32
 	prof := makeFakeProfile()
34
-	ui := &webInterface{prof, &plugin.Options{Obj: fakeObjTool{}}}
33
+	ui := &webInterface{
34
+		prof:    prof,
35
+		options: &plugin.Options{Obj: fakeObjTool{}},
36
+		help:    make(map[string]string),
37
+	}
35
 
38
 
36
 	// Start test server.
39
 	// Start test server.
37
 	server := httptest.NewServer(http.HandlerFunc(
40
 	server := httptest.NewServer(http.HandlerFunc(
41
 				ui.dot(w, r)
44
 				ui.dot(w, r)
42
 			case "/disasm":
45
 			case "/disasm":
43
 				ui.disasm(w, r)
46
 				ui.disasm(w, r)
47
+			case "/peek":
48
+				ui.peek(w, r)
44
 			case "/weblist":
49
 			case "/weblist":
45
 				ui.weblist(w, r)
50
 				ui.weblist(w, r)
46
 			}
51
 			}
61
 		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
66
 		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
62
 		{"/weblist?f=" + url.QueryEscape("F[12]"),
67
 		{"/weblist?f=" + url.QueryEscape("F[12]"),
63
 			[]string{"F1", "F2", "300ms line1"}, false},
68
 			[]string{"F1", "F2", "300ms line1"}, false},
69
+		{"/peek?f=" + url.QueryEscape("F[12]"),
70
+			[]string{"300ms.*F1", "200ms.*300ms.*F2"}, false},
64
 		{"/disasm?f=" + url.QueryEscape("F[12]"),
71
 		{"/disasm?f=" + url.QueryEscape("F[12]"),
65
 			[]string{"f1:asm", "f2:asm"}, false},
72
 			[]string{"f1:asm", "f2:asm"}, false},
66
 	}
73
 	}
82
 		}
89
 		}
83
 		result := string(data)
90
 		result := string(data)
84
 		for _, w := range c.want {
91
 		for _, w := range c.want {
85
-			if !strings.Contains(result, w) {
86
-				t.Errorf("response for %s does not contain "+
87
-					"expected string '%s'; "+
92
+			if match, _ := regexp.MatchString(w, result); !match {
93
+				t.Errorf("response for %s does not match "+
94
+					"expected pattern '%s'; "+
88
 					"actual result:\n%s", c.path, w, result)
95
 					"actual result:\n%s", c.path, w, result)
89
 			}
96
 			}
90
 		}
97
 		}