Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ on:
- main
schedule:
- cron: "12 0 * * *"
workflow_dispatch:
inputs:
run_benchmarks:
description: 'Run benchmarks'
required: false
default: false
type: boolean

env:
GOPATH: /home/runner/go
Expand Down Expand Up @@ -142,3 +149,38 @@ jobs:

- name: Run fuzzers
run: bin/task fuzz

benchmark:
name: Benchmark
runs-on: ubuntu-24.04
timeout-minutes: 30

# Do not run this job in parallel for any PR change or branch push.
concurrency:
group: ${{ github.workflow }}-benchmark-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true

# Only run on main branch, scheduled runs, or when triggered manually
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.run_benchmarks)

permissions:
contents: read

steps:
# TODO https://github.com/FerretDB/github-actions/issues/211
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Go
uses: FerretDB/github-actions/setup-go@main
with:
cache-key: benchmark

- name: Install Task and tools
run: go generate -x
working-directory: tools

- name: Run benchmarks and push results
run: bin/task bench-ci
env:
BENCHMARK_MONGODB_URI: ${{ secrets.BENCHMARK_MONGODB_URI }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/bin/
*.txt
benchpush
5 changes: 5 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ tasks:
- go test -bench='{{.BENCH}}' -count={{.BENCH_COUNT}} -benchtime={{.BENCH_TIME}} -timeout=60m ./wirebson | tee new.txt
- bin/benchstat old.txt new.txt

bench-ci:
desc: "Run benchmarks and push results to MongoDB (for CI)"
cmds:
- go run ./cmd/benchpush -bench='{{.BENCH}}' -count={{.BENCH_COUNT}} -benchtime={{.BENCH_TIME}} -uri='{{.BENCHMARK_MONGODB_URI}}'

fuzz:
desc: "Run fuzzers for about 1 minute (with default FUZZ_TIME)"
cmds:
Expand Down
116 changes: 116 additions & 0 deletions cmd/benchpush/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package main provides a command-line tool for running benchmarks and pushing results to MongoDB.
package main

import (
"context"
"flag"
"log/slog"
"os"
"os/exec"
"time"

"github.com/FerretDB/wire/internal/benchpusher"
)

func main() {
var (
mongoURI = flag.String("uri", "", "MongoDB URI for pushing results (if empty, only parse and print)")
benchRegex = flag.String("bench", "Benchmark.*", "Benchmark regex pattern")
benchTime = flag.String("benchtime", "1s", "Benchmark time")
benchCount = flag.String("count", "5", "Benchmark count")
pkg = flag.String("pkg", "./wirebson", "Package to benchmark")
timeout = flag.Duration("timeout", 10*time.Minute, "Benchmark timeout")
)
flag.Parse()

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))

// Run the benchmarks
logger.Info("Running benchmarks...",
slog.String("package", *pkg),
slog.String("pattern", *benchRegex),
slog.String("benchtime", *benchTime),
slog.String("count", *benchCount))

ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()

cmd := exec.CommandContext(ctx, "go", "test",
"-bench="+*benchRegex,
"-count="+*benchCount,
"-benchtime="+*benchTime,
"-timeout=60m",
*pkg)

output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
logger.Error("Benchmark command failed",
slog.String("error", err.Error()),
slog.String("stderr", string(exitErr.Stderr)))
} else {
logger.Error("Failed to run benchmark command", slog.String("error", err.Error()))
}
os.Exit(1)

Check warning on line 68 in cmd/benchpush/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/benchpush/main.go#L29-L68

Added lines #L29 - L68 were not covered by tests
}

outputStr := string(output)
logger.Info("Benchmark completed", slog.Int("output_length", len(outputStr)))

// Parse the benchmark output
var client *benchpusher.Client
if *mongoURI != "" {
var err error

Check failure on line 77 in cmd/benchpush/main.go

View workflow job for this annotation

GitHub Actions / Test MongoDB

shadow: declaration of "err" shadows declaration at line 59 (govet)

Check failure on line 77 in cmd/benchpush/main.go

View workflow job for this annotation

GitHub Actions / Test MongoDB TLS

shadow: declaration of "err" shadows declaration at line 59 (govet)

Check failure on line 77 in cmd/benchpush/main.go

View workflow job for this annotation

GitHub Actions / Test FerretDB v2

shadow: declaration of "err" shadows declaration at line 59 (govet)
client, err = benchpusher.New(*mongoURI, logger)
if err != nil {
logger.Error("Failed to create MongoDB client", slog.String("error", err.Error()))
os.Exit(1)
}
defer client.Close()

Check warning on line 83 in cmd/benchpush/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/benchpush/main.go#L71-L83

Added lines #L71 - L83 were not covered by tests
}

results, err := benchpusher.ParseBenchmarkOutput(outputStr)
if err != nil {
logger.Error("Failed to parse benchmark output", slog.String("error", err.Error()))
os.Exit(1)
}

Check warning on line 90 in cmd/benchpush/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/benchpush/main.go#L86-L90

Added lines #L86 - L90 were not covered by tests

logger.Info("Parsed benchmark results", slog.Int("count", len(results)))

// Print results summary
for _, result := range results {
logger.Info("Benchmark result",
slog.String("name", result.Name),
slog.Int("iterations", result.Iterations),
slog.Float64("ns_per_op", result.NsPerOp),
slog.Any("metrics", result.Metrics))
}

Check warning on line 101 in cmd/benchpush/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/benchpush/main.go#L92-L101

Added lines #L92 - L101 were not covered by tests

// Push to MongoDB if URI is provided
if *mongoURI != "" && len(results) > 0 {
logger.Info("Pushing results to MongoDB...")
if err := client.Push(context.Background(), results); err != nil {

Check failure on line 106 in cmd/benchpush/main.go

View workflow job for this annotation

GitHub Actions / Test MongoDB

shadow: declaration of "err" shadows declaration at line 59 (govet)

Check failure on line 106 in cmd/benchpush/main.go

View workflow job for this annotation

GitHub Actions / Test MongoDB TLS

shadow: declaration of "err" shadows declaration at line 59 (govet)

Check failure on line 106 in cmd/benchpush/main.go

View workflow job for this annotation

GitHub Actions / Test FerretDB v2

shadow: declaration of "err" shadows declaration at line 59 (govet)
logger.Error("Failed to push results to MongoDB", slog.String("error", err.Error()))
os.Exit(1)
}
logger.Info("Successfully pushed results to MongoDB")
} else if *mongoURI == "" {
logger.Info("No MongoDB URI provided, skipping push to database")
} else {
logger.Info("No benchmark results to push")
}

Check warning on line 115 in cmd/benchpush/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/benchpush/main.go#L104-L115

Added lines #L104 - L115 were not covered by tests
}
121 changes: 121 additions & 0 deletions docs/benchmark-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Benchmark CI

This document describes the benchmark CI setup for the FerretDB Wire repository.

## Overview

The repository includes automated benchmark running that:
1. Runs benchmarks using Go's built-in benchmark framework
2. Parses benchmark results using `golang.org/x/perf/benchfmt`
3. Converts results to BSON format
4. Pushes results to a MongoDB-compatible database for visualization in Grafana

## Components

### Internal Package: `internal/benchpusher`

This package provides:
- Benchmark result parsing using the modern `golang.org/x/perf/benchfmt` library
- MongoDB client for pushing structured benchmark data
- BSON document creation compatible with FerretDB

### CLI Tool: `cmd/benchpush`

Command-line tool that:
- Runs `go test -bench` with configurable parameters
- Parses the output and extracts benchmark metrics
- Optionally pushes results to MongoDB if URI is provided

Usage:
```bash
# Run benchmarks without pushing to database
go run ./cmd/benchpush -bench=BenchmarkDocumentDecode -count=1 -benchtime=100ms

# Run benchmarks and push to MongoDB
go run ./cmd/benchpush -bench=BenchmarkDocumentDecode -count=5 -benchtime=1s -uri="mongodb://..."
```

### Task Integration

Added `bench-ci` task to `Taskfile.yml`:
```yaml
bench-ci:
desc: "Run benchmarks and push results to MongoDB (for CI)"
cmds:
- go run ./cmd/benchpush -bench='{{.BENCH}}' -count={{.BENCH_COUNT}} -benchtime={{.BENCH_TIME}} -uri='{{.BENCHMARK_MONGODB_URI}}'
```

### GitHub Actions Workflow

Added `benchmark` job to `.github/workflows/go.yml` that:
- Runs on main branch pushes, scheduled runs, and manual workflow dispatch
- Uses the `bench-ci` task to run benchmarks and push results
- Requires `BENCHMARK_MONGODB_URI` secret to be configured

## Configuration

### Environment Variables

- `BENCHMARK_MONGODB_URI`: MongoDB URI for pushing benchmark results
- `RUNNER_NAME`: GitHub Actions runner name (automatically set)
- `GITHUB_REPOSITORY`: Repository name (automatically set)

### Benchmark Parameters

Configurable via Taskfile.yml variables:
- `BENCH`: Benchmark regex pattern (default: `Benchmark.*`)
- `BENCH_TIME`: Benchmark duration (default: `1s`)
- `BENCH_COUNT`: Number of benchmark runs (default: `5`)

## Data Format

Benchmark results are stored in MongoDB with this structure:

```json
{
"time": "2025-07-18T14:30:12.077Z",
"env": {
"runner": "runner-name",
"hostname": "hostname",
"repository": "FerretDB/wire"
},
"benchmarks": {
"DocumentDecode_handshake1-4": {
"iterations": 3148209,
"ns_per_op": 381.6,
"metrics": {
"B/op": "352.00",
"allocs/op": "10.00"
}
}
}
}
```

## Manual Execution

To manually trigger benchmark runs:

1. Go to the repository's Actions tab
2. Select "Go" workflow
3. Click "Run workflow"
4. Check "Run benchmarks" option
5. Click "Run workflow"

## Local Testing

To test locally without MongoDB:
```bash
# Install task
go generate -x tools/tools.go

# Run short benchmarks
BENCHMARK_MONGODB_URI="" bin/task bench-ci
```

## Integration with Grafana

The benchmark results pushed to MongoDB can be visualized in Grafana by:
1. Configuring MongoDB as a data source
2. Creating dashboards that query the `benchmarks` collection
3. Displaying metrics like ns/op, memory allocations, and throughput over time
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ require (
github.com/xdg-go/scram v1.1.2
go.mongodb.org/mongo-driver v1.17.4
go.mongodb.org/mongo-driver/v2 v2.2.2
golang.org/x/perf v0.0.0-20250710210952-7b7c2de18447
)

require (
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
18 changes: 12 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g=
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
Expand All @@ -6,6 +8,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
Expand All @@ -25,16 +29,18 @@ go.mongodb.org/mongo-driver/v2 v2.2.2 h1:9cYuS3fl1Xhqwpfazso10V7BHQD58kCgtzhfAmJ
go.mongodb.org/mongo-driver/v2 v2.2.2/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/perf v0.0.0-20250710210952-7b7c2de18447 h1:rW8zamu+F63omx9uT7NEv45cj82FinwZqCQxB6YpXaY=
golang.org/x/perf v0.0.0-20250710210952-7b7c2de18447/go.mod h1:/3Q08NGeGk1jEFl72e1LrFtPIrpY6mfw8iqURBiwZ2E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -46,8 +52,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Expand Down
Loading
Loading