Skip to content

Commit 1c93deb

Browse files
authored
ci(python): run Python benchmarks with CodSpeed (#10500)
Run vortex-python benchmarks under codspeed --------- Signed-off-by: Robert Kruszewski <github@robertk.io>
1 parent ce04bf9 commit 1c93deb

15 files changed

Lines changed: 306 additions & 192 deletions

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,6 @@ jobs:
142142
uv run --all-packages make check
143143
working-directory: docs/
144144

145-
# Keep this last: setup-benchmark.sh confines everything except the wrapped
146-
# benchmark to the housekeeping CPUs, so any step after it runs on few cores.
147-
- name: Setup benchmark environment
148-
run: sudo bash scripts/setup-benchmark.sh
149-
150-
- name: Pytest Benchmarks - Vortex
151-
run: |
152-
bash ../scripts/bench-taskset.sh uv run --all-packages pytest --benchmark-only benchmark/
153-
working-directory: vortex-python/
154-
155145
python-windows:
156146
name: "Python (windows-x64)"
157147
timeout-minutes: 60

‎.github/workflows/codspeed.yml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,47 @@ jobs:
126126
# Cached package restoration fails on dangling libc6-dbg documentation symlinks.
127127
cache-instruments: "false"
128128

129+
bench-codspeed-python:
130+
name: "Benchmark with Codspeed (Python)"
131+
timeout-minutes: 45
132+
runs-on: >-
133+
${{ github.repository == 'vortex-data/vortex'
134+
&& format('runs-on={0}/runner=amd64-medium/image=ubuntu24-full-x64-pre-v2/extras=s3-cache/tag=bench-codspeed-python', github.run_id)
135+
|| 'ubuntu-latest' }}
136+
env:
137+
# Same settings as the bench profile, which keeps the debug info that CodSpeed uses to
138+
# attribute native frames. See `profile.bench_python` in Cargo.toml.
139+
MATURIN_PEP517_ARGS: "--profile bench_python"
140+
steps:
141+
- uses: runs-on/action@v2
142+
if: github.repository == 'vortex-data/vortex'
143+
with:
144+
sccache: s3
145+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
146+
- uses: ./.github/actions/setup-prebuild
147+
with:
148+
enable-sccache: ${{ github.repository == 'vortex-data/vortex' && 'true' || 'false' }}
149+
- uses: ./.github/actions/system-info
150+
# Build the extension before measurement, so the benchmark step does not compile it.
151+
- name: Build Python extension
152+
run: uv sync --all-packages
153+
# Simulation runs all of the benchmark process on Valgrind, so write the input files first.
154+
- name: Write benchmark data
155+
working-directory: vortex-python
156+
env:
157+
VORTEX_BENCH_DATA_DIR: ${{ runner.temp }}/vortex-bench-data
158+
run: uv run --all-packages --no-sync python -m benchmark.data "$VORTEX_BENCH_DATA_DIR"
159+
- name: Run benchmarks
160+
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5
161+
env:
162+
VORTEX_BENCH_DATA_DIR: ${{ runner.temp }}/vortex-bench-data
163+
with:
164+
run: cd vortex-python && uv run --all-packages --no-sync pytest benchmark/ --codspeed
165+
token: ${{ secrets.CODSPEED_TOKEN }}
166+
mode: "simulation"
167+
# Cached package restoration fails on dangling libc6-dbg documentation symlinks.
168+
cache-instruments: "false"
169+
129170
# Compile on smaller VMs from the same CPU family as the measurement hosts. Globally
130171
# enabled features also affect build scripts, so the build host must support them.
131172
bench-codspeed-cpu-features-build:

‎Cargo.toml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,14 @@ lto = false
431431
[profile.bench.package."*"]
432432
debug = false
433433

434+
# Maturin builds with `cargo rustc`, which treats `--profile bench` as a request for a libtest
435+
# benchmark executable. This profile has the same settings and builds a normal library.
436+
[profile.bench_python]
437+
inherits = "bench"
438+
439+
[profile.bench_python.package."*"]
440+
debug = false
441+
434442
[profile.samply]
435443
inherits = "release"
436444
codegen-units = 1

‎uv.lock‎

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,21 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
import hashlib
5-
import math
64
import os
7-
from typing import cast
5+
from pathlib import Path
86

9-
import pyarrow as pa
107
import pytest
118

129
import vortex as vx
10+
from benchmark import data
1311

1412

15-
@pytest.fixture(
16-
scope="session",
17-
params=[{"x"}, {"x", "y"}, {"x", "z"}, {"x", "y", "z"}],
18-
ids=["int", "int_str", "int_float", "int_str_float"],
19-
)
13+
@pytest.fixture(scope="session", params=list(data.COLUMN_SETS))
2014
def vxf(
21-
tmpdir_factory: pytest.TempPathFactory,
15+
tmp_path_factory: pytest.TempPathFactory,
2216
request: pytest.FixtureRequest,
2317
) -> vx.VortexFile:
24-
fname = tmpdir_factory.mktemp("data") / "foo.vortex"
18+
directory = os.environ.get("VORTEX_BENCH_DATA_DIR")
19+
path = data.write(Path(directory) if directory else tmp_path_factory.getbasetemp() / "data", request.param)
2520

26-
if not os.path.exists(fname):
27-
length = 100_000
28-
29-
columns: dict[str, list[int] | list[float] | list[str]] = {}
30-
assert "x" in request.param
31-
columns["x"] = list(range(length))
32-
33-
if "y" in request.param:
34-
columns["y"] = [hashlib.md5(x.to_bytes(length=4), usedforsecurity=False).hexdigest() for x in range(length)]
35-
if "z" in request.param:
36-
columns["z"] = [math.sqrt(x) for x in range(length)]
37-
38-
a = vx.array(pa.table(columns)) # ty: ignore[no-matching-overload]
39-
vx.io.write(a, str(fname))
40-
return vx.open(str(fname))
41-
42-
43-
@pytest.fixture(scope="session", params=[10_000, 2_000_000], ids=["small", "large"])
44-
def array_fixture(request: pytest.FixtureRequest) -> vx.Array:
45-
size = cast(int, request.param)
46-
return vx.array(pa.table({"x": list(range(size))}))
21+
return vx.open(str(path))

‎vortex-python/benchmark/data.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
"""Benchmark input files.
5+
6+
Run ``python -m benchmark.data <dir>`` to write the files before a measured run, then set
7+
``VORTEX_BENCH_DATA_DIR=<dir>``. Under CodSpeed simulation all of the process runs on Valgrind,
8+
so data written ahead of time keeps the slow setup out of the run.
9+
"""
10+
11+
import random
12+
import sys
13+
from array import array
14+
from pathlib import Path
15+
from typing import Any
16+
17+
import pyarrow as pa
18+
import pyarrow.compute as pc
19+
20+
import vortex as vx
21+
22+
LENGTH = 100_000
23+
24+
COLUMN_SETS: dict[str, tuple[str, ...]] = {
25+
"int": ("x",),
26+
"int_str": ("x", "y"),
27+
"int_float": ("x", "z"),
28+
"int_str_float": ("x", "y", "z"),
29+
}
30+
31+
32+
def _hex_strings(length: int) -> pa.StringArray:
33+
# 32 random hex characters for each row, built from buffers without a Python loop per row.
34+
width = 32
35+
chars = random.Random(0).randbytes(length * width // 2).hex().encode()
36+
offsets = array("i", range(0, length * width + 1, width))
37+
38+
return pa.StringArray.from_buffers(length, pa.py_buffer(offsets), pa.py_buffer(chars))
39+
40+
41+
def table(columns: tuple[str, ...], length: int = LENGTH) -> pa.Table:
42+
x = pa.array(range(length), type=pa.int64())
43+
arrays: list[pa.Array[Any] | pa.ChunkedArray[Any]] = [x]
44+
45+
if "y" in columns:
46+
arrays.append(_hex_strings(length))
47+
if "z" in columns:
48+
arrays.append(pc.sqrt(x))
49+
50+
return pa.table(arrays, names=list(columns))
51+
52+
53+
def write(directory: Path, name: str) -> Path:
54+
"""Write the file for ``name`` into ``directory``, unless it is already there."""
55+
path = directory / f"{name}.vortex"
56+
57+
if not path.exists():
58+
directory.mkdir(parents=True, exist_ok=True)
59+
vx.io.write(table(COLUMN_SETS[name]), str(path))
60+
61+
return path
62+
63+
64+
if __name__ == "__main__":
65+
for name in COLUMN_SETS:
66+
print(write(Path(sys.argv[1]), name))

‎vortex-python/benchmark/test_aggregation.py‎

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
import duckdb
5-
import polars as pl
64
import pyarrow as pa
75
import pytest
86
from pytest_benchmark.fixture import BenchmarkFixture
@@ -11,32 +9,12 @@
119
from vortex.expr import column
1210

1311

14-
@pytest.mark.benchmark(group="filter", disable_gc=True)
12+
@pytest.mark.benchmark(group="filter")
1513
def test_scan_filter(benchmark: BenchmarkFixture, vxf: vx.VortexFile) -> None:
1614
benchmark(lambda: pa.concat_tables(x.to_arrow_table() for x in vxf.scan(expr=column("x") >= 50_000)))
1715

1816

19-
@pytest.mark.benchmark(group="filter", disable_gc=True)
17+
@pytest.mark.benchmark(group="filter")
2018
def test_repeated_scan_filter(benchmark: BenchmarkFixture, vxf: vx.VortexFile) -> None:
2119
rscan = vxf.to_repeated_scan(expr=column("x") > 50_000)
2220
benchmark(lambda: pa.concat_tables(x.to_arrow_table() for x in rscan.execute()))
23-
24-
25-
@pytest.mark.benchmark(group="filter", disable_gc=True)
26-
def test_polars_filter(benchmark: BenchmarkFixture, vxf: vx.VortexFile) -> None:
27-
lf = vxf.to_polars()
28-
benchmark(lambda: lf.filter(pl.col("x") >= pl.lit(50_000).cast(pl.Int64)).collect().to_arrow())
29-
30-
31-
@pytest.mark.benchmark(group="filter", disable_gc=True)
32-
def test_polars_streaming_filter(benchmark: BenchmarkFixture, vxf: vx.VortexFile) -> None:
33-
lf = vxf.to_polars()
34-
benchmark(lambda: lf.filter(pl.col("x") >= pl.lit(50_000).cast(pl.Int64)).collect(engine="streaming").to_arrow())
35-
36-
37-
@pytest.mark.benchmark(group="filter", disable_gc=True)
38-
def test_duckdb_filter(benchmark: BenchmarkFixture, vxf: vx.VortexFile) -> None:
39-
conn = duckdb.connect(database=":memory:")
40-
ds = vxf.to_dataset()
41-
_ = conn.register("ds", ds)
42-
benchmark(lambda: conn.sql("select ds.x from ds where x >= 50000").to_arrow_table())

0 commit comments

Comments
 (0)