Skip to content

Commit a596f99

Browse files
pkgdown fix and some benchmarks (#173)
1 parent e658217 commit a596f99

3 files changed

Lines changed: 675 additions & 0 deletions

File tree

‎inst/extdata/benchmark-xarray.py‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""Benchmark xarray read/write performance on zarr stores.
2+
3+
Called by benchmark-zarrs-vs-xarray.R with subcommands:
4+
5+
python benchmark-xarray.py generate <work_dir> <configs_json>
6+
python benchmark-xarray.py bench <work_dir> <configs_json> <n_iter>
7+
8+
generate: creates V2 zarr stores with numpy random data.
9+
bench: times xarray read/write operations and prints JSON results to stdout.
10+
11+
Prerequisites: zarr>=3, xarray, numpy, numcodecs
12+
"""
13+
14+
import json
15+
import os
16+
import sys
17+
import time
18+
19+
import numpy as np
20+
import numcodecs
21+
import zarr
22+
import xarray as xr
23+
24+
25+
# ---------------------------------------------------------------------------
26+
# Helpers
27+
# ---------------------------------------------------------------------------
28+
29+
def make_compressor(codec):
30+
if codec == "gzip":
31+
return numcodecs.GZip(level=6)
32+
return None
33+
34+
35+
def get_regions(cfg):
36+
"""Return (chunk_sel, subset_sel) as tuples of Python slices (0-based)."""
37+
shape = cfg["shape"]
38+
chunks = cfg["chunks"]
39+
chunk_sel = tuple(slice(0, c) for c in chunks)
40+
if shape == [100, 100, 100]:
41+
subset_sel = (slice(25, 75), slice(25, 75), slice(25, 75))
42+
else:
43+
subset_sel = (slice(50, 250), slice(50, 250), slice(10, 60))
44+
return chunk_sel, subset_sel
45+
46+
47+
def subset_size_mb(sel):
48+
"""Uncompressed size in MB of a float64 region defined by slices."""
49+
n = 1
50+
for s in sel:
51+
n *= (s.stop - s.start)
52+
return n * 8 / 1e6
53+
54+
55+
# ---------------------------------------------------------------------------
56+
# generate subcommand
57+
# ---------------------------------------------------------------------------
58+
59+
def cmd_generate(work_dir, configs):
60+
for cfg in configs:
61+
path = os.path.join(work_dir, f"data_{cfg['name']}.zarr")
62+
if os.path.exists(path):
63+
print(f" Store exists, skipping: {path}", file=sys.stderr)
64+
continue
65+
66+
shape = tuple(cfg["shape"])
67+
chunks = tuple(cfg["chunks"])
68+
compressor = make_compressor(cfg["codec"])
69+
70+
rng = np.random.default_rng(42)
71+
data = rng.random(shape)
72+
73+
root = zarr.open_group(path, mode="w", zarr_format=2)
74+
arr = root.create_array(
75+
"data",
76+
shape=shape,
77+
chunks=chunks,
78+
dtype="float64",
79+
fill_value=0.0,
80+
compressor=compressor,
81+
)
82+
arr[:] = data
83+
arr.update_attributes({"_ARRAY_DIMENSIONS": ["x", "y", "z"]})
84+
size_mb = np.prod(shape) * 8 / 1e6
85+
print(f" Generated {cfg['name']}: {path} ({size_mb:.1f} MB)",
86+
file=sys.stderr)
87+
88+
89+
# ---------------------------------------------------------------------------
90+
# bench subcommand
91+
# ---------------------------------------------------------------------------
92+
93+
def cmd_bench(work_dir, configs, n_iter):
94+
results = []
95+
96+
for cfg in configs:
97+
path = os.path.join(work_dir, f"data_{cfg['name']}.zarr")
98+
shape = tuple(cfg["shape"])
99+
size_mb = np.prod(shape) * 8 / 1e6
100+
chunk_sel, subset_sel = get_regions(cfg)
101+
chunk_mb = subset_size_mb(chunk_sel)
102+
sub_mb = subset_size_mb(subset_sel)
103+
104+
print(f" xarray benchmarking {cfg['name']} ...", file=sys.stderr)
105+
106+
# -- read_all --
107+
for i in range(n_iter):
108+
t0 = time.perf_counter()
109+
ds = xr.open_zarr(path, consolidated=False)
110+
_ = ds["data"].values # force full load
111+
ds.close()
112+
elapsed = time.perf_counter() - t0
113+
results.append({
114+
"scenario": "read_all",
115+
"array_config": cfg["name"],
116+
"codec": cfg["codec"],
117+
"implementation": "xarray",
118+
"iteration": i + 1,
119+
"time_seconds": round(elapsed, 6),
120+
"throughput_mb_s": round(size_mb / elapsed, 2),
121+
})
122+
123+
# -- read_chunk --
124+
for i in range(n_iter):
125+
t0 = time.perf_counter()
126+
ds = xr.open_zarr(path, consolidated=False)
127+
_ = ds["data"][chunk_sel].values
128+
ds.close()
129+
elapsed = time.perf_counter() - t0
130+
results.append({
131+
"scenario": "read_chunk",
132+
"array_config": cfg["name"],
133+
"codec": cfg["codec"],
134+
"implementation": "xarray",
135+
"iteration": i + 1,
136+
"time_seconds": round(elapsed, 6),
137+
"throughput_mb_s": round(chunk_mb / elapsed, 2),
138+
})
139+
140+
# -- read_subset --
141+
for i in range(n_iter):
142+
t0 = time.perf_counter()
143+
ds = xr.open_zarr(path, consolidated=False)
144+
_ = ds["data"][subset_sel].values
145+
ds.close()
146+
elapsed = time.perf_counter() - t0
147+
results.append({
148+
"scenario": "read_subset",
149+
"array_config": cfg["name"],
150+
"codec": cfg["codec"],
151+
"implementation": "xarray",
152+
"iteration": i + 1,
153+
"time_seconds": round(elapsed, 6),
154+
"throughput_mb_s": round(sub_mb / elapsed, 2),
155+
})
156+
157+
# -- write_all --
158+
rng = np.random.default_rng(42)
159+
write_data = rng.random(shape)
160+
ds_write = xr.Dataset({"data": (["x", "y", "z"], write_data)})
161+
encoding = {"data": {"chunks": tuple(cfg["chunks"])}}
162+
if cfg["codec"] == "gzip":
163+
encoding["data"]["compressor"] = numcodecs.GZip(level=6)
164+
elif cfg["codec"] == "none":
165+
encoding["data"]["compressor"] = None
166+
167+
for i in range(n_iter):
168+
out_path = os.path.join(
169+
work_dir, f"write_xarray_{cfg['name']}_{i}.zarr"
170+
)
171+
t0 = time.perf_counter()
172+
ds_write.to_zarr(out_path, mode="w", encoding=encoding,
173+
zarr_format=2)
174+
elapsed = time.perf_counter() - t0
175+
results.append({
176+
"scenario": "write_all",
177+
"array_config": cfg["name"],
178+
"codec": cfg["codec"],
179+
"implementation": "xarray",
180+
"iteration": i + 1,
181+
"time_seconds": round(elapsed, 6),
182+
"throughput_mb_s": round(size_mb / elapsed, 2),
183+
})
184+
185+
# JSON results to stdout; everything else went to stderr
186+
print(json.dumps(results))
187+
188+
189+
# ---------------------------------------------------------------------------
190+
# Main
191+
# ---------------------------------------------------------------------------
192+
193+
if __name__ == "__main__":
194+
if len(sys.argv) < 3:
195+
print("Usage: benchmark-xarray.py <generate|bench> <work_dir> "
196+
"<configs_json> [n_iter]", file=sys.stderr)
197+
sys.exit(1)
198+
199+
subcmd = sys.argv[1]
200+
work_dir = sys.argv[2]
201+
configs = json.loads(sys.argv[3])
202+
203+
if subcmd == "generate":
204+
cmd_generate(work_dir, configs)
205+
elif subcmd == "bench":
206+
n_iter = int(sys.argv[4]) if len(sys.argv) > 4 else 3
207+
cmd_bench(work_dir, configs, n_iter)
208+
else:
209+
print(f"Unknown subcommand: {subcmd}", file=sys.stderr)
210+
sys.exit(1)

0 commit comments

Comments
 (0)