perf: dashboard support for multiple branches

First and foremost, we must ingest uploads on alternative branches.
Right now ingest only branch master.

On the frontend, add a dropdown for branch, very similar to the
repository dropdown. In the future, we should probably fetch a list of
branches rather than hard-coding them.

Note that the branch here is the Go branch that the perf builder
triggered against, which is a bit confusing for subrepos, but I've added
a note to try to explain.

For golang/go#53538.

Change-Id: Ib5b74b9e5d7b67ce2b04bc2bb22e3521dffaee36
Reviewed-on: https://go-review.googlesource.com/c/build/+/459155
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Michael Pratt <mpratt@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/perf/app/dashboard.go b/perf/app/dashboard.go
index 4b3b116..fa6d764 100644
--- a/perf/app/dashboard.go
+++ b/perf/app/dashboard.go
@@ -94,7 +94,7 @@
 
 // validateRe is an allowlist of characters for a Flux string literal. The
 // string will be quoted, so we must not allow ending the quote sequence.
-var validateRe = regexp.MustCompile(`^[a-zA-Z0-9(),=/_:;-]+$`)
+var validateRe = regexp.MustCompile(`^[a-zA-Z0-9(),=/_:;.-]+$`)
 
 func validateFluxString(s string) error {
 	if !validateRe.MatchString(s) {
@@ -111,10 +111,13 @@
 var errBenchmarkNotFound = errors.New("benchmark not found")
 
 // fetchNamedUnitBenchmark queries Influx for a specific name + unit benchmark.
-func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, repository string, name, unit string) (*BenchmarkJSON, error) {
+func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, repository, branch, name, unit string) (*BenchmarkJSON, error) {
 	if err := validateFluxString(repository); err != nil {
 		return nil, fmt.Errorf("invalid repository name: %w", err)
 	}
+	if err := validateFluxString(branch); err != nil {
+		return nil, fmt.Errorf("invalid branch name: %w", err)
+	}
 	if err := validateFluxString(name); err != nil {
 		return nil, fmt.Errorf("invalid benchmark name: %w", err)
 	}
@@ -131,14 +134,14 @@
   |> filter(fn: (r) => r["_measurement"] == "benchmark-result")
   |> filter(fn: (r) => r["name"] == "%s")
   |> filter(fn: (r) => r["unit"] == "%s")
-  |> filter(fn: (r) => r["branch"] == "master")
+  |> filter(fn: (r) => r["branch"] == "%s")
   |> filter(fn: (r) => r["goos"] == "linux")
   |> filter(fn: (r) => r["goarch"] == "amd64")
   |> fill(column: "repository", value: "go")
   |> filter(fn: (r) => r["repository"] == "%s")
   |> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
   |> yield(name: "last")
-`, start.Format(time.RFC3339), end.Format(time.RFC3339), name, unit, repository)
+`, start.Format(time.RFC3339), end.Format(time.RFC3339), name, unit, branch, repository)
 
 	res, err := influxQuery(ctx, qc, query)
 	if err != nil {
@@ -159,7 +162,7 @@
 }
 
 // fetchDefaultBenchmarks queries Influx for the default benchmark set.
-func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI, start, end time.Time, repository string) ([]*BenchmarkJSON, error) {
+func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI, start, end time.Time, repository, branch string) ([]*BenchmarkJSON, error) {
 	if repository != "go" {
 		// No defaults defined for other subrepos yet, just return an
 		// empty set.
@@ -225,7 +228,7 @@
 
 	ret := make([]*BenchmarkJSON, 0, len(benchmarks))
 	for _, bench := range benchmarks {
-		b, err := fetchNamedUnitBenchmark(ctx, qc, start, end, repository, bench.name, bench.unit)
+		b, err := fetchNamedUnitBenchmark(ctx, qc, start, end, repository, branch, bench.name, bench.unit)
 		if err != nil {
 			return nil, fmt.Errorf("error fetching benchmark %s/%s: %w", bench.name, bench.unit, err)
 		}
@@ -237,10 +240,13 @@
 
 // fetchNamedBenchmark queries Influx for all benchmark results with the passed
 // name (for all units).
-func fetchNamedBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, repository string, name string) ([]*BenchmarkJSON, error) {
+func fetchNamedBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, repository, branch, name string) ([]*BenchmarkJSON, error) {
 	if err := validateFluxString(repository); err != nil {
 		return nil, fmt.Errorf("invalid repository name: %w", err)
 	}
+	if err := validateFluxString(branch); err != nil {
+		return nil, fmt.Errorf("invalid branch name: %w", err)
+	}
 	if err := validateFluxString(name); err != nil {
 		return nil, fmt.Errorf("invalid benchmark name: %w", err)
 	}
@@ -253,14 +259,14 @@
   |> range(start: %s, stop: %s)
   |> filter(fn: (r) => r["_measurement"] == "benchmark-result")
   |> filter(fn: (r) => r["name"] == "%s")
-  |> filter(fn: (r) => r["branch"] == "master")
+  |> filter(fn: (r) => r["branch"] == "%s")
   |> filter(fn: (r) => r["goos"] == "linux")
   |> filter(fn: (r) => r["goarch"] == "amd64")
   |> fill(column: "repository", value: "go")
   |> filter(fn: (r) => r["repository"] == "%s")
   |> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
   |> yield(name: "last")
-`, start.Format(time.RFC3339), end.Format(time.RFC3339), name, repository)
+`, start.Format(time.RFC3339), end.Format(time.RFC3339), name, branch, repository)
 
 	res, err := influxQuery(ctx, qc, query)
 	if err != nil {
@@ -278,10 +284,13 @@
 }
 
 // fetchAllBenchmarks queries Influx for all benchmark results.
-func fetchAllBenchmarks(ctx context.Context, qc api.QueryAPI, regressions bool, start, end time.Time, repository string) ([]*BenchmarkJSON, error) {
+func fetchAllBenchmarks(ctx context.Context, qc api.QueryAPI, regressions bool, start, end time.Time, repository, branch string) ([]*BenchmarkJSON, error) {
 	if err := validateFluxString(repository); err != nil {
 		return nil, fmt.Errorf("invalid repository name: %w", err)
 	}
+	if err := validateFluxString(branch); err != nil {
+		return nil, fmt.Errorf("invalid branch name: %w", err)
+	}
 
 	// Note that very old points are missing the "repository" field. fill()
 	// sets repository=go on all points missing that field, as they were
@@ -290,14 +299,14 @@
 from(bucket: "perf")
   |> range(start: %s, stop: %s)
   |> filter(fn: (r) => r["_measurement"] == "benchmark-result")
-  |> filter(fn: (r) => r["branch"] == "master")
+  |> filter(fn: (r) => r["branch"] == "%s")
   |> filter(fn: (r) => r["goos"] == "linux")
   |> filter(fn: (r) => r["goarch"] == "amd64")
   |> fill(column: "repository", value: "go")
   |> filter(fn: (r) => r["repository"] == "%s")
   |> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
   |> yield(name: "last")
-`, start.Format(time.RFC3339), end.Format(time.RFC3339), repository)
+`, start.Format(time.RFC3339), end.Format(time.RFC3339), branch, repository)
 
 	res, err := influxQuery(ctx, qc, query)
 	if err != nil {
@@ -640,17 +649,21 @@
 	if repository == "" {
 		repository = "go"
 	}
+	branch := r.FormValue("branch")
+	if branch == "" {
+		branch = "master"
+	}
 
 	benchmark := r.FormValue("benchmark")
 	var benchmarks []*BenchmarkJSON
 	if benchmark == "" {
-		benchmarks, err = fetchDefaultBenchmarks(ctx, qc, start, end, repository)
+		benchmarks, err = fetchDefaultBenchmarks(ctx, qc, start, end, repository, branch)
 	} else if benchmark == "all" {
-		benchmarks, err = fetchAllBenchmarks(ctx, qc, false, start, end, repository)
+		benchmarks, err = fetchAllBenchmarks(ctx, qc, false, start, end, repository, branch)
 	} else if benchmark == "regressions" {
-		benchmarks, err = fetchAllBenchmarks(ctx, qc, true, start, end, repository)
+		benchmarks, err = fetchAllBenchmarks(ctx, qc, true, start, end, repository, branch)
 	} else {
-		benchmarks, err = fetchNamedBenchmark(ctx, qc, start, end, repository, benchmark)
+		benchmarks, err = fetchNamedBenchmark(ctx, qc, start, end, repository, branch, benchmark)
 	}
 	if err == errBenchmarkNotFound {
 		log.Printf("Benchmark not found: %q", benchmark)
diff --git a/perf/app/dashboard/index.html b/perf/app/dashboard/index.html
index f5d2d8c..3d3a19f 100644
--- a/perf/app/dashboard/index.html
+++ b/perf/app/dashboard/index.html
@@ -46,6 +46,14 @@
 					<option>go</option>
 					<option>tools</option>
 				</select>
+				Branch:
+				<select id="branch-select" name="branch">
+					<option>master</option>
+					<option>release-branch.go1.17</option>
+					<option>release-branch.go1.18</option>
+					<option>release-branch.go1.19</option>
+					<option>release-branch.go1.20</option>
+				</select>
 				Duration (days):
 				<div class="Dashboard-duration">
 					<input id="days-input" type="number" name="days" value="30" />
@@ -72,6 +80,11 @@
 		<a href="https://build.golang.org/?repo=golang.org%2fx%2fbenchmarks">x/benchmarks build dashboard</a>
 		for tested ("ok") / untested (empty) commits.
 	</p>
+	<p>
+		Also note that the 'branch' selection above is the Go branch
+		that benchmarking ran against on https://build.golang.org, not
+		the subrepo branch.
+	</p>
 </div>
 
 <div id="dashboard">
@@ -154,6 +167,12 @@
 		select.value = repository;
 	}
 
+	let branch = params.get('branch');
+	if (branch) {
+		let select = document.getElementById('branch-select');
+		select.value = branch;
+	}
+
 	let days = params.get('days');
 	if (days) {
 		let input = document.getElementById('days-input');
diff --git a/perf/app/influx.go b/perf/app/influx.go
index 0bedf3c..ae7219c 100644
--- a/perf/app/influx.go
+++ b/perf/app/influx.go
@@ -214,9 +214,6 @@
 		"by:coordinator@symbolic-datum-552.iam.gserviceaccount.com",
 		// Only take results that were generated from post-submit runs, not trybots.
 		"post-submit:true",
-		// Limit to just the master branch for now.
-		// TODO(mknyszek): Support other branches.
-		"branch:master",
 	}, " ")
 	uploadList := a.StorageClient.ListUploads(
 		ctx,