Skip to content

Commit d1cc4d7

Browse files
log performance diff nightly to slack (#19)
Signed-off-by: Morgan Gallant <morgan@morgangallant.com>
1 parent e514fc6 commit d1cc4d7

File tree

2 files changed

+169
-9
lines changed

2 files changed

+169
-9
lines changed

cmd/nightly/nightly.go

Lines changed: 166 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"flag"
1010
"fmt"
1111
"log/slog"
12+
"math"
13+
"net/http"
1214
"os"
1315
"os/signal"
1416
"runtime"
@@ -52,6 +54,16 @@ var (
5254
"nightly_",
5355
"The prefix to use for the namespace names",
5456
)
57+
flagSlackToken = flag.String(
58+
"slack-token",
59+
"",
60+
"The Slack token to use for sending notifications (optional)",
61+
)
62+
flagSlackChannelId = flag.String(
63+
"slack-channel-id",
64+
"",
65+
"The Slack channel ID to send notifications to (optional)",
66+
)
5567
)
5668

5769
func main() {
@@ -96,23 +108,168 @@ func run(ctx context.Context, logger *slog.Logger) error {
96108
return fmt.Errorf("running benchmark for dataset %q: %w", ds.Label, err)
97109
}
98110
logger.Info("benchmark run complete", slog.String("dataset", ds.Label))
99-
if dbc != nil {
100-
start := time.Now()
101-
if err := recordResultsToMySQL(ctx, dbc, results); err != nil {
102-
return fmt.Errorf("recording results to MySQL: %w", err)
111+
112+
if dbc == nil {
113+
continue // TODO maybe do something useful when we don't have MySQL?
114+
}
115+
116+
if *flagSlackToken != "" && *flagSlackChannelId != "" {
117+
diff, err := performanceDiffAgainstLastRun(ctx, dbc, results)
118+
if err != nil {
119+
return fmt.Errorf("computing performance diff against last run: %w", err)
103120
}
104-
logger.Info(
105-
"recorded results to MySQL",
106-
slog.String("dataset", ds.Label),
107-
slog.Duration("took", time.Since(start)),
108-
)
121+
if err := diff.printToSlack(ctx, results.DatasetLabel, *flagSlackToken, *flagSlackChannelId); err != nil {
122+
return fmt.Errorf("sending performance diff to slack: %w", err)
123+
}
124+
logger.Info("sent performance diff to slack", slog.String("dataset", ds.Label))
125+
}
126+
127+
start := time.Now()
128+
if err := recordResultsToMySQL(ctx, dbc, results); err != nil {
129+
return fmt.Errorf("recording results to MySQL: %w", err)
109130
}
131+
logger.Info(
132+
"recorded results to MySQL",
133+
slog.String("dataset", ds.Label),
134+
slog.Duration("took", time.Since(start)),
135+
)
110136
}
111137

112138
logger.Info("all benchmarks completed successfully, exiting")
113139

114140
return nil
115141
}
142+
143+
type queryPerformanceDiff struct {
144+
queryResult *DatasetQueryResult
145+
prev map[Percentile]time.Duration
146+
}
147+
148+
type performanceDiff struct {
149+
queries []queryPerformanceDiff
150+
}
151+
152+
func (pd *performanceDiff) printToSlack(ctx context.Context, datasetLabel string, slackToken string, slackChannelId string) error {
153+
if len(pd.queries) == 0 {
154+
return nil
155+
}
156+
157+
var builder strings.Builder
158+
builder.WriteString(fmt.Sprintf("Nightly benchmark results (%s):\n", datasetLabel))
159+
160+
for _, qpd := range pd.queries {
161+
line := fmt.Sprintf("• *%s*: P50=%dms, P95=%dms, P99=%dms",
162+
qpd.queryResult.QueryLabel,
163+
qpd.queryResult.PercentileValues[p50].Milliseconds(),
164+
qpd.queryResult.PercentileValues[p95].Milliseconds(),
165+
qpd.queryResult.PercentileValues[p99].Milliseconds())
166+
167+
prev := qpd.prev
168+
if prev != nil {
169+
var (
170+
p50PctChange = float64(qpd.queryResult.PercentileValues[p50].Milliseconds()-prev[p50].Milliseconds()) / float64(prev[p50].Milliseconds()) * 100
171+
p95PctChange = float64(qpd.queryResult.PercentileValues[p95].Milliseconds()-prev[p95].Milliseconds()) / float64(prev[p95].Milliseconds()) * 100
172+
p99PctChange = float64(qpd.queryResult.PercentileValues[p99].Milliseconds()-prev[p99].Milliseconds()) / float64(prev[p99].Milliseconds()) * 100
173+
)
174+
line += fmt.Sprintf(" (vs prev: P50%+.1f%%, P95%+.1f%%, P99%+.1f%%)",
175+
math.Round(p50PctChange*10)/10,
176+
math.Round(p95PctChange*10)/10,
177+
math.Round(p99PctChange*10)/10,
178+
)
179+
}
180+
181+
builder.WriteString(line + "\n")
182+
}
183+
184+
payload, err := json.Marshal(map[string]any{
185+
"channel": slackChannelId,
186+
"text": builder.String(),
187+
})
188+
if err != nil {
189+
return fmt.Errorf("marshaling slack payload: %w", err)
190+
}
191+
192+
req, err := http.NewRequestWithContext(ctx, "POST", "https://slack.com/api/chat.postMessage", bytes.NewReader(payload))
193+
if err != nil {
194+
return fmt.Errorf("creating slack request: %w", err)
195+
}
196+
req.Header.Set("Content-Type", "application/json")
197+
req.Header.Set("Authorization", "Bearer "+slackToken)
198+
199+
resp, err := http.DefaultClient.Do(req)
200+
if err != nil {
201+
return fmt.Errorf("sending slack request: %w", err)
202+
}
203+
defer resp.Body.Close()
204+
205+
if resp.StatusCode != 200 {
206+
return fmt.Errorf("non-200 response from slack: %d", resp.StatusCode)
207+
}
208+
209+
return nil
210+
}
211+
212+
// Computes the performance diff of this run, against the last run that was stored in MySQL.
213+
// Returns a `performanceDiff` object, which can be formatted and sent to Slack etc.
214+
func performanceDiffAgainstLastRun(ctx context.Context, dbc *sql.DB, brr *BenchmarkRunResult) (*performanceDiff, error) {
215+
queries := dbq.New(dbc)
216+
diff := &performanceDiff{}
217+
218+
dataset, err := queries.GetDatasetByLabel(ctx, brr.DatasetLabel)
219+
if err != nil {
220+
if err == sql.ErrNoRows {
221+
for _, qr := range brr.QueryResults {
222+
diff.queries = append(diff.queries, queryPerformanceDiff{
223+
queryResult: qr,
224+
prev: nil,
225+
})
226+
}
227+
return diff, nil
228+
}
229+
return nil, fmt.Errorf("getting dataset: %w", err)
230+
}
231+
232+
for _, qr := range brr.QueryResults {
233+
pd := queryPerformanceDiff{
234+
queryResult: qr,
235+
prev: nil,
236+
}
237+
query, err := queries.GetQueryByLabel(ctx, dbq.GetQueryByLabelParams{
238+
DatasetID: dataset.ID,
239+
Label: qr.QueryLabel,
240+
})
241+
if err == sql.ErrNoRows {
242+
diff.queries = append(diff.queries, pd)
243+
continue
244+
} else if err != nil {
245+
return nil, fmt.Errorf("getting query %q: %w", qr.QueryLabel, err)
246+
}
247+
248+
// Get the last run for this query.
249+
lastRun, err := queries.GetLastQueryResult(ctx, query.ID)
250+
if err == sql.ErrNoRows {
251+
diff.queries = append(diff.queries, pd)
252+
continue
253+
} else if err != nil {
254+
return nil, fmt.Errorf("getting last run for query %q: %w", qr.QueryLabel, err)
255+
}
256+
257+
pd.prev = make(map[Percentile]time.Duration)
258+
pd.prev[p0] = time.Duration(lastRun.P0Ms) * time.Millisecond
259+
pd.prev[p25] = time.Duration(lastRun.P25Ms) * time.Millisecond
260+
pd.prev[p50] = time.Duration(lastRun.P50Ms) * time.Millisecond
261+
pd.prev[p75] = time.Duration(lastRun.P75Ms) * time.Millisecond
262+
pd.prev[p90] = time.Duration(lastRun.P90Ms) * time.Millisecond
263+
pd.prev[p95] = time.Duration(lastRun.P95Ms) * time.Millisecond
264+
pd.prev[p99] = time.Duration(lastRun.P99Ms) * time.Millisecond
265+
pd.prev[p100] = time.Duration(lastRun.P100Ms) * time.Millisecond
266+
267+
diff.queries = append(diff.queries, pd)
268+
}
269+
270+
return diff, nil
271+
}
272+
116273
func recordResultsToMySQL(ctx context.Context, dbc *sql.DB, brr *BenchmarkRunResult) error {
117274
tx, err := dbc.BeginTx(ctx, nil)
118275
if err != nil {

mysql/queries.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ UPDATE `benchmark_query` SET `tags` = ? WHERE `id` = ?;
1515

1616
-- name: InsertQueryResult :exec
1717
INSERT INTO `benchmark_query_result` (`query_id`, `timestamp`, `p0_ms`, `p25_ms`, `p50_ms`, `p75_ms`, `p90_ms`, `p95_ms`, `p99_ms`, `p100_ms`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
18+
19+
-- name: GetLastQueryResult :one
20+
SELECT * FROM `benchmark_query_result` WHERE `query_id` = ? ORDER BY `timestamp` DESC LIMIT 1;

0 commit comments

Comments
 (0)