perf: make duration and end time configurable
This adds a ?day=N parameter that adjusts the query range, and ?end=TIME
parameter that sets the view end time.
Addition form fields allow adjusting these settings.
Though the search box and duration/end boxes are conceptually different
adjustments, and have separate submit buttons, put them inside the same
<form> so that the browser automatically sets all query parameters as
expected. e.g., when on a single benchmark page, changing duration/end
should not change the selected benchmark.
For golang/go#48803.
Change-Id: I8ff366983c568dc0e887289a174082c248934c2d
Reviewed-on: https://go-review.googlesource.com/c/build/+/413578
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Michael Pratt <mpratt@google.com>
diff --git a/perf/app/dashboard.go b/perf/app/dashboard.go
index 0474cd0..bc6639c 100644
--- a/perf/app/dashboard.go
+++ b/perf/app/dashboard.go
@@ -15,6 +15,7 @@
"net/http"
"regexp"
"sort"
+ "strconv"
"strings"
"time"
@@ -105,7 +106,7 @@
var errBenchmarkNotFound = errors.New("benchmark not found")
// fetchNamedUnitBenchmark queries Influx for a specific name + unit benchmark.
-func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, name, unit string) (*BenchmarkJSON, error) {
+func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, name, unit string) (*BenchmarkJSON, error) {
if err := validateFluxString(name); err != nil {
return nil, fmt.Errorf("invalid benchmark name: %w", err)
}
@@ -115,7 +116,7 @@
query := fmt.Sprintf(`
from(bucket: "perf")
- |> range(start: -30d)
+ |> range(start: %s, stop: %s)
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
|> filter(fn: (r) => r["name"] == "%s")
|> filter(fn: (r) => r["unit"] == "%s")
@@ -124,7 +125,7 @@
|> filter(fn: (r) => r["goarch"] == "amd64")
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
|> yield(name: "last")
-`, name, unit)
+`, start.Format(time.RFC3339), end.Format(time.RFC3339), name, unit)
res, err := qc.Query(ctx, query)
if err != nil {
@@ -145,7 +146,7 @@
}
// fetchDefaultBenchmarks queries Influx for the default benchmark set.
-func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI) ([]*BenchmarkJSON, error) {
+func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI, start, end time.Time) ([]*BenchmarkJSON, error) {
// Keep benchmarks with the same name grouped together, which is
// assumed by the JS.
benchmarks := []struct{ name, unit string }{
@@ -205,7 +206,7 @@
ret := make([]*BenchmarkJSON, 0, len(benchmarks))
for _, bench := range benchmarks {
- b, err := fetchNamedUnitBenchmark(ctx, qc, bench.name, bench.unit)
+ b, err := fetchNamedUnitBenchmark(ctx, qc, start, end, bench.name, bench.unit)
if err != nil {
return nil, fmt.Errorf("error fetching benchmark %s/%s: %w", bench.name, bench.unit, err)
}
@@ -217,14 +218,14 @@
// fetchNamedBenchmark queries Influx for all benchmark results with the passed
// name (for all units).
-func fetchNamedBenchmark(ctx context.Context, qc api.QueryAPI, name string) ([]*BenchmarkJSON, error) {
+func fetchNamedBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, name string) ([]*BenchmarkJSON, error) {
if err := validateFluxString(name); err != nil {
return nil, fmt.Errorf("invalid benchmark name: %w", err)
}
query := fmt.Sprintf(`
from(bucket: "perf")
- |> range(start: -30d)
+ |> range(start: %s, stop: %s)
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
|> filter(fn: (r) => r["name"] == "%s")
|> filter(fn: (r) => r["branch"] == "master")
@@ -232,7 +233,7 @@
|> filter(fn: (r) => r["goarch"] == "amd64")
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
|> yield(name: "last")
-`, name)
+`, start.Format(time.RFC3339), end.Format(time.RFC3339), name)
res, err := qc.Query(ctx, query)
if err != nil {
@@ -250,17 +251,17 @@
}
// fetchAllBenchmarks queries Influx for all benchmark results.
-func fetchAllBenchmarks(ctx context.Context, qc api.QueryAPI) ([]*BenchmarkJSON, error) {
- const query = `
+func fetchAllBenchmarks(ctx context.Context, qc api.QueryAPI, start, end time.Time) ([]*BenchmarkJSON, error) {
+ query := fmt.Sprintf(`
from(bucket: "perf")
- |> range(start: -30d)
+ |> range(start: %s, stop: %s)
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
|> filter(fn: (r) => r["branch"] == "master")
|> filter(fn: (r) => r["goos"] == "linux")
|> filter(fn: (r) => r["goarch"] == "amd64")
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
|> yield(name: "last")
-`
+`, start.Format(time.RFC3339), end.Format(time.RFC3339))
res, err := qc.Query(ctx, query)
if err != nil {
@@ -340,6 +341,11 @@
return w.w.Write(b)
}
+const (
+ defaultDays = 30
+ maxDays = 366
+)
+
// search handles /dashboard/data.json.
//
// TODO(prattmic): Consider caching Influx results in-memory for a few mintures
@@ -347,9 +353,44 @@
func (a *App) dashboardData(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- start := time.Now()
+ days := uint64(defaultDays)
+ dayParam := r.FormValue("days")
+ if dayParam != "" {
+ var err error
+ days, err = strconv.ParseUint(dayParam, 10, 32)
+ if err != nil {
+ log.Printf("Error parsing days %q: %v", dayParam, err)
+ http.Error(w, fmt.Sprintf("day parameter must be a positive integer less than or equal to %d", maxDays), http.StatusBadRequest)
+ return
+ }
+ if days == 0 || days > maxDays {
+ log.Printf("days %d too large", days)
+ http.Error(w, fmt.Sprintf("day parameter must be a positive integer less than or equal to %d", maxDays), http.StatusBadRequest)
+ return
+ }
+ }
+
+ end := time.Now()
+ endParam := r.FormValue("end")
+ if endParam != "" {
+ var err error
+ // Quirk: Browsers don't have an easy built-in way to deal with
+ // timezone in input boxes. The datetime input type yields a
+ // string in this form, with no timezone (either local or UTC).
+ // Thus, we just treat this as UTC.
+ end, err = time.Parse("2006-01-02T15:04", endParam)
+ if err != nil {
+ log.Printf("Error parsing end %q: %v", endParam, err)
+ http.Error(w, "end parameter must be a timestamp similar to RFC3339 without a time zone, like 2000-12-31T15:00", http.StatusBadRequest)
+ return
+ }
+ }
+
+ start := end.Add(-24*time.Hour*time.Duration(days))
+
+ methStart := time.Now()
defer func() {
- log.Printf("Dashboard total query time: %s", time.Since(start))
+ log.Printf("Dashboard total query time: %s", time.Since(methStart))
}()
ifxc, err := a.influxClient(ctx)
@@ -365,11 +406,11 @@
benchmark := r.FormValue("benchmark")
var benchmarks []*BenchmarkJSON
if benchmark == "" {
- benchmarks, err = fetchDefaultBenchmarks(ctx, qc)
+ benchmarks, err = fetchDefaultBenchmarks(ctx, qc, start, end)
} else if benchmark == "all" {
- benchmarks, err = fetchAllBenchmarks(ctx, qc)
+ benchmarks, err = fetchAllBenchmarks(ctx, qc, start, end)
} else {
- benchmarks, err = fetchNamedBenchmark(ctx, qc, benchmark)
+ benchmarks, err = fetchNamedBenchmark(ctx, qc, start, end, 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 d7c0166..fd2c648 100644
--- a/perf/app/dashboard/index.html
+++ b/perf/app/dashboard/index.html
@@ -29,17 +29,26 @@
</header>
<nav class="Dashboard-controls">
- <ul>
- <li>
- <form autocomplete="off" action="./">
+ <form autocomplete="off" action="./">
+ <ul>
+ <li>
<div class="Dashboard-search">
- <input id="benchmarkInput" type="text" name="benchmark" placeholder="Type benchmark name...">
+ <input id="benchmark-input" type="text" name="benchmark" placeholder="Type benchmark name..." />
</div>
+ <input type="submit" />
+ </li>
+ <li><a href="?benchmark=all">All benchmarks</a></li>
+ <span class="left-separator"></span>
+ <li>
+ Duration (days):
+ <div class="Dashboard-duration">
+ <input id="days-input" type="number" name="days" value="30" />
+ </div>
+ End (UTC): <input id="end-input" type="datetime-local" name="end" />
<input type="submit">
- </form>
- </li>
- <li><a href="?benchmark=all">All benchmarks</a></li>
- </ul>
+ </li>
+ </ul>
+ </form>
</nav>
<div class="Dashboard-documentation">
@@ -105,7 +114,7 @@
}
}
-function failure(name) {
+function failure(name, response) {
let dashboard = document.getElementById("dashboard");
removeLoadingMessage();
@@ -114,17 +123,56 @@
title.classList.add("Dashboard-title");
title.innerHTML = "Benchmark \"" + name + "\" not found.";
dashboard.appendChild(title);
+
+ let message = document.createElement("p");
+ message.classList.add("Dashboard-documentation");
+ response.text().then(function(error) {
+ message.innerHTML = error;
+ });
+ dashboard.appendChild(message);
}
-let benchmark = (new URLSearchParams(window.location.search)).get('benchmark');
-let dataURL = './data.json';
-if (benchmark) {
- dataURL += '?benchmark=' + benchmark;
+// Fill search boxes from query params.
+function prefillSearch() {
+ let params = new URLSearchParams(window.location.search);
+
+ let benchmark = params.get('benchmark');
+ if (benchmark) {
+ let input = document.getElementById('benchmark-input');
+ input.value = benchmark;
+ }
+
+ let days = params.get('days');
+ if (days) {
+ let input = document.getElementById('days-input');
+ input.value = days;
+ }
+
+ let end = params.get('end');
+ let input = document.getElementById('end-input');
+ if (end) {
+ input.value = end;
+ } else {
+ // Default to now.
+ let now = new Date();
+
+ // toISOString always uses UTC, then we just chop off the end
+ // of string to get the datetime-local format of
+ // 2000-12-31T15:00.
+ //
+ // Yes, this is really the suggested approach...
+ input.value = now.toISOString().slice(0, 16);
+ }
}
+prefillSearch()
+
+// Fetch content.
+let benchmark = (new URLSearchParams(window.location.search)).get('benchmark');
+let dataURL = './data.json' + window.location.search; // Pass through all URL params.
fetch(dataURL)
.then(response => {
if (!response.ok) {
- failure(benchmark);
+ failure(benchmark, response);
throw new Error("Data fetch failed");
}
return response.json();
diff --git a/perf/app/dashboard/static/style.css b/perf/app/dashboard/static/style.css
index d832d87..1db9a4f 100644
--- a/perf/app/dashboard/static/style.css
+++ b/perf/app/dashboard/static/style.css
@@ -77,6 +77,12 @@
padding: 10px;
text-decoration: none;
}
+nav .left-separator {
+ border-left: 2px solid #b7b7b7;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
.Dashboard {
margin: 0;
@@ -108,7 +114,6 @@
font-size: 16px;
}
input[type=text] {
- background-color: #f4f4f4;
width: 100%;
}
input[type=submit] {
@@ -123,6 +128,15 @@
padding: 10px;
text-decoration: none;
}
+
+.Dashboard-duration {
+ display: inline-block;
+ width: 4em;
+}
+input[type=number] {
+ width: 100%;
+}
+
.Dashboard-grid {
display: flex;
flex-direction: row;