analysis/app: group benchstat results if a name label is in the query

Change-Id: Ia8761c7709e09196dbb2499bcec76ab5bfc0c715
Reviewed-on: https://go-review.googlesource.com/35949
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/analysis/app/compare.go b/analysis/app/compare.go
index 21a89ea..9780d95 100644
--- a/analysis/app/compare.go
+++ b/analysis/app/compare.go
@@ -6,14 +6,18 @@
 
 import (
 	"bytes"
+	"fmt"
 	"html/template"
 	"io/ioutil"
 	"net/http"
 	"sort"
+	"strconv"
 	"strings"
+	"unicode"
 
 	"golang.org/x/perf/analysis/internal/benchstat"
 	"golang.org/x/perf/storage/benchfmt"
+	"golang.org/x/perf/storage/query"
 )
 
 // A resultGroup holds a list of results and tracks the distinct labels found in that list.
@@ -165,6 +169,56 @@
 	CommonLabels benchfmt.Labels
 }
 
+// queryKeys returns the keys that are exact-matched by q.
+func queryKeys(q string) map[string]bool {
+	out := make(map[string]bool)
+	for _, part := range query.SplitWords(q) {
+		// TODO(quentin): This func is shared with db.go; refactor?
+		i := strings.IndexFunc(part, func(r rune) bool {
+			return r == ':' || r == '>' || r == '<' || unicode.IsSpace(r) || unicode.IsUpper(r)
+		})
+		if i >= 0 && part[i] == ':' {
+			out[part[:i]] = true
+		}
+	}
+	return out
+}
+
+// elideKeyValues returns content, a benchmark format line, with the
+// values of any keys in keys elided.
+func elideKeyValues(content string, keys map[string]bool) string {
+	var end string
+	if i := strings.IndexFunc(content, unicode.IsSpace); i >= 0 {
+		content, end = content[:i], content[i:]
+	}
+	// Check for gomaxprocs value
+	if i := strings.LastIndex(content, "-"); i >= 0 {
+		_, err := strconv.Atoi(content[i+1:])
+		if err == nil {
+			if keys["gomaxprocs"] {
+				content, end = content[:i], "-*"+end
+			} else {
+				content, end = content[:i], content[i:]+end
+			}
+		}
+	}
+	parts := strings.Split(content, "/")
+	for i, part := range parts {
+		if equals := strings.Index(part, "="); equals >= 0 {
+			if keys[part[:equals]] {
+				parts[i] = part[:equals] + "=*"
+			}
+		} else if i == 0 {
+			if keys["name"] {
+				parts[i] = "Benchmark*"
+			}
+		} else if keys[fmt.Sprintf("sub%d", i)] {
+			parts[i] = "*"
+		}
+	}
+	return strings.Join(parts, "/") + end
+}
+
 func (a *App) compareQuery(q string) *compareData {
 	// Parse query
 	prefix, queries := parseQueryString(q)
@@ -174,13 +228,16 @@
 	var groups []*resultGroup
 	var found int
 	for _, qPart := range queries {
+		keys := queryKeys(qPart)
 		group := &resultGroup{}
 		if prefix != "" {
 			qPart = prefix + " " + qPart
 		}
 		res := a.StorageClient.Query(qPart)
 		for res.Next() {
-			group.add(res.Result())
+			result := res.Result()
+			result.Content = elideKeyValues(result.Content, keys)
+			group.add(result)
 			found++
 		}
 		err := res.Err()
diff --git a/analysis/app/compare_test.go b/analysis/app/compare_test.go
index 1728ae0..5552c53 100644
--- a/analysis/app/compare_test.go
+++ b/analysis/app/compare_test.go
@@ -131,3 +131,27 @@
 		})
 	}
 }
+
+func TestElideKeyValues(t *testing.T) {
+	type sb map[string]bool
+	tests := []struct {
+		content string
+		keys    sb
+		want    string
+	}{
+		{"BenchmarkOne/key=1-1 1 ns/op", sb{"key": true}, "BenchmarkOne/key=*-1 1 ns/op"},
+		{"BenchmarkOne/key=1-2 1 ns/op", sb{"other": true}, "BenchmarkOne/key=1-2 1 ns/op"},
+		{"BenchmarkOne/key=1/key2=2-3 1 ns/op", sb{"key": true}, "BenchmarkOne/key=*/key2=2-3 1 ns/op"},
+		{"BenchmarkOne/foo/bar-4 1 ns/op", sb{"sub1": true}, "BenchmarkOne/*/bar-4 1 ns/op"},
+		{"BenchmarkOne/foo/bar-5 1 ns/op", sb{"gomaxprocs": true}, "BenchmarkOne/foo/bar-* 1 ns/op"},
+		{"BenchmarkOne/foo/bar-6 1 ns/op", sb{"name": true}, "Benchmark*/foo/bar-6 1 ns/op"},
+	}
+	for i, test := range tests {
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			have := elideKeyValues(test.content, test.keys)
+			if have != test.want {
+				t.Errorf("elideKeys(%q, %#v) = %q, want %q", test.content, map[string]bool(test.keys), have, test.want)
+			}
+		})
+	}
+}