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 (
"io/ioutil"
"net/http"
"sort"
"strings"
"golang.org/x/perf/analysis/internal/benchstat"
"golang.org/x/perf/storage/benchfmt"
......@@ -20,21 +21,30 @@ type resultGroup struct {
// 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 @@ func (g *resultGroup) splitOn(key string) []*resultGroup {
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 @@ func (a *App) compare(w http.ResponseWriter, r *http.Request) {
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 @@ func (a *App) compare(w http.ResponseWriter, r *http.Request) {
}
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 @@ func (a *App) compareQuery(q string) *compareData {
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 @@ func (a *App) compareQuery(q string) *compareData {
return &compareData{
Q: q,
Error: "No results matched the query string.",
}, nil
}
}
// Attempt to automatically split results.
......@@ -150,18 +213,42 @@ func (a *App) compareQuery(q string) *compareData {
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
}
......@@ -34,7 +34,7 @@ BenchmarkName 1 ns/op`
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 @@ func TestCompareQuery(t *testing.T) {
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 @@ func TestCompareQuery(t *testing.T) {
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)
}
})
}
}
......@@ -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>
<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}}
</tbody>
{{end}}
</tr>
{{range $label, $exists := .Labels}}
<tr>
<th>{{$label}}</th>
{{range $.Groups}}
<td>
{{with index .LabelValues $label}}
[
{{range $value, $exists := .}}
{{printf "%q" $value}}
<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}}
</td>
{{end}}
</tr>
{{end}}
</table>
{{end}}
</tbody>
</table>
<div id="benchstat">
{{.Benchstat}}
</div>
{{end}}
</div>
</body>
</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