Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ef5db51
add benchmarks
d-v-b Oct 30, 2025
4c2a935
remove failing zipstore
d-v-b Oct 30, 2025
3e5c6cb
don't do benchmarking in default pytest runs
d-v-b Oct 30, 2025
fc3388d
changelog
d-v-b Oct 30, 2025
da64194
codspeed workflow
d-v-b Oct 30, 2025
ba2c4cf
lint
d-v-b Oct 30, 2025
009f739
remove pedantic mode
d-v-b Oct 30, 2025
7b26982
only run benchmarks in one environment
d-v-b Oct 30, 2025
c0342ee
use better string id for test params, make test data 1MB, and simplif…
d-v-b Oct 31, 2025
800b64c
move layout to an external file
d-v-b Oct 31, 2025
f86291b
get workloads to resemble recent sharding perf tests
d-v-b Nov 3, 2025
dd76776
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
d-v-b Nov 3, 2025
90502a6
test ids
d-v-b Nov 3, 2025
5e0c228
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
d-v-b Nov 3, 2025
f613b29
tweak tests
d-v-b Nov 3, 2025
602d79f
tweak tests
d-v-b Nov 3, 2025
523f565
fix typo
d-v-b Nov 3, 2025
9eb4bb6
add slice indexing benchmarks
d-v-b Nov 3, 2025
c9ab694
remove readme
d-v-b Nov 3, 2025
0ecc190
add docs documentation
d-v-b Nov 3, 2025
b657e59
simplify pytest benchmark options
d-v-b Nov 3, 2025
3033fbf
use --codspeed flag in benchmark ci
d-v-b Nov 3, 2025
4394226
measure walltime in ci
d-v-b Nov 3, 2025
7e4ca58
Merge branch 'main' into chore/benchmarks
d-v-b Nov 4, 2025
27ca426
Merge branch 'main' into chore/benchmarks
d-v-b Nov 5, 2025
93ea2e2
Update .github/workflows/codspeed.yml
d-v-b Nov 5, 2025
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
33 changes: 33 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CodSpeed Benchmarks

on:
push:
branches:
- "main"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:

permissions:
contents: read

jobs:
benchmarks:
name: Run benchmarks
runs-on: codspeed-macro
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # grab all branches and tags
- name: Set up Python
uses: actions/setup-python@v6
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch
- name: Run the benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: walltime
run: hatch run test.py3.11-1.26-minimal:pytest tests/benchmarks --codspeed
1 change: 1 addition & 0 deletions changes/3562.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add continuous performance benchmarking infrastructure.
9 changes: 8 additions & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,11 @@ If an existing Zarr format version changes, or a new version of the Zarr format
## Release procedure

Open an issue on GitHub announcing the release using the release checklist template:
[https://github.com/zarr-developers/zarr-python/issues/new?template=release-checklist.md](https://github.com/zarr-developers/zarr-python/issues/new?template=release-checklist.md>). The release checklist includes all steps necessary for the release.
[https://github.com/zarr-developers/zarr-python/issues/new?template=release-checklist.md](https://github.com/zarr-developers/zarr-python/issues/new?template=release-checklist.md>). The release checklist includes all steps necessary for the release.

## Benchmarks

Zarr uses [pytest-benchmark](https://pytest-benchmark.readthedocs.io/en/latest/) for running
performance benchmarks as part of our test suite. The benchmarks can be are found in `tests/benchmarks`.
By default pytest is configured to run these benchmarks as plain tests (i.e., no benchmarking). To run
a benchmark with timing measurements, use the `--benchmark-enable` when invoking `pytest`.
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ test = [
'numpydoc',
"hypothesis",
"pytest-xdist",
"pytest-benchmark",
"pytest-codspeed",
"packaging",
"tomlkit",
"uv",
Expand Down Expand Up @@ -181,6 +183,7 @@ run-pytest = "run"
run-verbose = "run-coverage --verbose"
run-mypy = "mypy src"
run-hypothesis = "run-coverage -nauto --run-slow-hypothesis tests/test_properties.py tests/test_store/test_stateful*"
run-benchmark = "pytest --benchmark-enable tests/benchmarks"
list-env = "pip list"

[tool.hatch.envs.gputest]
Expand Down Expand Up @@ -405,7 +408,10 @@ doctest_optionflags = [
"IGNORE_EXCEPTION_DETAIL",
]
addopts = [
"--durations=10", "-ra", "--strict-config", "--strict-markers",
"--benchmark-columns", "min,mean,stddev,outliers,rounds,iterations",
"--benchmark-disable", # run benchmark routines but don't do benchmarking
"--durations", "10",
"-ra", "--strict-config", "--strict-markers",
]
filterwarnings = [
"error",
Expand Down
Empty file added tests/benchmarks/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions tests/benchmarks/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass


@dataclass(kw_only=True, frozen=True)
class Layout:
shape: tuple[int, ...]
chunks: tuple[int, ...]
shards: tuple[int, ...] | None
82 changes: 82 additions & 0 deletions tests/benchmarks/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""
Benchmarks for end-to-end read/write performance of Zarr
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from tests.benchmarks.common import Layout

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture

from zarr.abc.store import Store
from zarr.core.common import NamedConfig
from operator import getitem, setitem
from typing import Any, Literal

import pytest

from zarr import create_array

CompressorName = Literal["gzip"] | None

compressors: dict[CompressorName, NamedConfig[Any, Any] | None] = {
None: None,
"gzip": {"name": "gzip", "configuration": {"level": 1}},
}


layouts: tuple[Layout, ...] = (
# No shards, just 1000 chunks
Layout(shape=(1_000_000,), chunks=(1000,), shards=None),
# 1:1 chunk:shard shape, should measure overhead of sharding
Layout(shape=(1_000_000,), chunks=(1000,), shards=(1000,)),
# One shard with all the chunks, should measure overhead of handling inner shard chunks
Layout(shape=(1_000_000,), chunks=(100,), shards=(10000 * 100,)),
)


@pytest.mark.parametrize("compression_name", [None, "gzip"])
@pytest.mark.parametrize("layout", layouts, ids=str)
@pytest.mark.parametrize("store", ["memory", "local"], indirect=["store"])
def test_write_array(
store: Store, layout: Layout, compression_name: CompressorName, benchmark: BenchmarkFixture
) -> None:
"""
Test the time required to fill an array with a single value
"""
arr = create_array(
store,
dtype="uint8",
shape=layout.shape,
chunks=layout.chunks,
shards=layout.shards,
compressors=compressors[compression_name], # type: ignore[arg-type]
fill_value=0,
)

benchmark(setitem, arr, Ellipsis, 1)


@pytest.mark.parametrize("compression_name", [None, "gzip"])
@pytest.mark.parametrize("layout", layouts, ids=str)
@pytest.mark.parametrize("store", ["memory", "local"], indirect=["store"])
def test_read_array(
store: Store, layout: Layout, compression_name: CompressorName, benchmark: BenchmarkFixture
) -> None:
"""
Test the time required to fill an array with a single value
"""
arr = create_array(
store,
dtype="uint8",
shape=layout.shape,
chunks=layout.chunks,
shards=layout.shards,
compressors=compressors[compression_name], # type: ignore[arg-type]
fill_value=0,
)
arr[:] = 1
benchmark(getitem, arr, Ellipsis)
43 changes: 43 additions & 0 deletions tests/benchmarks/test_indexing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture

from zarr.abc.store import Store

from operator import getitem

import pytest

from zarr import create_array

indexers = (
(0,) * 3,
(slice(None),) * 3,
(slice(0, None, 4),) * 3,
(slice(10),) * 3,
(slice(10, -10, 4),) * 3,
(slice(None), slice(0, 3, 2), slice(0, 10)),
)


@pytest.mark.parametrize("store", ["memory"], indirect=["store"])
@pytest.mark.parametrize("indexer", indexers, ids=str)
def test_slice_indexing(
store: Store, indexer: tuple[int | slice], benchmark: BenchmarkFixture
) -> None:
data = create_array(
store=store,
shape=(105,) * 3,
dtype="uint8",
chunks=(10,) * 3,
shards=None,
compressors=None,
filters=None,
fill_value=0,
)

data[:] = 1
benchmark(getitem, data, indexer)
Loading