Skip to content

Commit 2e87f3b

Browse files
committed
perf: add MVP dashboard
Add an MVP dashboard of benchmark results at /dashboard. This dashboard is heavily based on mknyszek@'s prototype in CL 385554. Results from the past 7 days for a few hand-picked benchmarks are fetched from Influx and sent to the frontend, where they are graphed using d3.js. For golang/go#48803. Change-Id: Id6cc7c51afc5a6bf718559a93b7b1e9a18c4b9bf Reviewed-on: https://go-review.googlesource.com/c/build/+/412136 Reviewed-by: Michael Knyszek <[email protected]> Run-TryBot: Michael Pratt <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 59e7a6b commit 2e87f3b

File tree

9 files changed

+775
-11
lines changed

9 files changed

+775
-11
lines changed

perf/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func (a *App) RegisterOnMux(mux *http.ServeMux) {
5757
mux.HandleFunc("/trend", a.trend)
5858
mux.HandleFunc("/cron/syncinflux", a.syncInflux)
5959
mux.HandleFunc("/healthz", a.healthz)
60+
a.dashboardRegisterOnMux(mux)
6061
}
6162

6263
// search handles /search.

perf/app/dashboard.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package app
6+
7+
import (
8+
"context"
9+
"embed"
10+
"encoding/json"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"sort"
15+
"time"
16+
17+
"github.com/influxdata/influxdb-client-go/v2/api"
18+
"golang.org/x/build/internal/influx"
19+
"golang.org/x/build/third_party/bandchart"
20+
)
21+
22+
// /dashboard/ displays a dashboard of benchmark results over time for
23+
// performance monitoring.
24+
25+
//go:embed dashboard/*
26+
var dashboardFS embed.FS
27+
28+
// dashboardRegisterOnMux registers the dashboard URLs on mux.
29+
func (a *App) dashboardRegisterOnMux(mux *http.ServeMux) {
30+
mux.Handle("/dashboard/", http.FileServer(http.FS(dashboardFS)))
31+
mux.Handle("/dashboard/third_party/bandchart/", http.StripPrefix("/dashboard/third_party/bandchart/", http.FileServer(http.FS(bandchart.FS))))
32+
mux.HandleFunc("/dashboard/data.json", a.dashboardData)
33+
}
34+
35+
// BenchmarkJSON contains the timeseries values for a single benchmark name +
36+
// unit.
37+
//
38+
// We could try to shoehorn this into benchfmt.Result, but that isn't really
39+
// the best fit for a graph.
40+
type BenchmarkJSON struct {
41+
Name string
42+
Unit string
43+
44+
// These will be sorted by CommitDate.
45+
Values []ValueJSON
46+
}
47+
48+
type ValueJSON struct {
49+
CommitHash string
50+
CommitDate time.Time
51+
52+
// These are pre-formatted as percent change.
53+
Low float64
54+
Center float64
55+
High float64
56+
}
57+
58+
// fetch queries Influx to fill Values. Name and Unit must be set.
59+
//
60+
// WARNING: Name and Unit are not sanitized. DO NOT pass user input.
61+
func (b *BenchmarkJSON) fetch(ctx context.Context, qc api.QueryAPI) error {
62+
if b.Name == "" {
63+
return fmt.Errorf("Name must be set")
64+
}
65+
if b.Unit == "" {
66+
return fmt.Errorf("Unit must be set")
67+
}
68+
69+
// TODO(prattmic): Adjust UI to comfortably display more than 7d of
70+
// data.
71+
query := fmt.Sprintf(`
72+
from(bucket: "perf")
73+
|> range(start: -7d)
74+
|> filter(fn: (r) => r["_measurement"] == "benchmark-result")
75+
|> filter(fn: (r) => r["name"] == "%s")
76+
|> filter(fn: (r) => r["unit"] == "%s")
77+
|> filter(fn: (r) => r["branch"] == "master")
78+
|> filter(fn: (r) => r["goos"] == "linux")
79+
|> filter(fn: (r) => r["goarch"] == "amd64")
80+
|> pivot(columnKey: ["_field"], rowKey: ["_time"], valueColumn: "_value")
81+
|> yield(name: "last")
82+
`, b.Name, b.Unit)
83+
84+
ir, err := qc.Query(ctx, query)
85+
if err != nil {
86+
return fmt.Errorf("error performing query: %W", err)
87+
}
88+
89+
for ir.Next() {
90+
rec := ir.Record()
91+
92+
low, ok := rec.ValueByKey("low").(float64)
93+
if !ok {
94+
return fmt.Errorf("record %s low value got type %T want float64", rec, rec.ValueByKey("low"))
95+
}
96+
97+
center, ok := rec.ValueByKey("center").(float64)
98+
if !ok {
99+
return fmt.Errorf("record %s center value got type %T want float64", rec, rec.ValueByKey("center"))
100+
}
101+
102+
high, ok := rec.ValueByKey("high").(float64)
103+
if !ok {
104+
return fmt.Errorf("record %s high value got type %T want float64", rec, rec.ValueByKey("high"))
105+
}
106+
107+
commit, ok := rec.ValueByKey("experiment-commit").(string)
108+
if !ok {
109+
return fmt.Errorf("record %s experiment-commit value got type %T want float64", rec, rec.ValueByKey("experiment-commit"))
110+
}
111+
112+
b.Values = append(b.Values, ValueJSON{
113+
CommitDate: rec.Time(),
114+
CommitHash: commit,
115+
Low: (low - 1) * 100,
116+
Center: (center - 1) * 100,
117+
High: (high - 1) * 100,
118+
})
119+
}
120+
121+
sort.Slice(b.Values, func(i, j int) bool {
122+
return b.Values[i].CommitDate.Before(b.Values[j].CommitDate)
123+
})
124+
125+
return nil
126+
}
127+
128+
// search handles /dashboard/data.json.
129+
//
130+
// TODO(prattmic): Consider caching Influx results in-memory for a few mintures
131+
// to reduce load on Influx.
132+
func (a *App) dashboardData(w http.ResponseWriter, r *http.Request) {
133+
ctx := r.Context()
134+
135+
start := time.Now()
136+
defer func() {
137+
log.Printf("Dashboard total query time: %s", time.Since(start))
138+
}()
139+
140+
ifxc, err := a.influxClient(ctx)
141+
if err != nil {
142+
log.Printf("Error getting Influx client: %v", err)
143+
http.Error(w, "Error connecting to Influx", 500)
144+
return
145+
}
146+
defer ifxc.Close()
147+
148+
qc := ifxc.QueryAPI(influx.Org)
149+
150+
// Keep benchmarks with the same name grouped together, which is
151+
// assumed by the JS.
152+
//
153+
// WARNING: Name and Unit are not sanitized. DO NOT pass user input.
154+
benchmarks := []BenchmarkJSON{
155+
{
156+
Name: "Tile38WithinCircle100kmRequest",
157+
Unit: "sec/op",
158+
},
159+
{
160+
Name: "Tile38WithinCircle100kmRequest",
161+
Unit: "p90-latency-sec",
162+
},
163+
{
164+
Name: "Tile38WithinCircle100kmRequest",
165+
Unit: "average-RSS-bytes",
166+
},
167+
{
168+
Name: "Tile38WithinCircle100kmRequest",
169+
Unit: "peak-RSS-bytes",
170+
},
171+
{
172+
Name: "GoBuildKubelet",
173+
Unit: "sec/op",
174+
},
175+
{
176+
Name: "GoBuildKubeletLink",
177+
Unit: "sec/op",
178+
},
179+
}
180+
181+
for i := range benchmarks {
182+
b := &benchmarks[i]
183+
// WARNING: Name and Unit are not sanitized. DO NOT pass user
184+
// input.
185+
if err := b.fetch(ctx, qc); err != nil {
186+
log.Printf("Error fetching benchmark %s/%s: %v", b.Name, b.Unit, err)
187+
http.Error(w, "Error fetching benchmark", 500)
188+
return
189+
}
190+
}
191+
192+
w.Header().Set("Content-Type", "application/json")
193+
w.WriteHeader(http.StatusOK)
194+
e := json.NewEncoder(w)
195+
e.SetIndent("", "\t")
196+
e.Encode(benchmarks)
197+
}

perf/app/dashboard/index.html

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<!--
2+
Copyright 2022 The Go Authors. All rights reserved.
3+
Use of this source code is governed by a BSD-style
4+
license that can be found in the LICENSE file.
5+
-->
6+
7+
<!DOCTYPE html>
8+
<html lang="en">
9+
<head>
10+
<title>Go Performance Dashboard</title>
11+
<link rel="icon" href="https://go.dev/favicon.ico"/>
12+
<link rel="stylesheet" href="./static/style.css"/>
13+
<script src="https://ajax.googleapis.com/ajax/libs/d3js/7.4.2/d3.min.js"></script>
14+
<script src="./third_party/bandchart/bandchart.js"></script>
15+
</head>
16+
17+
<body class="Dashboard">
18+
<header class="Dashboard-topbar">
19+
<h1>
20+
<a href="./">Go Performance Dashboard</a>
21+
</h1>
22+
<nav>
23+
<ul>
24+
<li><a href="https://build.golang.org">Build Dashboard</a></li>
25+
</ul>
26+
</nav>
27+
</header>
28+
29+
<form autocomplete="off" action="./">
30+
<nav class="Dashboard-controls">
31+
<div class="Dashboard-search">
32+
<input id="benchmarkInput" type="text" name="benchmark" placeholder="Type benchmark name...">
33+
</div>
34+
<input type="submit">
35+
</nav>
36+
</form>
37+
38+
<script>
39+
</script>
40+
41+
<div id="dashboard"></div>
42+
43+
<script>
44+
function addContent(name, benchmarks) {
45+
let dashboard = document.getElementById("dashboard");
46+
47+
if (name == "" || name == null || name == undefined) {
48+
// All benchmarks.
49+
// TODO(prattmic): Replace with a simpler overview?
50+
} else {
51+
// Filter to specified benchmark.
52+
benchmarks = benchmarks.filter(function(b) {
53+
return b.Name == name;
54+
});
55+
if (benchmarks.length == 0) {
56+
let title = document.createElement("h2");
57+
title.classList.add("Dashboard-title");
58+
title.innerHTML = "Benchmark \"" + name + "\" not found.";
59+
dashboard.appendChild(title);
60+
return;
61+
}
62+
}
63+
64+
let prevName = "";
65+
let grid = null;
66+
for (const b in benchmarks) {
67+
const bench = benchmarks[b];
68+
69+
if (bench.Name != prevName) {
70+
prevName = bench.Name;
71+
72+
let title = document.createElement("h2");
73+
title.classList.add("Dashboard-title");
74+
title.innerHTML = bench.Name;
75+
dashboard.appendChild(title);
76+
77+
grid = document.createElement("grid");
78+
grid.classList.add("Dashboard-grid");
79+
dashboard.appendChild(grid);
80+
}
81+
82+
let item = document.createElement("div");
83+
item.classList.add("Dashboard-grid-item");
84+
item.appendChild(BandChart(bench.Values, {
85+
unit: bench.Unit,
86+
}));
87+
grid.appendChild(item);
88+
}
89+
}
90+
91+
let benchmark = (new URLSearchParams(window.location.search)).get('benchmark');
92+
fetch('./data.json')
93+
.then(response => response.json())
94+
.then(function(benchmarks) {
95+
// Convert CommitDate to a proper date.
96+
benchmarks.forEach(function(b) {
97+
b.Values.forEach(function(v) {
98+
v.CommitDate = new Date(v.CommitDate);
99+
});
100+
});
101+
102+
addContent(benchmark, benchmarks);
103+
});
104+
</script>
105+
106+
</body>
107+
</html>

0 commit comments

Comments
 (0)