Skip to content

Commit 8a278d4

Browse files
committed
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 <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> Run-TryBot: Michael Pratt <[email protected]>
1 parent 60cf787 commit 8a278d4

File tree

3 files changed

+135
-32
lines changed

3 files changed

+135
-32
lines changed

perf/app/dashboard.go

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"net/http"
1616
"regexp"
1717
"sort"
18+
"strconv"
1819
"strings"
1920
"time"
2021

@@ -105,7 +106,7 @@ func validateFluxString(s string) error {
105106
var errBenchmarkNotFound = errors.New("benchmark not found")
106107

107108
// fetchNamedUnitBenchmark queries Influx for a specific name + unit benchmark.
108-
func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, name, unit string) (*BenchmarkJSON, error) {
109+
func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, name, unit string) (*BenchmarkJSON, error) {
109110
if err := validateFluxString(name); err != nil {
110111
return nil, fmt.Errorf("invalid benchmark name: %w", err)
111112
}
@@ -115,7 +116,7 @@ func fetchNamedUnitBenchmark(ctx context.Context, qc api.QueryAPI, name, unit st
115116

116117
query := fmt.Sprintf(`
117118
from(bucket: "perf")
118-
|> range(start: -30d)
119+
|> range(start: %s, stop: %s)
119120
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
120121
|> filter(fn: (r) => r["name"] == "%s")
121122
|> filter(fn: (r) => r["unit"] == "%s")
@@ -124,7 +125,7 @@ from(bucket: "perf")
124125
|> filter(fn: (r) => r["goarch"] == "amd64")
125126
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
126127
|> yield(name: "last")
127-
`, name, unit)
128+
`, start.Format(time.RFC3339), end.Format(time.RFC3339), name, unit)
128129

129130
res, err := qc.Query(ctx, query)
130131
if err != nil {
@@ -145,7 +146,7 @@ from(bucket: "perf")
145146
}
146147

147148
// fetchDefaultBenchmarks queries Influx for the default benchmark set.
148-
func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI) ([]*BenchmarkJSON, error) {
149+
func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI, start, end time.Time) ([]*BenchmarkJSON, error) {
149150
// Keep benchmarks with the same name grouped together, which is
150151
// assumed by the JS.
151152
benchmarks := []struct{ name, unit string }{
@@ -205,7 +206,7 @@ func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI) ([]*BenchmarkJ
205206

206207
ret := make([]*BenchmarkJSON, 0, len(benchmarks))
207208
for _, bench := range benchmarks {
208-
b, err := fetchNamedUnitBenchmark(ctx, qc, bench.name, bench.unit)
209+
b, err := fetchNamedUnitBenchmark(ctx, qc, start, end, bench.name, bench.unit)
209210
if err != nil {
210211
return nil, fmt.Errorf("error fetching benchmark %s/%s: %w", bench.name, bench.unit, err)
211212
}
@@ -217,22 +218,22 @@ func fetchDefaultBenchmarks(ctx context.Context, qc api.QueryAPI) ([]*BenchmarkJ
217218

218219
// fetchNamedBenchmark queries Influx for all benchmark results with the passed
219220
// name (for all units).
220-
func fetchNamedBenchmark(ctx context.Context, qc api.QueryAPI, name string) ([]*BenchmarkJSON, error) {
221+
func fetchNamedBenchmark(ctx context.Context, qc api.QueryAPI, start, end time.Time, name string) ([]*BenchmarkJSON, error) {
221222
if err := validateFluxString(name); err != nil {
222223
return nil, fmt.Errorf("invalid benchmark name: %w", err)
223224
}
224225

225226
query := fmt.Sprintf(`
226227
from(bucket: "perf")
227-
|> range(start: -30d)
228+
|> range(start: %s, stop: %s)
228229
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
229230
|> filter(fn: (r) => r["name"] == "%s")
230231
|> filter(fn: (r) => r["branch"] == "master")
231232
|> filter(fn: (r) => r["goos"] == "linux")
232233
|> filter(fn: (r) => r["goarch"] == "amd64")
233234
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
234235
|> yield(name: "last")
235-
`, name)
236+
`, start.Format(time.RFC3339), end.Format(time.RFC3339), name)
236237

237238
res, err := qc.Query(ctx, query)
238239
if err != nil {
@@ -250,17 +251,17 @@ from(bucket: "perf")
250251
}
251252

252253
// fetchAllBenchmarks queries Influx for all benchmark results.
253-
func fetchAllBenchmarks(ctx context.Context, qc api.QueryAPI) ([]*BenchmarkJSON, error) {
254-
const query = `
254+
func fetchAllBenchmarks(ctx context.Context, qc api.QueryAPI, start, end time.Time) ([]*BenchmarkJSON, error) {
255+
query := fmt.Sprintf(`
255256
from(bucket: "perf")
256-
|> range(start: -30d)
257+
|> range(start: %s, stop: %s)
257258
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
258259
|> filter(fn: (r) => r["branch"] == "master")
259260
|> filter(fn: (r) => r["goos"] == "linux")
260261
|> filter(fn: (r) => r["goarch"] == "amd64")
261262
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
262263
|> yield(name: "last")
263-
`
264+
`, start.Format(time.RFC3339), end.Format(time.RFC3339))
264265

265266
res, err := qc.Query(ctx, query)
266267
if err != nil {
@@ -340,16 +341,56 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
340341
return w.w.Write(b)
341342
}
342343

344+
const (
345+
defaultDays = 30
346+
maxDays = 366
347+
)
348+
343349
// search handles /dashboard/data.json.
344350
//
345351
// TODO(prattmic): Consider caching Influx results in-memory for a few mintures
346352
// to reduce load on Influx.
347353
func (a *App) dashboardData(w http.ResponseWriter, r *http.Request) {
348354
ctx := r.Context()
349355

350-
start := time.Now()
356+
days := uint64(defaultDays)
357+
dayParam := r.FormValue("days")
358+
if dayParam != "" {
359+
var err error
360+
days, err = strconv.ParseUint(dayParam, 10, 32)
361+
if err != nil {
362+
log.Printf("Error parsing days %q: %v", dayParam, err)
363+
http.Error(w, fmt.Sprintf("day parameter must be a positive integer less than or equal to %d", maxDays), http.StatusBadRequest)
364+
return
365+
}
366+
if days == 0 || days > maxDays {
367+
log.Printf("days %d too large", days)
368+
http.Error(w, fmt.Sprintf("day parameter must be a positive integer less than or equal to %d", maxDays), http.StatusBadRequest)
369+
return
370+
}
371+
}
372+
373+
end := time.Now()
374+
endParam := r.FormValue("end")
375+
if endParam != "" {
376+
var err error
377+
// Quirk: Browsers don't have an easy built-in way to deal with
378+
// timezone in input boxes. The datetime input type yields a
379+
// string in this form, with no timezone (either local or UTC).
380+
// Thus, we just treat this as UTC.
381+
end, err = time.Parse("2006-01-02T15:04", endParam)
382+
if err != nil {
383+
log.Printf("Error parsing end %q: %v", endParam, err)
384+
http.Error(w, "end parameter must be a timestamp similar to RFC3339 without a time zone, like 2000-12-31T15:00", http.StatusBadRequest)
385+
return
386+
}
387+
}
388+
389+
start := end.Add(-24*time.Hour*time.Duration(days))
390+
391+
methStart := time.Now()
351392
defer func() {
352-
log.Printf("Dashboard total query time: %s", time.Since(start))
393+
log.Printf("Dashboard total query time: %s", time.Since(methStart))
353394
}()
354395

355396
ifxc, err := a.influxClient(ctx)
@@ -365,11 +406,11 @@ func (a *App) dashboardData(w http.ResponseWriter, r *http.Request) {
365406
benchmark := r.FormValue("benchmark")
366407
var benchmarks []*BenchmarkJSON
367408
if benchmark == "" {
368-
benchmarks, err = fetchDefaultBenchmarks(ctx, qc)
409+
benchmarks, err = fetchDefaultBenchmarks(ctx, qc, start, end)
369410
} else if benchmark == "all" {
370-
benchmarks, err = fetchAllBenchmarks(ctx, qc)
411+
benchmarks, err = fetchAllBenchmarks(ctx, qc, start, end)
371412
} else {
372-
benchmarks, err = fetchNamedBenchmark(ctx, qc, benchmark)
413+
benchmarks, err = fetchNamedBenchmark(ctx, qc, start, end, benchmark)
373414
}
374415
if err == errBenchmarkNotFound {
375416
log.Printf("Benchmark not found: %q", benchmark)

perf/app/dashboard/index.html

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,26 @@ <h1>
2929
</header>
3030

3131
<nav class="Dashboard-controls">
32-
<ul>
33-
<li>
34-
<form autocomplete="off" action="./">
32+
<form autocomplete="off" action="./">
33+
<ul>
34+
<li>
3535
<div class="Dashboard-search">
36-
<input id="benchmarkInput" type="text" name="benchmark" placeholder="Type benchmark name...">
36+
<input id="benchmark-input" type="text" name="benchmark" placeholder="Type benchmark name..." />
37+
</div>
38+
<input type="submit" />
39+
</li>
40+
<li><a href="?benchmark=all">All benchmarks</a></li>
41+
<span class="left-separator"></span>
42+
<li>
43+
Duration (days):
44+
<div class="Dashboard-duration">
45+
<input id="days-input" type="number" name="days" value="30" />
3746
</div>
47+
End (UTC): <input id="end-input" type="datetime-local" name="end" />
3848
<input type="submit">
39-
</form>
40-
</li>
41-
<li><a href="?benchmark=all">All benchmarks</a></li>
42-
</ul>
49+
</li>
50+
</ul>
51+
</form>
4352
</nav>
4453

4554
<div class="Dashboard-documentation">
@@ -105,7 +114,7 @@ <h2 class="Dashboard-title" id="loading">Loading...</h2>
105114
}
106115
}
107116

108-
function failure(name) {
117+
function failure(name, response) {
109118
let dashboard = document.getElementById("dashboard");
110119

111120
removeLoadingMessage();
@@ -114,17 +123,56 @@ <h2 class="Dashboard-title" id="loading">Loading...</h2>
114123
title.classList.add("Dashboard-title");
115124
title.innerHTML = "Benchmark \"" + name + "\" not found.";
116125
dashboard.appendChild(title);
126+
127+
let message = document.createElement("p");
128+
message.classList.add("Dashboard-documentation");
129+
response.text().then(function(error) {
130+
message.innerHTML = error;
131+
});
132+
dashboard.appendChild(message);
117133
}
118134

119-
let benchmark = (new URLSearchParams(window.location.search)).get('benchmark');
120-
let dataURL = './data.json';
121-
if (benchmark) {
122-
dataURL += '?benchmark=' + benchmark;
135+
// Fill search boxes from query params.
136+
function prefillSearch() {
137+
let params = new URLSearchParams(window.location.search);
138+
139+
let benchmark = params.get('benchmark');
140+
if (benchmark) {
141+
let input = document.getElementById('benchmark-input');
142+
input.value = benchmark;
143+
}
144+
145+
let days = params.get('days');
146+
if (days) {
147+
let input = document.getElementById('days-input');
148+
input.value = days;
149+
}
150+
151+
let end = params.get('end');
152+
let input = document.getElementById('end-input');
153+
if (end) {
154+
input.value = end;
155+
} else {
156+
// Default to now.
157+
let now = new Date();
158+
159+
// toISOString always uses UTC, then we just chop off the end
160+
// of string to get the datetime-local format of
161+
// 2000-12-31T15:00.
162+
//
163+
// Yes, this is really the suggested approach...
164+
input.value = now.toISOString().slice(0, 16);
165+
}
123166
}
167+
prefillSearch()
168+
169+
// Fetch content.
170+
let benchmark = (new URLSearchParams(window.location.search)).get('benchmark');
171+
let dataURL = './data.json' + window.location.search; // Pass through all URL params.
124172
fetch(dataURL)
125173
.then(response => {
126174
if (!response.ok) {
127-
failure(benchmark);
175+
failure(benchmark, response);
128176
throw new Error("Data fetch failed");
129177
}
130178
return response.json();

perf/app/dashboard/static/style.css

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ nav a {
7777
padding: 10px;
7878
text-decoration: none;
7979
}
80+
nav .left-separator {
81+
border-left: 2px solid #b7b7b7;
82+
padding-left: 10px;
83+
padding-top: 10px;
84+
padding-bottom: 10px;
85+
}
8086

8187
.Dashboard {
8288
margin: 0;
@@ -108,7 +114,6 @@ input {
108114
font-size: 16px;
109115
}
110116
input[type=text] {
111-
background-color: #f4f4f4;
112117
width: 100%;
113118
}
114119
input[type=submit] {
@@ -123,6 +128,15 @@ input[type=submit] {
123128
padding: 10px;
124129
text-decoration: none;
125130
}
131+
132+
.Dashboard-duration {
133+
display: inline-block;
134+
width: 4em;
135+
}
136+
input[type=number] {
137+
width: 100%;
138+
}
139+
126140
.Dashboard-grid {
127141
display: flex;
128142
flex-direction: row;

0 commit comments

Comments
 (0)