Browse Source

Make top table sortable by column (#220)

* Make top table sortable.

* Recompute Sum% column contents after sorting.

* Cleanups identified during review.

* Simplify top table sorting by generating it in Javascript.
Sanjay Ghemawat 7 years ago
parent
commit
15340c8ac9
4 changed files with 137 additions and 30 deletions
  1. 108
    9
      internal/driver/webhtml.go
  2. 8
    6
      internal/driver/webui.go
  3. 5
    2
      internal/driver/webui_test.go
  4. 16
    13
      internal/report/report.go

+ 108
- 9
internal/driver/webhtml.go View File

142
 #toptable {
142
 #toptable {
143
   border-spacing: 0px;
143
   border-spacing: 0px;
144
   width: 100%;
144
   width: 100%;
145
+  padding-bottom: 1em;
145
 }
146
 }
146
 #toptable tr th {
147
 #toptable tr th {
147
   border-bottom: 1px solid black;
148
   border-bottom: 1px solid black;
148
   text-align: right;
149
   text-align: right;
149
   padding-left: 1em;
150
   padding-left: 1em;
151
+  padding-top: 0.2em;
152
+  padding-bottom: 0.2em;
150
 }
153
 }
151
-#toptable tr th:nth-child(6) { text-align: left; }
152
-#toptable tr th:nth-child(7) { text-align: left; }
153
 #toptable tr td {
154
 #toptable tr td {
154
   padding-left: 1em;
155
   padding-left: 1em;
155
   font: monospace;
156
   font: monospace;
157
   white-space: nowrap;
158
   white-space: nowrap;
158
   cursor: default;
159
   cursor: default;
159
 }
160
 }
160
-#toptable tr td:nth-child(6) {
161
+#toptable tr th:nth-child(6),
162
+#toptable tr th:nth-child(7),
163
+#toptable tr td:nth-child(6),
164
+#toptable tr td:nth-child(7) {
161
   text-align: left;
165
   text-align: left;
166
+}
167
+#toptable tr td:nth-child(6) {
162
   max-width: 30em;  // Truncate very long names
168
   max-width: 30em;  // Truncate very long names
163
   overflow: hidden;
169
   overflow: hidden;
164
 }
170
 }
165
-#toptable tr td:nth-child(7) { text-align: left; }
171
+#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
172
+  cursor: ns-resize;
173
+}
166
 .hilite {
174
 .hilite {
167
   background-color: #ccf;
175
   background-color: #ccf;
168
 }
176
 }
741
 <meta charset="utf-8">
749
 <meta charset="utf-8">
742
 <title>{{.Title}}</title>
750
 <title>{{.Title}}</title>
743
 {{template "css" .}}
751
 {{template "css" .}}
752
+<style type="text/css">
753
+</style>
744
 </head>
754
 </head>
745
 <body>
755
 <body>
746
 
756
 
748
 
758
 
749
 <div id="bodycontainer">
759
 <div id="bodycontainer">
750
 <table id="toptable">
760
 <table id="toptable">
751
-<tr><th>Flat<th>Flat%<th>Sum%<th>Cum<th>Cum%<th>Name<th>Inlined?</tr>
752
-{{range $i,$e := .Top}}
753
-  <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>
754
-{{end}}
761
+<tr>
762
+<th id="flathdr1">Flat
763
+<th id="flathdr2">Flat%
764
+<th>Sum%
765
+<th id="cumhdr1">Cum
766
+<th id="cumhdr2">Cum%
767
+<th id="namehdr">Name
768
+<th>Inlined?</tr>
769
+<tbody id="rows">
770
+</tbody>
755
 </table>
771
 </table>
756
 </div>
772
 </div>
757
 
773
 
758
 {{template "script" .}}
774
 {{template "script" .}}
759
-<script>viewer({{.BaseURL}}, {{.Nodes}})</script>
775
+<script>
776
+function makeTopTable(total, entries) {
777
+  const rows = document.getElementById("rows")
778
+  if (rows == null) return
779
+
780
+  // Store initial index in each entry so we have stable node ids for selection.
781
+  for (let i = 0; i < entries.length; i++) {
782
+    entries[i].Id = "node" + i
783
+  }
784
+
785
+  // Which column are we currently sorted by and in what order?
786
+  let currentColumn = ""
787
+  let descending = false
788
+  sortBy("Flat")
789
+
790
+  function sortBy(column) {
791
+    // Update sort criteria
792
+    if (column == currentColumn) {
793
+      descending = !descending  // Reverse order
794
+    } else {
795
+      currentColumn = column
796
+      descending = (column != "Name")
797
+    }
798
+
799
+    // Sort according to current criteria.
800
+    function cmp(a, b) {
801
+      const av = a[currentColumn]
802
+      const bv = b[currentColumn]
803
+      if (av < bv) return -1
804
+      if (av > bv) return +1
805
+      return 0
806
+    }
807
+    entries.sort(cmp)
808
+    if (descending) entries.reverse()
809
+
810
+    function addCell(tr, val) {
811
+      const td = document.createElement('td')
812
+      td.textContent = val
813
+      tr.appendChild(td)
814
+    }
815
+
816
+    function percent(v) {
817
+      return (v * 100.0 / total).toFixed(2) + "%"
818
+    }
819
+
820
+    // Generate rows
821
+    const fragment = document.createDocumentFragment()
822
+    let sum = 0
823
+    for (const row of entries) {
824
+      const tr = document.createElement('tr')
825
+      tr.id = row.Id
826
+      sum += row.Flat
827
+      addCell(tr, row.FlatFormat)
828
+      addCell(tr, percent(row.Flat))
829
+      addCell(tr, percent(sum))
830
+      addCell(tr, row.CumFormat)
831
+      addCell(tr, percent(row.Cum))
832
+      addCell(tr, row.Name)
833
+      addCell(tr, row.InlineLabel)
834
+      fragment.appendChild(tr)
835
+    }
836
+
837
+    rows.textContent = ''  // Remove old rows
838
+    rows.appendChild(fragment)
839
+  }
840
+
841
+  // Make different column headers trigger sorting.
842
+  function bindSort(id, column) {
843
+    const hdr = document.getElementById(id)
844
+    if (hdr == null) return
845
+    const fn = function() { sortBy(column) }
846
+    hdr.addEventListener("click", fn)
847
+    hdr.addEventListener("touch", fn)
848
+  }
849
+  bindSort("flathdr1", "Flat")
850
+  bindSort("flathdr2", "Flat")
851
+  bindSort("cumhdr1", "Cum")
852
+  bindSort("cumhdr2", "Cum")
853
+  bindSort("namehdr", "Name")
854
+}
855
+
856
+viewer({{.BaseURL}}, {{.Nodes}})
857
+makeTopTable({{.Total}}, {{.Top}})
858
+</script>
760
 </body>
859
 </body>
761
 </html>
860
 </html>
762
 {{end}}
861
 {{end}}

+ 8
- 6
internal/driver/webui.go View File

71
 	BaseURL  string
71
 	BaseURL  string
72
 	Title    string
72
 	Title    string
73
 	Errors   []string
73
 	Errors   []string
74
+	Total    int64
74
 	Legend   []string
75
 	Legend   []string
75
 	Help     map[string]string
76
 	Help     map[string]string
76
 	Nodes    []string
77
 	Nodes    []string
219
 
220
 
220
 // render generates html using the named template based on the contents of data.
221
 // render generates html using the named template based on the contents of data.
221
 func (ui *webInterface) render(w http.ResponseWriter, baseURL, tmpl string,
222
 func (ui *webInterface) render(w http.ResponseWriter, baseURL, tmpl string,
222
-	errList, legend []string, data webArgs) {
223
+	rpt *report.Report, errList, legend []string, data webArgs) {
223
 	file := getFromLegend(legend, "File: ", "unknown")
224
 	file := getFromLegend(legend, "File: ", "unknown")
224
 	profile := getFromLegend(legend, "Type: ", "unknown")
225
 	profile := getFromLegend(legend, "Type: ", "unknown")
225
 	data.BaseURL = baseURL
226
 	data.BaseURL = baseURL
226
 	data.Title = file + " " + profile
227
 	data.Title = file + " " + profile
227
 	data.Errors = errList
228
 	data.Errors = errList
229
+	data.Total = rpt.Total()
228
 	data.Legend = legend
230
 	data.Legend = legend
229
 	data.Help = ui.help
231
 	data.Help = ui.help
230
 	html := &bytes.Buffer{}
232
 	html := &bytes.Buffer{}
272
 		nodes = append(nodes, n.Info.Name)
274
 		nodes = append(nodes, n.Info.Name)
273
 	}
275
 	}
274
 
276
 
275
-	ui.render(w, "/", "graph", errList, legend, webArgs{
277
+	ui.render(w, "/", "graph", rpt, errList, legend, webArgs{
276
 		HTMLBody: template.HTML(string(svg)),
278
 		HTMLBody: template.HTML(string(svg)),
277
 		Nodes:    nodes,
279
 		Nodes:    nodes,
278
 	})
280
 	})
307
 		nodes = append(nodes, item.Name)
309
 		nodes = append(nodes, item.Name)
308
 	}
310
 	}
309
 
311
 
310
-	ui.render(w, "/top", "top", errList, legend, webArgs{
312
+	ui.render(w, "/top", "top", rpt, errList, legend, webArgs{
311
 		Top:   top,
313
 		Top:   top,
312
 		Nodes: nodes,
314
 		Nodes: nodes,
313
 	})
315
 	})
329
 	}
331
 	}
330
 
332
 
331
 	legend := report.ProfileLabels(rpt)
333
 	legend := report.ProfileLabels(rpt)
332
-	ui.render(w, "/disasm", "plaintext", errList, legend, webArgs{
334
+	ui.render(w, "/disasm", "plaintext", rpt, errList, legend, webArgs{
333
 		TextBody: out.String(),
335
 		TextBody: out.String(),
334
 	})
336
 	})
335
 
337
 
353
 	}
355
 	}
354
 
356
 
355
 	legend := report.ProfileLabels(rpt)
357
 	legend := report.ProfileLabels(rpt)
356
-	ui.render(w, "/source", "sourcelisting", errList, legend, webArgs{
358
+	ui.render(w, "/source", "sourcelisting", rpt, errList, legend, webArgs{
357
 		HTMLBody: template.HTML(body.String()),
359
 		HTMLBody: template.HTML(body.String()),
358
 	})
360
 	})
359
 }
361
 }
374
 	}
376
 	}
375
 
377
 
376
 	legend := report.ProfileLabels(rpt)
378
 	legend := report.ProfileLabels(rpt)
377
-	ui.render(w, "/peek", "plaintext", errList, legend, webArgs{
379
+	ui.render(w, "/peek", "plaintext", rpt, errList, legend, webArgs{
378
 		TextBody: out.String(),
380
 		TextBody: out.String(),
379
 	})
381
 	})
380
 }
382
 }

+ 5
- 2
internal/driver/webui_test.go View File

32
 
32
 
33
 func TestWebInterface(t *testing.T) {
33
 func TestWebInterface(t *testing.T) {
34
 	prof := makeFakeProfile()
34
 	prof := makeFakeProfile()
35
-	ui := makeWebInterface(prof, &plugin.Options{Obj: fakeObjTool{}})
35
+	ui := makeWebInterface(prof, &plugin.Options{
36
+		Obj: fakeObjTool{},
37
+		UI:  &stdUI{},
38
+	})
36
 
39
 
37
 	// Start test server.
40
 	// Start test server.
38
 	server := httptest.NewServer(http.HandlerFunc(
41
 	server := httptest.NewServer(http.HandlerFunc(
64
 	}
67
 	}
65
 	testcases := []testCase{
68
 	testcases := []testCase{
66
 		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
69
 		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
67
-		{"/top", []string{"Flat", "200ms.*100%.*F2"}, false},
70
+		{"/top", []string{`"Name":"F2","InlineLabel":"","Flat":200,"Cum":300,"FlatFormat":"200ms","CumFormat":"300ms"}`}, false},
68
 		{"/source?f=" + url.QueryEscape("F[12]"),
71
 		{"/source?f=" + url.QueryEscape("F[12]"),
69
 			[]string{"F1", "F2", "300ms line1"}, false},
72
 			[]string{"F1", "F2", "300ms line1"}, false},
70
 		{"/peek?f=" + url.QueryEscape("F[12]"),
73
 		{"/peek?f=" + url.QueryEscape("F[12]"),

+ 16
- 13
internal/report/report.go View File

704
 
704
 
705
 // TextItem holds a single text report entry.
705
 // TextItem holds a single text report entry.
706
 type TextItem struct {
706
 type TextItem struct {
707
-	Name              string
708
-	InlineLabel       string // Not empty if inlined
709
-	Flat, FlatPercent string
710
-	SumPercent        string
711
-	Cum, CumPercent   string
707
+	Name                  string
708
+	InlineLabel           string // Not empty if inlined
709
+	Flat, Cum             int64  // Raw values
710
+	FlatFormat, CumFormat string // Formatted values
712
 }
711
 }
713
 
712
 
714
 // TextItems returns a list of text items from the report and a list
713
 // TextItems returns a list of text items from the report and a list
745
 		items = append(items, TextItem{
744
 		items = append(items, TextItem{
746
 			Name:        name,
745
 			Name:        name,
747
 			InlineLabel: inl,
746
 			InlineLabel: inl,
748
-			Flat:        rpt.formatValue(flat),
749
-			FlatPercent: percentage(flat, rpt.total),
750
-			SumPercent:  percentage(flatSum, rpt.total),
751
-			Cum:         rpt.formatValue(cum),
752
-			CumPercent:  percentage(cum, rpt.total),
747
+			Flat:        flat,
748
+			Cum:         cum,
749
+			FlatFormat:  rpt.formatValue(flat),
750
+			CumFormat:   rpt.formatValue(cum),
753
 		})
751
 		})
754
 	}
752
 	}
755
 	return items, labels
753
 	return items, labels
761
 	fmt.Fprintln(w, strings.Join(labels, "\n"))
759
 	fmt.Fprintln(w, strings.Join(labels, "\n"))
762
 	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
760
 	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
763
 		"flat", "flat", "sum", "cum", "cum")
761
 		"flat", "flat", "sum", "cum", "cum")
762
+	var flatSum int64
764
 	for _, item := range items {
763
 	for _, item := range items {
765
 		inl := item.InlineLabel
764
 		inl := item.InlineLabel
766
 		if inl != "" {
765
 		if inl != "" {
767
 			inl = " " + inl
766
 			inl = " " + inl
768
 		}
767
 		}
768
+		flatSum += item.Flat
769
 		fmt.Fprintf(w, "%10s %s %s %10s %s  %s%s\n",
769
 		fmt.Fprintf(w, "%10s %s %s %10s %s  %s%s\n",
770
-			item.Flat, item.FlatPercent,
771
-			item.SumPercent,
772
-			item.Cum, item.CumPercent,
770
+			item.FlatFormat, percentage(item.Flat, rpt.total),
771
+			percentage(flatSum, rpt.total),
772
+			item.CumFormat, percentage(item.Cum, rpt.total),
773
 			item.Name, inl)
773
 			item.Name, inl)
774
 	}
774
 	}
775
 	return nil
775
 	return nil
1239
 	formatValue func(int64) string
1239
 	formatValue func(int64) string
1240
 }
1240
 }
1241
 
1241
 
1242
+// Total returns the total number of samples in a report.
1243
+func (rpt *Report) Total() int64 { return rpt.total }
1244
+
1242
 func abs64(i int64) int64 {
1245
 func abs64(i int64) int64 {
1243
 	if i < 0 {
1246
 	if i < 0 {
1244
 		return -i
1247
 		return -i