Skip to content

Commit c32f38b

Browse files
authored
test: Benchmarks (#160)
# Summary Adds a simple benchmark to the test suite. # Changes * Adds benchmark test on a simple 3-node model. * Reports benchmark results on each new PR, so that the code changes can be compared with main branch before merging.
1 parent 4bcda05 commit c32f38b

6 files changed

Lines changed: 145 additions & 1 deletion

File tree

.github/workflows/benchmarks.yaml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Benchmarks
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- synchronize
8+
9+
jobs:
10+
lint:
11+
name: Benchmark tests
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python_version: [3.12]
16+
steps:
17+
- name: Checkout branch
18+
uses: actions/checkout@v4
19+
with:
20+
path: pr
21+
22+
- name: Checkout main
23+
uses: actions/checkout@v4
24+
with:
25+
ref: main
26+
path: main
27+
28+
- name: Install python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: ${{matrix.python_version}}
32+
33+
- name: Install uv
34+
uses: astral-sh/setup-uv@v4
35+
with:
36+
enable-cache: true
37+
cache-dependency-glob: "main/uv.lock"
38+
39+
- name: Setup benchmarks
40+
run: |
41+
echo "BASE_SHA=$(echo ${{ github.event.pull_request.base.sha }} | cut -c1-8)" >> $GITHUB_ENV
42+
echo "HEAD_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-8)" >> $GITHUB_ENV
43+
echo "PR_COMMENT=$(mktemp)" >> $GITHUB_ENV
44+
45+
- name: Run benchmarks on PR
46+
working-directory: ./pr
47+
run: |
48+
uv sync --group test
49+
uv run pytest --benchmark-only --benchmark-save=pr
50+
51+
- name: Run benchmarks on main
52+
working-directory: ./main
53+
continue-on-error: true
54+
run: |
55+
uv sync --group test
56+
uv run pytest --benchmark-only --benchmark-save=base
57+
58+
- name: Compare results
59+
continue-on-error: false
60+
run: |
61+
uvx pytest-benchmark compare **/.benchmarks/**/*.json | tee cmp_results
62+
63+
echo 'Benchmark comparison for [`${{ env.BASE_SHA }}`](${{ github.event.repository.html_url }}/commit/${{ github.event.pull_request.base.sha }}) (base) vs [`${{ env.HEAD_SHA }}`](${{ github.event.repository.html_url }}/commit/${{ github.event.pull_request.head.sha }}) (PR)' >> pr_comment
64+
echo '```' >> pr_comment
65+
cat cmp_results >> pr_comment
66+
echo '```' >> pr_comment
67+
cat pr_comment > ${{ env.PR_COMMENT }}
68+
69+
- name: Comment on PR
70+
uses: actions/github-script@v7
71+
with:
72+
github-token: ${{ secrets.GITHUB_TOKEN }}
73+
script: |
74+
github.rest.issues.createComment({
75+
issue_number: context.issue.number,
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
body: require('fs').readFileSync('${{ env.PR_COMMENT }}').toString()
79+
});
80+

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ coverage.json
5454
.pytest_cache/
5555
cover/
5656

57+
# Benchmarking
58+
.benchmarks/
59+
5760
# Translations
5861
*.mo
5962
*.pot

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ test = [
7575
"optuna>=3.0,<4",
7676
"pytest>=8.3,<9",
7777
"pytest-asyncio>=1.0,<2",
78+
"pytest-benchmark>=5.1.0",
7879
"pytest-cases>=3.8,<4",
7980
"pytest-env>=1.1,<2",
8081
"pytest-rerunfailures>=15.0,<16",

tests/benchmark/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Provides benchmark tests for Plugboard."""
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Simple benchmark tests for Plugboard models."""
2+
3+
import asyncio
4+
5+
from pytest_benchmark.fixture import BenchmarkFixture
6+
7+
from plugboard.connector import AsyncioConnector
8+
from plugboard.process import LocalProcess, Process
9+
from plugboard.schemas import ConnectorSpec
10+
from tests.integration.test_process_with_components_run import A, B
11+
12+
13+
def _setup_process() -> tuple[tuple[Process], dict]:
14+
comp_a = A(name="comp_a", iters=1000)
15+
comp_b1 = B(name="comp_b1", factor=1)
16+
comp_b2 = B(name="comp_b2", factor=2)
17+
components = [comp_a, comp_b1, comp_b2]
18+
connectors = [
19+
AsyncioConnector(spec=ConnectorSpec(source="comp_a.out_1", target="comp_b1.in_1")),
20+
AsyncioConnector(spec=ConnectorSpec(source="comp_b1.out_1", target="comp_b2.in_1")),
21+
]
22+
process = LocalProcess(components=components, connectors=connectors)
23+
# Initialise process so that this is excluded from the benchmark timing
24+
asyncio.run(process.init())
25+
# Return args and kwargs tuple for benchmark.pedantic
26+
return (process,), {}
27+
28+
29+
def _run_process(process: Process) -> None:
30+
asyncio.run(process.run())
31+
32+
33+
def test_benchmark_process_run(benchmark: BenchmarkFixture) -> None:
34+
"""Benchmark the running of a Plugboard Process."""
35+
benchmark.pedantic(_run_process, setup=_setup_process, rounds=5)

uv.lock

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)