Commit bb62666d authored by Quentin Smith's avatar Quentin Smith Committed by Russ Cox

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/35675Reviewed-by: default avatarRuss Cox <rsc@golang.org>
parent c47bedaa
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "sort"
"strings"
"golang.org/x/perf/analysis/internal/benchstat" "golang.org/x/perf/analysis/internal/benchstat"
"golang.org/x/perf/storage/benchfmt" "golang.org/x/perf/storage/benchfmt"
...@@ -20,21 +21,30 @@ type resultGroup struct { ...@@ -20,21 +21,30 @@ type resultGroup struct {
// Raw list of results. // Raw list of results.
results []*benchfmt.Result results []*benchfmt.Result
// LabelValues is the count of results found with each distinct (key, value) pair found in labels. // 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. // add adds res to the resultGroup.
func (g *resultGroup) add(res *benchfmt.Result) { func (g *resultGroup) add(res *benchfmt.Result) {
g.results = append(g.results, res) g.results = append(g.results, res)
if g.LabelValues == nil { if g.LabelValues == nil {
g.LabelValues = make(map[string]map[string]int) g.LabelValues = make(map[string]valueSet)
} }
for k, v := range res.Labels { for k, v := range res.Labels {
if g.LabelValues[k] == nil { 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]++ 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. // splitOn returns a new set of groups sharing a common value for key.
...@@ -58,6 +68,57 @@ func (g *resultGroup) splitOn(key string) []*resultGroup { ...@@ -58,6 +68,57 @@ func (g *resultGroup) splitOn(key string) []*resultGroup {
return out 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. // compare handles queries that require comparison of the groups in the query.
func (a *App) compare(w http.ResponseWriter, r *http.Request) { func (a *App) compare(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
...@@ -73,7 +134,9 @@ func (a *App) compare(w http.ResponseWriter, r *http.Request) { ...@@ -73,7 +134,9 @@ func (a *App) compare(w http.ResponseWriter, r *http.Request) {
return 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 { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
...@@ -89,11 +152,12 @@ func (a *App) compare(w http.ResponseWriter, r *http.Request) { ...@@ -89,11 +152,12 @@ func (a *App) compare(w http.ResponseWriter, r *http.Request) {
} }
type compareData struct { type compareData struct {
Q string Q string
Error string Error string
Benchstat template.HTML Benchstat template.HTML
Groups []*resultGroup Groups []*resultGroup
Labels map[string]bool Labels map[string]bool
CommonLabels benchfmt.Labels
} }
func (a *App) compareQuery(q string) *compareData { func (a *App) compareQuery(q string) *compareData {
...@@ -107,7 +171,6 @@ func (a *App) compareQuery(q string) *compareData { ...@@ -107,7 +171,6 @@ func (a *App) compareQuery(q string) *compareData {
for _, qPart := range queries { for _, qPart := range queries {
group := &resultGroup{} group := &resultGroup{}
res := a.StorageClient.Query(qPart) res := a.StorageClient.Query(qPart)
defer res.Close() // TODO: Should happen each time through the loop
for res.Next() { for res.Next() {
group.add(res.Result()) group.add(res.Result())
found++ found++
...@@ -128,7 +191,7 @@ func (a *App) compareQuery(q string) *compareData { ...@@ -128,7 +191,7 @@ func (a *App) compareQuery(q string) *compareData {
return &compareData{ return &compareData{
Q: q, Q: q,
Error: "No results matched the query string.", Error: "No results matched the query string.",
}, nil }
} }
// Attempt to automatically split results. // Attempt to automatically split results.
...@@ -150,18 +213,42 @@ func (a *App) compareQuery(q string) *compareData { ...@@ -150,18 +213,42 @@ func (a *App) compareQuery(q string) *compareData {
HTML: true, HTML: true,
}) })
// Render template. // Prepare struct for template.
labels := make(map[string]bool) 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 _, g := range groups {
for k := range g.LabelValues { for k := range g.LabelValues {
if commonLabels[k] != "" {
continue
}
labels[k] = true labels[k] = true
} }
} }
data := &compareData{ data := &compareData{
Q: q, Q: q,
Benchstat: template.HTML(buf.String()), Benchstat: template.HTML(buf.String()),
Groups: groups, Groups: groups,
Labels: labels, Labels: labels,
CommonLabels: commonLabels,
} }
return data return data
} }
...@@ -34,7 +34,7 @@ BenchmarkName 1 ns/op` ...@@ -34,7 +34,7 @@ BenchmarkName 1 ns/op`
if !reflect.DeepEqual(g.results, results) { if !reflect.DeepEqual(g.results, results) {
t.Errorf("g.results = %#v, want %#v", 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) t.Errorf("g.LabelValues = %#v, want %#v", g.LabelValues, want)
} }
groups := g.splitOn("key") groups := g.splitOn("key")
...@@ -89,9 +89,9 @@ func TestCompareQuery(t *testing.T) { ...@@ -89,9 +89,9 @@ func TestCompareQuery(t *testing.T) {
for _, q := range []string{"one vs two", "onetwo"} { for _, q := range []string{"one vs two", "onetwo"} {
t.Run(q, func(t *testing.T) { t.Run(q, func(t *testing.T) {
data, err := a.compareQuery(q) data := a.compareQuery(q)
if err != nil { if data.Error != "" {
t.Fatalf("compareQuery failed: %v", err) t.Fatalf("compareQuery failed: %s", data.Error)
} }
if have := data.Q; have != q { if have := data.Q; have != q {
t.Errorf("Q = %q, want %q", have, q) t.Errorf("Q = %q, want %q", have, q)
...@@ -102,9 +102,12 @@ func TestCompareQuery(t *testing.T) { ...@@ -102,9 +102,12 @@ func TestCompareQuery(t *testing.T) {
if len(data.Benchstat) == 0 { if len(data.Benchstat) == 0 {
t.Error("len(Benchstat) = 0, want >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) 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)
}
}) })
} }
} }
...@@ -2,46 +2,137 @@ ...@@ -2,46 +2,137 @@
<html> <html>
<head> <head>
<title>Performance Result Comparison</title> <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> </head>
<body> <body>
<div> <div id="header">
<h1>Go Performance Dashboard</h1>
<a href="/">about</a>
</div>
<div id="search">
<form action="/search"> <form action="/search">
<input type="text" name="q" value="{{.Q}}" size="120"> <input type="text" name="q" value="{{.Q}}" size="120">
<input type="submit" value="Search"> <input type="submit" value="Search">
</form> </form>
</div> </div>
{{with .Error}} <div id="results">
<p>{{.}}</p> {{with .Error}}
{{else}} <p>{{.}}</p>
<div> {{else}}
{{.Benchstat}} <table id="labels">
</div> {{with .CommonLabels}}
<table> <tbody>
<tr> <tr>
<th>label</th> <th>label</th><th>common value</th>
{{range $index, $group := .Groups}} </tr>
<th> {{range $label, $value := .}}
#{{$index}} <tr>
</th> <th class="label">{{$label}}</th><td>{{$value}}</td>
</tr>
{{end}}
</tbody>
{{end}} {{end}}
</tr> <tbody class="diff">
{{range $label, $exists := .Labels}} <tr>
<tr> <th>label</th>
<th>{{$label}}</th> <th>values</th>
{{range $.Groups}} </tr>
<td> {{range $label, $exists := .Labels}}
{{with index .LabelValues $label}} <tr class="diff">
[ <th class="label">{{$label}}</th>
{{range $value, $exists := .}} <td>
{{printf "%q" $value}} {{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}} {{end}}
] </td>
</tr>
{{end}} {{end}}
</td> </tbody>
{{end}} </table>
</tr> <div id="benchstat">
{{end}} {{.Benchstat}}
</table> </div>
{{end}} {{end}}
</div>
</body> </body>
</html> </html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment