Переглянути джерело

Extend web UI to contain a tabular "top" display (#194)

* Split template into multiple defined templates for ease of sharing.

* Add a menu bar at the top and tie all buttons to it.

* Add top view.

* Handle top table clicks.

* Handle regexp based selection of top entries.

* Added test for /top view.

* Reduced code duplication between / and /top handlers.

* Add Refine menu to Top view as well.

Details:
* Pass base URL to web page.
* Use base URL in navigate() to stay in same view.
* Change Reset to stay in same view.

* Handled review comments + increased top display from 50 to 500 rows:

* Moved "inline" indicator to separate column of top display.
* Moved "Reset" button to "Refine" menu.
* Simplified some Javascript.
* Handle all meta characters in quotemeta.

* Removed unnecessary css "display: inline" from closedetails.

Signed-off-by: Sanjay Ghemawat <sanjay@alum.mit.edu>
Sanjay Ghemawat 7 роки тому
джерело
коміт
01dfac58a3

+ 273
- 99
internal/driver/webhtml.go Переглянути файл

@@ -16,12 +16,14 @@ package driver
16 16
 
17 17
 import "html/template"
18 18
 
19
-var graphTemplate = template.Must(template.New("graph").Parse(
20
-	`<!DOCTYPE html>
21
-<html>
22
-<head>
23
-<meta charset="utf-8">
24
-<title>{{.Title}}</title>
19
+// webTemplate defines a collection of related templates:
20
+//    css
21
+//    header
22
+//    script
23
+//    graph
24
+//    top
25
+var webTemplate = template.Must(template.New("web").Parse(`
26
+{{define "css"}}
25 27
 <style type="text/css">
26 28
 html, body {
27 29
   height: 100%;
@@ -30,9 +32,11 @@ html, body {
30 32
 }
31 33
 body {
32 34
   width: 100%;
35
+  height: 100%;
36
+  min-height: 100%;
33 37
   overflow: hidden;
34 38
 }
35
-#page {
39
+#graphcontainer {
36 40
   display: flex;
37 41
   flex-direction: column;
38 42
   height: 100%;
@@ -53,90 +57,185 @@ button {
53 57
   margin-top: 5px;
54 58
   margin-bottom: 5px;
55 59
 }
56
-#reset {
57
-  margin-left: 10px;
58
-}
59 60
 #detailtext {
60 61
   display: none;
61
-  position: absolute;
62
+  position: fixed;
63
+  top: 20px;
64
+  right: 10px;
62 65
   background-color: #ffffff;
63 66
   min-width: 160px;
64
-  border-top: 1px solid black;
65
-  box-shadow: 2px 2px 2px 0px #aaa;
67
+  border: 1px solid #888;
68
+  box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2);
66 69
   z-index: 1;
67 70
 }
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;
71
+#closedetails {
72
+  float: right;
73
+  margin: 2px;
74
+}
75
+#home {
76
+  font-size: 14pt;
77
+  padding-left: 0.5em;
78
+  padding-right: 0.5em;
79
+  float: right;
77 80
 }
78
-.actionhdr {
79
-  background-color: #ddd;
81
+.menubar {
82
+  display: inline-block;
83
+  background-color: #f8f8f8;
84
+  border: 1px solid #ccc;
80 85
   width: 100%;
81
-  border-bottom: 1px solid black;
82
-  border-top: 1px solid black;
86
+}
87
+.menu-header {
88
+  position: relative;
89
+  display: inline-block;
90
+  padding: 2px 2px;
91
+  cursor: default;
83 92
   font-size: 14pt;
84 93
 }
85
-#actionbox > button {
94
+.menu {
95
+  display: none;
96
+  position: absolute;
97
+  background-color: #f8f8f8;
98
+  border: 1px solid #888;
99
+  box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2);
100
+  z-index: 1;
101
+  margin-top: 2px;
102
+  left: 0px;
103
+  min-width: 5em;
104
+}
105
+.menu hr {
106
+  background-color: #fff;
107
+  margin-top: 0px;
108
+  margin-bottom: 0px;
109
+}
110
+.menu button {
86 111
   display: block;
87 112
   width: 100%;
88 113
   margin: 0px;
89 114
   text-align: left;
90
-  padding-left: 0.5em;
115
+  padding-left: 2px;
91 116
   background-color: #fff;
92
-  border: none;
93 117
   font-size: 12pt;
118
+  border: none;
94 119
 }
95
-#actionbox > button:hover {
96
-  background-color: #ddd;
120
+.menu-header:hover {
121
+  background-color: #ccc;
97 122
 }
98
-#home {
99
-  font-size: 20pt;
100
-  padding-left: 0.5em;
101
-  padding-right: 0.5em;
123
+.menu-header:hover .menu {
124
+  display: block;
125
+}
126
+.menu button:hover {
127
+  background-color: #ccc;
128
+}
129
+#searchbox {
130
+  margin-left: 10pt;
131
+}
132
+#topcontainer {
133
+  width: 100%;
134
+  height: 100%;
135
+  max-height: 100%;
136
+  overflow: scroll;
137
+}
138
+#toptable {
139
+  border-spacing: 0px;
140
+}
141
+#toptable tr th {
142
+  border-bottom: 1px solid black;
143
+  text-align: right;
144
+  padding-left: 1em;
145
+}
146
+#toptable tr th:nth-child(6) { text-align: left; }
147
+#toptable tr th:nth-child(7) { text-align: left; }
148
+#toptable tr td {
149
+  padding-left: 1em;
150
+  font: monospace;
151
+  text-align: right;
152
+  white-space: nowrap;
153
+  cursor: default;
154
+}
155
+#toptable tr td:nth-child(6) { text-align: left; }
156
+#toptable tr td:nth-child(7) { text-align: left; }
157
+.hilite {
158
+  background-color: #ccf;
102 159
 }
103 160
 </style>
104
-</head>
105
-<body>
161
+{{end}}
106 162
 
107
-<button id="details">&#x25b7; Details</button>
163
+{{define "header"}}
108 164
 <div id="detailtext">
165
+<button id="closedetails">Close</button>
109 166
 {{range .Legend}}<div>{{.}}</div>{{end}}
110 167
 </div>
111 168
 
112
-<button id="reset">Reset</button>
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>
169
+<div class="menubar">
170
+
171
+<div class="menu-header">
172
+View
173
+<div class="menu">
174
+{{if (ne .Type "top")}}
175
+  <button title="{{.Help.top}}" id="topbtn">Top</button>
176
+{{end}}
177
+{{if (ne .Type "dot")}}
178
+  <button title="{{.Help.graph}}" id="graphbtn">Graph</button>
179
+{{end}}
180
+<hr>
181
+<button title="{{.Help.details}}" id="details">Details</button>
182
+</div>
183
+</div>
121 184
 
122
-<div id="graph">
185
+<div class="menu-header">
186
+Functions
187
+<div class="menu">
188
+<button title="{{.Help.peek}}" id="peek">Peek</button>
189
+<button title="{{.Help.list}}" id="list">List</button>
190
+<button title="{{.Help.disasm}}" id="disasm">Disassemble</button>
191
+</div>
192
+</div>
123 193
 
124
-<div id="actionbox">
125
-<div class="actionhdr">Refine graph</div>
194
+<div class="menu-header">
195
+Refine
196
+<div class="menu">
126 197
 <button title="{{.Help.focus}}" id="focus">Focus</button>
127 198
 <button title="{{.Help.ignore}}" id="ignore">Ignore</button>
128 199
 <button title="{{.Help.hide}}" id="hide">Hide</button>
129 200
 <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>
201
+<hr>
202
+<button title="{{.Help.reset}}" id="reset">Reset</button>
203
+</div>
134 204
 </div>
135 205
 
206
+<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
207
+
208
+<span id="home">{{.Title}}</span>
209
+
210
+</div> <!-- menubar -->
211
+
212
+<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
213
+{{end}}
214
+
215
+{{define "graph" -}}
216
+<!DOCTYPE html>
217
+<html>
218
+<head>
219
+<meta charset="utf-8">
220
+<title>{{.Title}}</title>
221
+{{template "css" .}}
222
+</head>
223
+<body>
224
+
225
+{{template "header" .}}
226
+<div id="graphcontainer">
227
+<div id="graph">
136 228
 {{.Svg}}
137 229
 </div>
138 230
 
139 231
 </div>
232
+{{template "script" .}}
233
+<script>viewer({{.BaseURL}}, {{.Nodes}})</script>
234
+</body>
235
+</html>
236
+{{end}}
237
+
238
+{{define "script"}}
140 239
 <script>
141 240
 // Make svg pannable and zoomable.
142 241
 // Call clickHandler(t) if a click event is caught by the pan event handlers.
@@ -354,24 +453,14 @@ function initPanAndZoom(svg, clickHandler) {
354 453
   svg.addEventListener("wheel", handleWheel, true)
355 454
 }
356 455
 
357
-function dotviewer(nodes) {
456
+function viewer(baseUrl, nodes) {
358 457
   'use strict';
359 458
 
360 459
   // Elements
361
-  const detailsButton = document.getElementById("details")
362
-  const detailsText = document.getElementById("detailtext")
363
-  const actionBox = document.getElementById("actionbox")
364
-  const listButton = document.getElementById("list")
365
-  const disasmButton = document.getElementById("disasm")
366
-  const resetButton = document.getElementById("reset")
367
-  const peekButton = document.getElementById("peek")
368
-  const focusButton = document.getElementById("focus")
369
-  const showButton = document.getElementById("show")
370
-  const ignoreButton = document.getElementById("ignore")
371
-  const hideButton = document.getElementById("hide")
372 460
   const search = document.getElementById("searchbox")
373 461
   const graph0 = document.getElementById("graph0")
374
-  const svg = graph0.parentElement
462
+  const svg = (graph0 == null ? null : graph0.parentElement)
463
+  const toptable = document.getElementById("toptable")
375 464
 
376 465
   let regexpActive = false
377 466
   let selected = new Map()
@@ -380,23 +469,25 @@ function dotviewer(nodes) {
380 469
   let buttonsEnabled = true
381 470
 
382 471
   function handleDetails() {
383
-    if (detailtext.style.display == "block") {
384
-      detailtext.style.display = "none"
385
-      detailsButton.innerText = "\u25b7 Details"
386
-    } else {
387
-      detailtext.style.display = "block"
388
-      detailsButton.innerText = "\u25bd Details"
389
-    }
472
+    const detailsText = document.getElementById("detailtext")
473
+    if (detailsText != null) detailsText.style.display = "block"
474
+  }
475
+
476
+  function handleCloseDetails() {
477
+    const detailsText = document.getElementById("detailtext")
478
+    if (detailsText != null) detailsText.style.display = "none"
390 479
   }
391 480
 
392
-  function handleReset() { window.location.href = "/" }
481
+  function handleReset() { window.location.href = baseUrl }
482
+  function handleTop() { navigate("/top", "f", false) }
483
+  function handleGraph() { navigate("/", "f", false) }
393 484
   function handleList() { navigate("/weblist", "f", true) }
394 485
   function handleDisasm() { navigate("/disasm", "f", true) }
395 486
   function handlePeek() { navigate("/peek", "f", true) }
396
-  function handleFocus() { navigate("/", "f", false) }
397
-  function handleShow() { navigate("/", "s", false) }
398
-  function handleIgnore() { navigate("/", "i", false) }
399
-  function handleHide() { navigate("/", "h", false) }
487
+  function handleFocus() { navigate(baseUrl, "f", false) }
488
+  function handleShow() { navigate(baseUrl, "s", false) }
489
+  function handleIgnore() { navigate(baseUrl, "i", false) }
490
+  function handleHide() { navigate(baseUrl, "h", false) }
400 491
 
401 492
   function handleKey(e) {
402 493
     if (e.keyCode != 13) return
@@ -448,7 +539,7 @@ function dotviewer(nodes) {
448 539
     updateButtons()
449 540
   }
450 541
 
451
-  function toggleSelect(elem) {
542
+  function toggleSvgSelect(elem) {
452 543
     // Walk up to immediate child of graph0
453 544
     while (elem != null && elem.parentElement != graph0) {
454 545
       elem = elem.parentElement
@@ -491,6 +582,13 @@ function dotviewer(nodes) {
491 582
   }
492 583
 
493 584
   function setBackground(elem, set) {
585
+    // Handle table row highlighting.
586
+    if (elem.nodeName == "TR") {
587
+      elem.classList.toggle("hilite", set)
588
+      return
589
+    }
590
+
591
+    // Handle svg element highlighting.
494 592
     const p = findPolygon(elem)
495 593
     if (p != null) {
496 594
       if (set) {
@@ -511,6 +609,11 @@ function dotviewer(nodes) {
511 609
     return null
512 610
   }
513 611
 
612
+  // convert a string to a regexp that matches that string.
613
+  function quotemeta(str) {
614
+    return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1')
615
+  }
616
+
514 617
   // Navigate to specified path with current selection reflected
515 618
   // in the named parameter.
516 619
   function navigate(path, param, newWindow) {
@@ -520,7 +623,7 @@ function dotviewer(nodes) {
520 623
     if (!regexpActive) {
521 624
       selected.forEach(function(v, key) {
522 625
         if (re != "") re += "|"
523
-        re += nodes[key]
626
+        re += quotemeta(nodes[key])
524 627
       })
525 628
     }
526 629
 
@@ -547,38 +650,109 @@ function dotviewer(nodes) {
547 650
     }
548 651
   }
549 652
 
653
+  function handleTopClick(e) {
654
+    // Walk back until we find TR and then get the Name column (index 5)
655
+    let elem = e.target
656
+    while (elem != null && elem.nodeName != "TR") {
657
+      elem = elem.parentElement
658
+    }
659
+    if (elem == null || elem.children.length < 6) return
660
+
661
+    e.preventDefault()
662
+    const tr = elem
663
+    const td = elem.children[5]
664
+    if (td.nodeName != "TD") return
665
+    const name = td.innerText
666
+    const index = nodes.indexOf(name)
667
+    if (index < 0) return
668
+
669
+    // Disable regexp mode.
670
+    regexpActive = false
671
+
672
+    if (selected.has(index)) {
673
+      unselect(index, elem)
674
+    } else {
675
+      select(index, elem)
676
+    }
677
+    updateButtons()
678
+  }
679
+
550 680
   function updateButtons() {
551 681
     const enable = (search.value != "" || selected.size != 0)
552 682
     if (buttonsEnabled == enable) return
553 683
     buttonsEnabled = enable
554
-    actionBox.style.display = enable ? "block" : "none"
684
+    for (const id of ["peek", "list", "disasm", "focus", "ignore", "hide", "show"]) {
685
+      const btn = document.getElementById(id)
686
+      if (btn != null) {
687
+        btn.disabled = !enable
688
+      }
689
+    }
555 690
   }
556 691
 
557 692
   // Initialize button states
558 693
   updateButtons()
559 694
 
560 695
   // Setup event handlers
561
-  initPanAndZoom(svg, toggleSelect)
562
-  
563
-  function bindButtons(evt) {
564
-    detailsButton.addEventListener(evt, handleDetails)
565
-    resetButton.addEventListener(evt, handleReset)
566
-    listButton.addEventListener(evt, handleList)
567
-    disasmButton.addEventListener(evt, handleDisasm)
568
-    peekButton.addEventListener(evt, handlePeek)
569
-    focusButton.addEventListener(evt, handleFocus)
570
-    showButton.addEventListener(evt, handleShow)
571
-    ignoreButton.addEventListener(evt, handleIgnore)
572
-    hideButton.addEventListener(evt, handleHide)
573
-  }
574
-  bindButtons("click")
575
-  bindButtons("touchstart")
696
+  if (svg != null) {
697
+    initPanAndZoom(svg, toggleSvgSelect)
698
+  }
699
+  if (toptable != null) {
700
+    toptable.addEventListener("mousedown", handleTopClick)
701
+    toptable.addEventListener("touchstart", handleTopClick)
702
+  }
703
+
704
+  // Bind action to button with specified id.
705
+  function addAction(id, action) {
706
+    const btn = document.getElementById(id)
707
+    if (btn != null) {
708
+      btn.addEventListener("click", action)
709
+      btn.addEventListener("touchstart", action)
710
+    }
711
+  }
712
+
713
+  addAction("details", handleDetails)
714
+  addAction("closedetails", handleCloseDetails)
715
+  addAction("topbtn", handleTop)
716
+  addAction("graphbtn", handleGraph)
717
+  addAction("reset", handleReset)
718
+  addAction("peek", handlePeek)
719
+  addAction("list", handleList)
720
+  addAction("disasm", handleDisasm)
721
+  addAction("focus", handleFocus)
722
+  addAction("ignore", handleIgnore)
723
+  addAction("hide", handleHide)
724
+  addAction("show", handleShow)
725
+
576 726
   search.addEventListener("input", handleSearch)
577 727
   search.addEventListener("keydown", handleKey)
578 728
 }
579
-
580
-dotviewer({{.Nodes}})
581 729
 </script>
730
+{{end}}
731
+
732
+{{define "top" -}}
733
+<!DOCTYPE html>
734
+<html>
735
+<head>
736
+<meta charset="utf-8">
737
+<title>{{.Title}}</title>
738
+{{template "css" .}}
739
+</head>
740
+<body>
741
+
742
+{{template "header" .}}
743
+
744
+<div id="topcontainer">
745
+<table id="toptable">
746
+<tr><th>Flat<th>Flat%<th>Sum%<th>Cum<th>Cum%<th>Name<th>Inlined?</tr>
747
+{{range $i,$e := .Top}}
748
+  <tr id="node{{$i}}"><td>{{$e.Flat}}<td>{{$e.FlatPercent}}<td>{{$e.SumPercent}}<td>{{$e.Cum}}<td>{{$e.CumPercent}}<td>{{$e.Name}}<td>{{$e.InlineLabel}}</tr>
749
+{{end}}
750
+</table>
751
+</div>
752
+
753
+{{template "script" .}}
754
+<script>viewer({{.BaseURL}}, {{.Nodes}})</script>
582 755
 </body>
583 756
 </html>
757
+{{end}}
584 758
 `))

+ 88
- 24
internal/driver/webui.go Переглянути файл

@@ -24,7 +24,6 @@ import (
24 24
 	gourl "net/url"
25 25
 	"os"
26 26
 	"os/exec"
27
-	"regexp"
28 27
 	"strings"
29 28
 	"time"
30 29
 
@@ -52,6 +51,19 @@ func (ec *errorCatcher) PrintErr(args ...interface{}) {
52 51
 	ec.UI.PrintErr(args...)
53 52
 }
54 53
 
54
+// webArgs contains arguments passed to templates in webhtml.go.
55
+type webArgs struct {
56
+	BaseURL string
57
+	Type    string
58
+	Title   string
59
+	Errors  []string
60
+	Legend  []string
61
+	Help    map[string]string
62
+	Nodes   []string
63
+	Svg     template.HTML
64
+	Top     []report.TextItem
65
+}
66
+
55 67
 func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) error {
56 68
 	interactiveMode = true
57 69
 	ui := &webInterface{
@@ -65,6 +77,9 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) e
65 77
 	for n, v := range pprofVariables {
66 78
 		ui.help[n] = v.help
67 79
 	}
80
+	ui.help["details"] = "Show information about the profile and this view"
81
+	ui.help["graph"] = "Display profile as a directed graph"
82
+	ui.help["reset"] = "Show the entire profile"
68 83
 
69 84
 	ln, url, isLocal, err := newListenerAndURL(hostport)
70 85
 	if err != nil {
@@ -84,6 +99,7 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) e
84 99
 
85 100
 	mux := http.NewServeMux()
86 101
 	mux.Handle("/", wrap(http.HandlerFunc(ui.dot)))
102
+	mux.Handle("/top", wrap(http.HandlerFunc(ui.top)))
87 103
 	mux.Handle("/disasm", wrap(http.HandlerFunc(ui.disasm)))
88 104
 	mux.Handle("/weblist", wrap(http.HandlerFunc(ui.weblist)))
89 105
 	mux.Handle("/peek", wrap(http.HandlerFunc(ui.peek)))
@@ -162,6 +178,15 @@ func checkLocalHost(h http.Handler) http.Handler {
162 178
 	})
163 179
 }
164 180
 
181
+func varsFromURL(u *gourl.URL) variables {
182
+	vars := pprofVariables.makeCopy()
183
+	vars["focus"].value = u.Query().Get("f")
184
+	vars["show"].value = u.Query().Get("s")
185
+	vars["ignore"].value = u.Query().Get("i")
186
+	vars["hide"].value = u.Query().Get("h")
187
+	return vars
188
+}
189
+
165 190
 // dot generates a web page containing an svg diagram.
166 191
 func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
167 192
 	if req.URL.Path != "/" {
@@ -176,11 +201,7 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
176 201
 
177 202
 	// Generate dot graph.
178 203
 	args := []string{"svg"}
179
-	vars := pprofVariables.makeCopy()
180
-	vars["focus"].value = req.URL.Query().Get("f")
181
-	vars["show"].value = req.URL.Query().Get("s")
182
-	vars["ignore"].value = req.URL.Query().Get("i")
183
-	vars["hide"].value = req.URL.Query().Get("h")
204
+	vars := varsFromURL(req.URL)
184 205
 	_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
185 206
 	if err != nil {
186 207
 		http.Error(w, err.Error(), http.StatusBadRequest)
@@ -202,32 +223,27 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
202 223
 		return
203 224
 	}
204 225
 
205
-	// Get regular expression for each node.
206
-	nodes := []string{""}
226
+	// Get all node names into an array.
227
+	nodes := []string{""} // dot starts with node numbered 1
207 228
 	for _, n := range g.Nodes {
208
-		nodes = append(nodes, regexp.QuoteMeta(n.Info.Name))
229
+		nodes = append(nodes, n.Info.Name)
209 230
 	}
210 231
 
211 232
 	// Embed in html.
212 233
 	file := getFromLegend(legend, "File: ", "unknown")
213 234
 	profile := getFromLegend(legend, "Type: ", "unknown")
214
-	data := struct {
215
-		Title  string
216
-		Errors []string
217
-		Svg    template.HTML
218
-		Legend []string
219
-		Nodes  []string
220
-		Help   map[string]string
221
-	}{
222
-		Title:  file + " " + profile,
223
-		Errors: catcher.errors,
224
-		Svg:    template.HTML(string(svg)),
225
-		Legend: legend,
226
-		Nodes:  nodes,
227
-		Help:   ui.help,
235
+	data := webArgs{
236
+		BaseURL: "/",
237
+		Type:    "dot",
238
+		Title:   file + " " + profile,
239
+		Errors:  catcher.errors,
240
+		Svg:     template.HTML(string(svg)),
241
+		Legend:  legend,
242
+		Nodes:   nodes,
243
+		Help:    ui.help,
228 244
 	}
229 245
 	html := &bytes.Buffer{}
230
-	if err := graphTemplate.Execute(html, data); err != nil {
246
+	if err := webTemplate.ExecuteTemplate(html, "graph", data); err != nil {
231 247
 		http.Error(w, "internal template error", http.StatusInternalServerError)
232 248
 		ui.options.UI.PrintErr(err)
233 249
 		return
@@ -254,6 +270,54 @@ func dotToSvg(dot []byte) ([]byte, error) {
254 270
 	return svg, nil
255 271
 }
256 272
 
273
+func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
274
+	// Capture any error messages generated while generating a report.
275
+	catcher := &errorCatcher{UI: ui.options.UI}
276
+	options := *ui.options
277
+	options.UI = catcher
278
+
279
+	// Generate top report
280
+	args := []string{"top"}
281
+	vars := varsFromURL(req.URL)
282
+	vars["nodecount"].value = "500"
283
+	_, rpt, err := generateRawReport(ui.prof, args, vars, &options)
284
+	if err != nil {
285
+		http.Error(w, err.Error(), http.StatusBadRequest)
286
+		ui.options.UI.PrintErr(err)
287
+		return
288
+	}
289
+
290
+	top, legend := report.TextItems(rpt)
291
+
292
+	// Get all node names into an array.
293
+	var nodes []string
294
+	for _, item := range top {
295
+		nodes = append(nodes, item.Name)
296
+	}
297
+
298
+	// Embed in html.
299
+	file := getFromLegend(legend, "File: ", "unknown")
300
+	profile := getFromLegend(legend, "Type: ", "unknown")
301
+	data := webArgs{
302
+		BaseURL: "/top",
303
+		Type:    "top",
304
+		Title:   file + " " + profile,
305
+		Errors:  catcher.errors,
306
+		Legend:  legend,
307
+		Help:    ui.help,
308
+		Top:     top,
309
+		Nodes:   nodes,
310
+	}
311
+	html := &bytes.Buffer{}
312
+	if err := webTemplate.ExecuteTemplate(html, "top", data); err != nil {
313
+		http.Error(w, "internal template error", http.StatusInternalServerError)
314
+		ui.options.UI.PrintErr(err)
315
+		return
316
+	}
317
+	w.Header().Set("Content-Type", "text/html")
318
+	w.Write(html.Bytes())
319
+}
320
+
257 321
 // disasm generates a web page containing disassembly.
258 322
 func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
259 323
 	ui.output(w, req, "disasm", "text/plain", pprofVariables.makeCopy())

+ 3
- 0
internal/driver/webui_test.go Переглянути файл

@@ -44,6 +44,8 @@ func TestWebInterface(t *testing.T) {
44 44
 			switch r.URL.Path {
45 45
 			case "/":
46 46
 				ui.dot(w, r)
47
+			case "/top":
48
+				ui.top(w, r)
47 49
 			case "/disasm":
48 50
 				ui.disasm(w, r)
49 51
 			case "/peek":
@@ -66,6 +68,7 @@ func TestWebInterface(t *testing.T) {
66 68
 	}
67 69
 	testcases := []testCase{
68 70
 		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
71
+		{"/top", []string{"Flat", "200ms.*100%.*F2"}, false},
69 72
 		{"/weblist?f=" + url.QueryEscape("F[12]"),
70 73
 			[]string{"F1", "F2", "300ms line1"}, false},
71 74
 		{"/peek?f=" + url.QueryEscape("F[12]"),

+ 46
- 16
internal/report/report.go Переглянути файл

@@ -683,16 +683,23 @@ func printComments(w io.Writer, rpt *Report) error {
683 683
 	return nil
684 684
 }
685 685
 
686
-// printText prints a flat text report for a profile.
687
-func printText(w io.Writer, rpt *Report) error {
686
+// TextItem holds a single text report entry.
687
+type TextItem struct {
688
+	Name              string
689
+	InlineLabel       string // Not empty if inlined
690
+	Flat, FlatPercent string
691
+	SumPercent        string
692
+	Cum, CumPercent   string
693
+}
694
+
695
+// TextItems returns a list of text items from the report and a list
696
+// of labels that describe the report.
697
+func TextItems(rpt *Report) ([]TextItem, []string) {
688 698
 	g, origCount, droppedNodes, _ := rpt.newTrimmedGraph()
689 699
 	rpt.selectOutputUnit(g)
700
+	labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false)
690 701
 
691
-	fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n"))
692
-
693
-	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
694
-		"flat", "flat", "sum", "cum", "cum")
695
-
702
+	var items []TextItem
696 703
 	var flatSum int64
697 704
 	for _, n := range g.Nodes {
698 705
 		name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue()
@@ -706,22 +713,45 @@ func printText(w io.Writer, rpt *Report) error {
706 713
 			}
707 714
 		}
708 715
 
716
+		var inl string
709 717
 		if inline {
710 718
 			if noinline {
711
-				name = name + " (partial-inline)"
719
+				inl = "(partial-inline)"
712 720
 			} else {
713
-				name = name + " (inline)"
721
+				inl = "(inline)"
714 722
 			}
715 723
 		}
716 724
 
717 725
 		flatSum += flat
718
-		fmt.Fprintf(w, "%10s %s %s %10s %s  %s\n",
719
-			rpt.formatValue(flat),
720
-			percentage(flat, rpt.total),
721
-			percentage(flatSum, rpt.total),
722
-			rpt.formatValue(cum),
723
-			percentage(cum, rpt.total),
724
-			name)
726
+		items = append(items, TextItem{
727
+			Name:        name,
728
+			InlineLabel: inl,
729
+			Flat:        rpt.formatValue(flat),
730
+			FlatPercent: percentage(flat, rpt.total),
731
+			SumPercent:  percentage(flatSum, rpt.total),
732
+			Cum:         rpt.formatValue(cum),
733
+			CumPercent:  percentage(cum, rpt.total),
734
+		})
735
+	}
736
+	return items, labels
737
+}
738
+
739
+// printText prints a flat text report for a profile.
740
+func printText(w io.Writer, rpt *Report) error {
741
+	items, labels := TextItems(rpt)
742
+	fmt.Fprintln(w, strings.Join(labels, "\n"))
743
+	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
744
+		"flat", "flat", "sum", "cum", "cum")
745
+	for _, item := range items {
746
+		inl := item.InlineLabel
747
+		if inl != "" {
748
+			inl = " " + inl
749
+		}
750
+		fmt.Fprintf(w, "%10s %s %s %10s %s  %s%s\n",
751
+			item.Flat, item.FlatPercent,
752
+			item.SumPercent,
753
+			item.Cum, item.CumPercent,
754
+			item.Name, inl)
725 755
 	}
726 756
 	return nil
727 757
 }