analysis/appengine/template: improve label display
The display now shows the top N labels, and shows common labels
separately. Each label is a link that filters the results based on
that label.
Also fixes a typo and removes a harmless double Close.
Change-Id: I25b93c7bbfd584ad345c4508e64cd5db73298745
Reviewed-on: https://go-review.googlesource.com/35675
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/analysis/app/compare.go b/analysis/app/compare.go
index 073ba5c..4428a3d 100644
--- a/analysis/app/compare.go
+++ b/analysis/app/compare.go
@@ -10,6 +10,7 @@
"io/ioutil"
"net/http"
"sort"
+ "strings"
"golang.org/x/perf/analysis/internal/benchstat"
"golang.org/x/perf/storage/benchfmt"
@@ -20,21 +21,30 @@
// Raw list of results.
results []*benchfmt.Result
// LabelValues is the count of results found with each distinct (key, value) pair found in labels.
- LabelValues map[string]map[string]int
+ // A value of "" counts results missing that key.
+ LabelValues map[string]valueSet
}
// add adds res to the resultGroup.
func (g *resultGroup) add(res *benchfmt.Result) {
g.results = append(g.results, res)
if g.LabelValues == nil {
- g.LabelValues = make(map[string]map[string]int)
+ g.LabelValues = make(map[string]valueSet)
}
for k, v := range res.Labels {
if g.LabelValues[k] == nil {
- g.LabelValues[k] = make(map[string]int)
+ g.LabelValues[k] = make(valueSet)
+ if len(g.results) > 1 {
+ g.LabelValues[k][""] = len(g.results) - 1
+ }
}
g.LabelValues[k][v]++
}
+ for k := range g.LabelValues {
+ if res.Labels[k] == "" {
+ g.LabelValues[k][""]++
+ }
+ }
}
// splitOn returns a new set of groups sharing a common value for key.
@@ -58,6 +68,57 @@
return out
}
+// valueSet is a set of values and the number of results with each value.
+type valueSet map[string]int
+
+// valueCount and byCount are used for sorting a valueSet
+type valueCount struct {
+ Value string
+ Count int
+}
+type byCount []valueCount
+
+func (s byCount) Len() int { return len(s) }
+func (s byCount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+func (s byCount) Less(i, j int) bool {
+ if s[i].Count != s[j].Count {
+ return s[i].Count > s[j].Count
+ }
+ return s[i].Value < s[j].Value
+}
+
+// TopN returns a slice containing n valueCount entries, and if any labels were omitted, an extra entry with value "…".
+func (vs valueSet) TopN(n int) []valueCount {
+ var s []valueCount
+ var total int
+ for v, count := range vs {
+ s = append(s, valueCount{v, count})
+ total += count
+ }
+ sort.Sort(byCount(s))
+ out := s
+ if len(out) > n {
+ out = s[:n]
+ }
+ if len(out) < len(s) {
+ var outTotal int
+ for _, vc := range out {
+ outTotal += vc.Count
+ }
+ out = append(out, valueCount{"…", total - outTotal})
+ }
+ return out
+}
+
+// addToQuery returns a new query string with add applied as a filter.
+func addToQuery(query, add string) string {
+ if strings.Contains(query, "|") {
+ return add + " " + query
+ }
+ return add + " | " + query
+}
+
// compare handles queries that require comparison of the groups in the query.
func (a *App) compare(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
@@ -73,7 +134,9 @@
return
}
- t, err := template.New("main").Parse(string(tmpl))
+ t, err := template.New("main").Funcs(template.FuncMap{
+ "addToQuery": addToQuery,
+ }).Parse(string(tmpl))
if err != nil {
http.Error(w, err.Error(), 500)
return
@@ -89,11 +152,12 @@
}
type compareData struct {
- Q string
- Error string
- Benchstat template.HTML
- Groups []*resultGroup
- Labels map[string]bool
+ Q string
+ Error string
+ Benchstat template.HTML
+ Groups []*resultGroup
+ Labels map[string]bool
+ CommonLabels benchfmt.Labels
}
func (a *App) compareQuery(q string) *compareData {
@@ -107,7 +171,6 @@
for _, qPart := range queries {
group := &resultGroup{}
res := a.StorageClient.Query(qPart)
- defer res.Close() // TODO: Should happen each time through the loop
for res.Next() {
group.add(res.Result())
found++
@@ -128,7 +191,7 @@
return &compareData{
Q: q,
Error: "No results matched the query string.",
- }, nil
+ }
}
// Attempt to automatically split results.
@@ -150,18 +213,42 @@
HTML: true,
})
- // Render template.
+ // Prepare struct for template.
labels := make(map[string]bool)
+ // commonLabels are the key: value of every label that has an
+ // identical value on every result.
+ commonLabels := make(benchfmt.Labels)
+ // Scan the first group for common labels.
+ for k, vs := range groups[0].LabelValues {
+ if len(vs) == 1 {
+ for v := range vs {
+ commonLabels[k] = v
+ }
+ }
+ }
+ // Remove any labels not common in later groups.
+ for _, g := range groups[1:] {
+ for k, v := range commonLabels {
+ if len(g.LabelValues[k]) != 1 || g.LabelValues[k][v] == 0 {
+ delete(commonLabels, k)
+ }
+ }
+ }
+ // List all labels present and not in commonLabels.
for _, g := range groups {
for k := range g.LabelValues {
+ if commonLabels[k] != "" {
+ continue
+ }
labels[k] = true
}
}
data := &compareData{
- Q: q,
- Benchstat: template.HTML(buf.String()),
- Groups: groups,
- Labels: labels,
+ Q: q,
+ Benchstat: template.HTML(buf.String()),
+ Groups: groups,
+ Labels: labels,
+ CommonLabels: commonLabels,
}
return data
}
diff --git a/analysis/app/compare_test.go b/analysis/app/compare_test.go
index 301fd0e..f7f92e9 100644
--- a/analysis/app/compare_test.go
+++ b/analysis/app/compare_test.go
@@ -34,7 +34,7 @@
if !reflect.DeepEqual(g.results, results) {
t.Errorf("g.results = %#v, want %#v", g.results, results)
}
- if want := map[string]map[string]int{"key": {"value": 1, "value2": 1}}; !reflect.DeepEqual(g.LabelValues, want) {
+ if want := map[string]valueSet{"key": {"value": 1, "value2": 1}}; !reflect.DeepEqual(g.LabelValues, want) {
t.Errorf("g.LabelValues = %#v, want %#v", g.LabelValues, want)
}
groups := g.splitOn("key")
@@ -89,9 +89,9 @@
for _, q := range []string{"one vs two", "onetwo"} {
t.Run(q, func(t *testing.T) {
- data, err := a.compareQuery(q)
- if err != nil {
- t.Fatalf("compareQuery failed: %v", err)
+ data := a.compareQuery(q)
+ if data.Error != "" {
+ t.Fatalf("compareQuery failed: %s", data.Error)
}
if have := data.Q; have != q {
t.Errorf("Q = %q, want %q", have, q)
@@ -102,9 +102,12 @@
if len(data.Benchstat) == 0 {
t.Error("len(Benchstat) = 0, want >0")
}
- if want := map[string]bool{"upload": true, "upload-part": true, "label": true}; !reflect.DeepEqual(data.Labels, want) {
+ if want := map[string]bool{"upload-part": true, "label": true}; !reflect.DeepEqual(data.Labels, want) {
t.Errorf("Labels = %#v, want %#v", data.Labels, want)
}
+ if want := (benchfmt.Labels{"upload": "1"}); !reflect.DeepEqual(data.CommonLabels, want) {
+ t.Errorf("CommonLabels = %#v, want %#v", data.CommonLabels, want)
+ }
})
}
}
diff --git a/analysis/appengine/template/compare.html b/analysis/appengine/template/compare.html
index 741ca6e..2725d72 100644
--- a/analysis/appengine/template/compare.html
+++ b/analysis/appengine/template/compare.html
@@ -2,46 +2,137 @@
<html>
<head>
<title>Performance Result Comparison</title>
+ <style type="text/css">
+#header h1 {
+ display: inline;
+}
+#search {
+ padding: 1em .5em;
+ width: 100%;
+}
+input[type="text"] {
+ font-size: 100%;
+}
+#results {
+ border-top: 1px solid black;
+}
+tr.diff td {
+ font-size: 80%;
+ font-family: sans-serif;
+ vertical-align: top;
+}
+th.label {
+ text-align: left;
+ vertical-align: top;
+}
+td.count {
+ text-align: right;
+}
+#labels {
+ float: left;
+ margin-right: 1em;
+ border-right: 1px solid black;
+ border-collapse: collapse;
+ vertical-align: top;
+}
+#labels tbody {
+ border-collapse: collapse;
+ border-bottom: 1px solid black;
+}
+table.benchstat {
+ border-collapse: collapse;
+}
+table.benchstat td, table.benchstat th {
+ padding-right: 2px;
+ padding-bottom: 2px;
+}
+#labels > tbody > tr:last-child th, #labels > tbody > tr:last-child td {
+ padding-bottom: 1em;
+}
+#labels tbody tr:first-child th, #benchstat {
+ padding-top: 1em;
+}
+#labels tbody.diff tr:first-child th {
+ padding-top: 1em;
+ border-collapse: collapse;
+ border-top: 1px solid black;
+}
+#labels .diff {
+ padding-bottom: 1em;
+}
+ </style>
</head>
<body>
- <div>
+ <div id="header">
+ <h1>Go Performance Dashboard</h1>
+ <a href="/">about</a>
+ </div>
+ <div id="search">
<form action="/search">
<input type="text" name="q" value="{{.Q}}" size="120">
<input type="submit" value="Search">
</form>
</div>
- {{with .Error}}
- <p>{{.}}</p>
- {{else}}
- <div>
- {{.Benchstat}}
- </div>
- <table>
- <tr>
- <th>label</th>
- {{range $index, $group := .Groups}}
- <th>
- #{{$index}}
- </th>
- {{end}}
- </tr>
- {{range $label, $exists := .Labels}}
- <tr>
- <th>{{$label}}</th>
- {{range $.Groups}}
- <td>
- {{with index .LabelValues $label}}
- [
- {{range $value, $exists := .}}
- {{printf "%q" $value}}
+ <div id="results">
+ {{with .Error}}
+ <p>{{.}}</p>
+ {{else}}
+ <table id="labels">
+ {{with .CommonLabels}}
+ <tbody>
+ <tr>
+ <th>label</th><th>common value</th>
+ </tr>
+ {{range $label, $value := .}}
+ <tr>
+ <th class="label">{{$label}}</th><td>{{$value}}</td>
+ </tr>
{{end}}
- ]
- {{end}}
- </td>
+ </tbody>
{{end}}
- </tr>
- {{end}}
- </table>
- {{end}}
+ <tbody class="diff">
+ <tr>
+ <th>label</th>
+ <th>values</th>
+ </tr>
+ {{range $label, $exists := .Labels}}
+ <tr class="diff">
+ <th class="label">{{$label}}</th>
+ <td>
+ {{range $index, $group := $.Groups}}
+ Query {{$index}}:
+ <table>
+ {{with index $group.LabelValues $label}}
+ {{range .TopN 4}}
+ <tr>
+ <td class="count">
+ {{.Count}}
+ </td>
+ <td>
+ {{if eq .Value ""}}
+ missing
+ {{else if eq .Value "…"}}
+ {{.Value}}
+ {{else}}
+ <a href="/search?q={{addToQuery $.Q (printf "%s:%s" $label .Value)}}">
+ {{printf "%q" .Value}}
+ </a>
+ {{end}}
+ </td>
+ </tr>
+ {{end}}
+ {{end}}
+ </table>
+ {{end}}
+ </td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ <div id="benchstat">
+ {{.Benchstat}}
+ </div>
+ {{end}}
+ </div>
</body>
</html>