Skip per-split zone-mask expansion when covering zones are uniform - #9274
Skip per-split zone-mask expansion when covering zones are uniform#9274joseph-isaacs wants to merge 2 commits into
Conversation
Merging this PR will improve performance by 13.05%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
|
The CodSpeed regression is not reachable from this diff, so I'm not pushing a change for it. Both flagged benchmarks live in
This PR modifies exactly one file, The shape of the report supports that reading: one benchmark −10.64% and another +10.01%, in Simulation mode, is the usual code/heap-layout artifact rather than a behavioral change. Worth noting alongside this, from CodSpeed's own footnote: no successful run existed on Leaving the acknowledgement on CodSpeed to a maintainer rather than dismissing it myself. Generated by Claude Code |
Polar Signals Profiling ResultsLatest Run
Previous Runs (25)
Powered by Polar Signals Cloud |
Benchmarks: PolarSignals Profiling 📖Vortex (geomean): 1.017x ➖ datafusion / vortex-file-compressed / ns (1.017x ➖, 0↑ 1↓)
No file size changes detected. |
Benchmarks: TPC-H SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.994x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.986x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed / ns (1.015x ➖, 0↑ 1↓)
duckdb / parquet / ns (0.999x ➖, 0↑ 0↓)
File Size Changes (9 files changed, -43.9% overall, 0↑ 9↓)
Totals:
|
Benchmarks: FineWeb NVMe 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.011x ➖, 0↑ 1↓)
datafusion / parquet / ns (1.024x ➖, 0↑ 1↓)
duckdb / vortex-file-compressed / ns (1.065x ➖, 0↑ 1↓)
duckdb / parquet / ns (0.985x ➖, 0↑ 0↓)
File Size Changes (2 files changed, -46.3% overall, 0↑ 2↓)
Totals:
|
Benchmarks: TPC-DS SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.993x ➖, 2↑ 0↓)
datafusion / parquet / ns (0.991x ➖, 3↑ 0↓)
duckdb / vortex-file-compressed / ns (0.973x ➖, 10↑ 2↓)
duckdb / parquet / ns (1.003x ➖, 3↑ 6↓)
File Size Changes (25 files changed, -43.5% overall, 0↑ 25↓)
Totals:
|
Benchmarks: Clickbench Sorted on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.985x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.026x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.984x ➖, 1↑ 0↓)
duckdb / parquet / ns (0.999x ➖, 0↑ 0↓)
File Size Changes (201 files changed, -42.8% overall, 42↑ 159↓)
Totals:
|
Benchmarks: TPC-H SF=10 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.998x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.003x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.998x ➖, 0↑ 0↓)
duckdb / parquet / ns (0.999x ➖, 0↑ 0↓)
File Size Changes (9 files changed, -44.0% overall, 0↑ 9↓)
Totals:
|
Benchmarks: Clickbench on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.980x ➖, 2↑ 1↓)
datafusion / parquet / ns (0.999x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed / ns (1.009x ➖, 1↑ 6↓)
duckdb / parquet / ns (0.995x ➖, 1↑ 1↓)
File Size Changes (101 files changed, -39.2% overall, 0↑ 101↓)
Totals:
|
Benchmarks: Statistical and Population Genetics 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
duckdb / vortex-file-compressed / ns (0.943x ➖, 3↑ 0↓)
duckdb / parquet / ns (1.003x ➖, 0↑ 0↓)
File Size Changes (2 files changed, -32.3% overall, 0↑ 2↓)
Totals:
|
ZonedReader::pruning_evaluation
Benchmarks: FineWeb S3 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.069x ➖, 0↑ 2↓)
datafusion / parquet / ns (0.966x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.951x ➖, 0↑ 0↓)
duckdb / parquet / ns (0.813x ➖, 1↑ 0↓)
|
0fa9c84 to
5eff4dc
Compare
Benchmarks: TPC-H SF=1 on S3 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.925x ➖, 1↑ 1↓)
datafusion / parquet / ns (0.967x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.968x ➖, 0↑ 0↓)
duckdb / parquet / ns (0.965x ➖, 0↑ 0↓)
|
5eff4dc to
ec8e480
Compare
Benchmarks: String Encoding 📖vortex / vortex-file-compressed / ms (0.999x ➖, 0↑ 0↓)
vortex / vortex-file-compressed / % (1.000x ➖, 0↑ 0↓)
|
Benchmarks: FineWeb NVMe 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.029x ➖, 0↑ 1↓)
datafusion / vortex-compact / ns (1.001x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.006x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.001x ➖, 0↑ 1↓)
duckdb / vortex-compact / ns (0.982x ➖, 1↑ 1↓)
duckdb / parquet / ns (1.000x ➖, 0↑ 0↓)
No file size changes detected. |
Benchmarks: Clickbench Sorted on NVME 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.078x ➖, 1↑ 3↓)
datafusion / vortex-compact / ns (1.107x ❌, 0↑ 1↓)
datafusion / parquet / ns (1.029x ➖, 1↑ 1↓)
duckdb / vortex-file-compressed / ns (1.023x ➖, 0↑ 2↓)
duckdb / vortex-compact / ns (1.038x ➖, 0↑ 1↓)
duckdb / parquet / ns (1.003x ➖, 0↑ 0↓)
File Size Changes (200 files changed, +0.0% overall, 100↑ 100↓)
Totals:
|
Benchmarks: Random Access 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
random-access / vortex-file-compressed / ns (0.999x ➖, 0↑ 0↓)
random-access / parquet / ns (1.003x ➖, 0↑ 0↓)
random-access / lance / ns (0.996x ➖, 0↑ 0↓)
|
Benchmarks: TPC-H SF=10 on S3 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-compact / ns (0.851x ➖, 3↑ 0↓)
datafusion / parquet / ns (1.032x ➖, 1↑ 2↓)
duckdb / vortex-compact / ns (0.961x ➖, 0↑ 0↓)
duckdb / parquet / ns (1.010x ➖, 0↑ 1↓)
|
ec8e480 to
cad24dc
Compare
Benchmarks: TPC-H SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.006x ➖, 0↑ 0↓)
datafusion / vortex-compact / ns (1.009x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.004x ➖, 1↑ 1↓)
duckdb / vortex-file-compressed / ns (1.006x ➖, 0↑ 1↓)
duckdb / vortex-compact / ns (1.007x ➖, 0↑ 0↓)
duckdb / parquet / ns (0.995x ➖, 1↑ 0↓)
No file size changes detected. |
Benchmarks: TPC-DS SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.008x ➖, 0↑ 1↓)
datafusion / vortex-compact / ns (1.008x ➖, 1↑ 2↓)
datafusion / parquet / ns (1.003x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed / ns (1.026x ➖, 2↑ 10↓)
duckdb / vortex-compact / ns (1.011x ➖, 2↑ 5↓)
duckdb / parquet / ns (1.003x ➖, 4↑ 4↓)
No file size changes detected. |
Benchmarks: PolarSignals Profiling 📖Vortex (geomean): 0.978x ➖ datafusion / vortex-file-compressed / ns (0.978x ➖, 1↑ 0↓)
File Size Changes (1 files changed, +0.0% overall, 1↑ 0↓)
Totals:
|
Benchmarks: Appian on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-compact / ns (1.000x ➖, 0↑ 0↓)
datafusion / parquet / ns (0.999x ➖, 0↑ 0↓)
duckdb / vortex-compact / ns (0.994x ➖, 0↑ 0↓)
duckdb / parquet / ns (1.002x ➖, 0↑ 0↓)
File Size Changes (10 files changed, -63.8% overall, 0↑ 10↓)
Totals:
|
Benchmarks: Statistical and Population Genetics 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
duckdb / vortex-file-compressed / ns (1.022x ➖, 0↑ 1↓)
duckdb / vortex-compact / ns (0.979x ➖, 1↑ 1↓)
duckdb / parquet / ns (1.001x ➖, 0↑ 0↓)
No file size changes detected. |
`ZonedReader::pruning_evaluation` expanded the cached zone-level pruning mask into a row-aligned bit buffer for every split, then intersected it with the incoming mask. That cost a `Vec` of zone lengths (built eagerly, before the future was polled), a `BitBufferMut` of the full split length, a popcount over it, and a bitand, on every split of every scan. For most splits the zones covering that split are uniform: either none are pruned or all of them are. Both collapse to a constant stats mask, so counting the covered zone bits first - a handful of bit reads via `BitBuffer::count_range` - lets us skip the expansion entirely: - no covered zone pruned: the stats mask is all-true, so forward the incoming mask unchanged, - every covered zone pruned: return `Mask::new_false` directly, - otherwise: fall through to the existing expansion. The zone-length computation now lives on the non-uniform path, so uniform splits allocate nothing. Results and masks are unchanged. The equivalence is exact, not just bit-equal. `Mask::from_buffer` canonicalises an all-ones buffer to `AllTrue` and an all-zeros buffer to `AllFalse`, and owned-left `bitand` short circuits on both (`(_, AllOr::All) => self` and `(_, AllOr::None) => new_false`). So the old code already returned the incoming mask unchanged in the no-prune case, after an allocation and two O(n) passes to rediscover that. The new fast paths return the same `Mask` variant, not merely the same bits. No path in this change does more work than before; the only addition is a `count_range` over the covering zones on the non-uniform path. This is a simplification, not a demonstrated speedup. Two full benchmark sweeps were run over the same commit. Every suite returned "No clear signal", and four of the six suites common to both sweeps flipped sign: statpopgen -6.0% then +1.8%, clickbench-sorted -2.8% then +1.4%, tpcds -1.4% then +0.7%, polarsignals +1.7% then -2.0%. The apparent statpopgen win came from an anomalously slow baseline: q10 measured 4.26s base / 2.94s HEAD in the first sweep and 2.88s base / 3.12s HEAD in the second, so HEAD was stable and the base was not. A tight Parquet control does not rule this out - it bounds host drift, not per-path measurement variance. What holds up is the absence of a regression. The lowest-noise suites all read zero: random-access -0.1% (controls 0.98-1.04), appian +0.2% (all rows 0.98-1.02), string encoding +0.1% (sizes byte-identical), tpch sf=10 -0.3% (controls 0.96-1.03). CodSpeed reports no change across 1934 benchmarks. No suite in either sweep showed a credible regression. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
cad24dc to
3062ebb
Compare
Benchmarks: TPC-H SF=1 on S3 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (1.031x ➖, 1↑ 2↓)
datafusion / vortex-compact / ns (1.105x ➖, 1↑ 3↓)
datafusion / parquet / ns (0.981x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.021x ➖, 0↑ 1↓)
duckdb / vortex-compact / ns (0.916x ➖, 1↑ 0↓)
duckdb / parquet / ns (0.991x ➖, 0↑ 0↓)
|
Benchmarks: Clickbench on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.990x ➖, 1↑ 1↓)
datafusion / vortex-compact / ns (1.000x ➖, 1↑ 0↓)
datafusion / parquet / ns (1.002x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (0.995x ➖, 4↑ 2↓)
duckdb / vortex-compact / ns (0.992x ➖, 4↑ 2↓)
duckdb / parquet / ns (0.999x ➖, 1↑ 1↓)
No file size changes detected. |
Benchmarks: TPC-H SF=10 on NVME 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.987x ➖, 0↑ 0↓)
datafusion / vortex-compact / ns (0.993x ➖, 0↑ 0↓)
datafusion / parquet / ns (1.003x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.002x ➖, 0↑ 0↓)
duckdb / vortex-compact / ns (1.002x ➖, 0↑ 0↓)
duckdb / parquet / ns (1.011x ➖, 0↑ 1↓)
No file size changes detected. |
Benchmarks: FineWeb S3 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed / ns (0.985x ➖, 0↑ 1↓)
datafusion / vortex-compact / ns (0.929x ➖, 1↑ 1↓)
datafusion / parquet / ns (0.982x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed / ns (1.014x ➖, 0↑ 0↓)
duckdb / vortex-compact / ns (0.977x ➖, 0↑ 0↓)
duckdb / parquet / ns (1.141x ➖, 0↑ 1↓)
|
Benchmarks: Compression 📖vortex / vortex-file-compressed / ns (0.991x ➖, 3↑ 0↓)
vortex / vortex-file-compressed / bytes (1.000x ➖, 0↑ 0↓)
vortex / vortex-file-compressed / ratio (0.999x ➖, 2↑ 1↓)
vortex / parquet / ns (0.992x ➖, 0↑ 0↓)
vortex / parquet / bytes (1.000x ➖, 0↑ 0↓)
|
The uniform fast path in `ZonedReader::pruning_evaluation` had no measurement behind it - the SQL suites cannot resolve a per-split cost against per-split decode, so two full sweeps disagreed with each other and neither confirmed nor refuted anything. This benchmarks the changed code directly. `expand` reproduces the old behaviour (build a row-aligned buffer, popcount it, intersect) and `uniform` is the fast path that counts the covering zone bits instead. The incoming mask is a `Values` mask, not `AllTrue`, so `bitand` takes the `(AllOr::Some, AllOr::All) => self` branch that the real code hits rather than the all-true short circuit. Medians on a c6id-class host, one zone per split at the default 8192-row zone and block length: no zone pruned 7.4 ns vs 147 ns expanded (~20x) all zones pruned 14.5 ns vs 151 ns expanded (~10x) The gap widens with split length - at 65536 rows over 8 zones the no-prune case is 7.4 ns against 420 ns. The absolute saving is roughly 140-400 ns per split per pruning expression, which is why it does not surface in a query-level benchmark: a split then decodes 8192 rows across every projected column. `mixed_zones` covers the case where the covering zones genuinely disagree and the expansion still has to run. It needs at least two zones to be meaningful, so it takes its own argument set - a one-zone split is uniform by construction and would silently measure a fast path. There the added `count_range` shows a small overhead, within a few percent and at the edge of what this environment resolves, so it is bounded rather than claimed to be free. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Rationale for this change
ZonedReader::pruning_evaluation(the v1LayoutReaderscan path) caches the zone-levelpruning mask per expression, but every split still expands it into a row-aligned buffer
inside the returned
MaskFuture:Vecof per-zone lengths, built eagerly — before the future is even polled,BitBufferMutof the full split length filled viaappend_nper zone,Mask::from(builder.freeze())— a popcount pass,bitandagainst the incoming mask.For most splits the zones covering that split are uniform — either none are pruned or
all are — and the whole expansion collapses to a constant. This PR detects that case and
skips the work.
What changes are included in this PR?
vortex-layout/src/layouts/zoned/reader.rs, plus a new benchmark for the path it changes.After resolving the zone-level pruning mask, count the pruned bits among only the zones
covering this split (
zone_range) before building anything. That count is a handful ofbit reads via
BitBuffer::count_range— it does not scan the whole zone mask — and itshort-circuits both uniform cases:
continue with the incoming mask unchanged (still forwarded to the data child evaluation,
as before);
Mask::new_false(mask.len())directly;The per-zone length computation moved onto the non-uniform path and inlined into the
builder loop, dropping the intermediate
Vec, so uniform splits allocate nothing.first_row_offsetbecame a private free function so the non-uniform path can call it fromthe
'staticfuture without capturing the reader.The default zone length and row block size are both 8192, so a typical split covers one
zone and the uniform test essentially always succeeds.
Why the fast paths are exactly equivalent
Not merely bit-equal — the same
Maskvariant.Mask::from_buffer(
vortex-mask/src/lib.rs:189) canonicalises:and owned-left
bitand(vortex-mask/src/bitops.rs:41) short-circuits on both:So in the no-prune case the old code built an all-ones buffer, ran an O(n)
true_count(), gotAllTrue, and thenbitandreturnedself— the incoming mask,unchanged. The buffer was allocated, filled, counted and dropped to rediscover a value the
code already held. The all-pruned case is the same story via
AllFalse→Mask::new_false.Benchmarks
The change is measured directly, by a new
vortex-layout/benches/zone_mask_expansion.rs.expandreproduces the old behaviour anduniformis the fast path; the incoming mask is aValuesmask, notAllTrue, sobitandtakes the(AllOr::Some, AllOr::All) => selfbranch the real code hits rather than the all-true short circuit. Medians, split of 8192
rows over one zone:
So the saving is roughly 140–400 ns per split, per pruning expression — real work
removed, but small next to what a split then does: decode 8192 rows across every projected
column.
mixed_zonescovers the case where the covering zones genuinely disagree and the expansionstill runs. There the added
count_rangeshows a small overhead — within a few percent, atthe edge of what the host resolves — so it is bounded rather than claimed to be free.
The SQL suites cannot resolve this, across three sweeps
Only the SQL suites exercise
pruning_evaluationat all:random-access-benchreads rowswithout filter pushdown,
string-benchis a compression benchmark, and CodSpeed'smicrobenchmarks do not cover
vortex-layout. All three report clean, but none of them canspeak to this change.
Three full sweeps were run:
bench-sqlon 0fa9c84,bench-allon ec8e480,and
bench-allon 3062ebb.Every suite in all three returned "No clear signal". Attributed Vortex impact:
Given a ~150 ns per-split saving, this is the expected outcome: the effect sits far below
the noise floor, so the sweeps measure drift rather than the change. Five of eight repeated
suites changed sign at least once, and the two that did not — ClickBench (−0.3/−1.0/−0.6)
and TPC-H SF=1 (+1.2/+1.1/+0.7) — held opposite signs. They cannot both be caused by a
change that provably removes work on the dominant path, so both are per-suite systematic
offsets, not signal.
Three specific failures of the methodology, each reproduced rather than asserted:
Baselines are not stable. Sweep 3 alone compared against six different bases —
0f0390a6,ad5de229,74a2b861,39fde8c1,2b3109a8,adab5b84— depending on thesuite.
A tight Parquet control does not bound Vortex-side variance. StatPopGen's control is
the tightest in the matrix (all eleven rows 0.98–1.01, geomean 1.001x), yet in that same
run
q05moved 1.25 🚨 andq10moved 0.72 🚀. Host-wide drift cannot produce a 25% and a28% swing under a 1% control.
The attribution step adds variance of its own. TPC-H SF=10's raw Vortex geomean was
0.998x, 1.002x, 0.996x across the three sweeps — stable and centred — while the
attributed figure swung −0.3% / +1.2% / −1.1% because the Parquet control drifted. In
sweep 3 that drift traces to one control row:
tpch_q01/duckdb:parquetat 1.54.The
statpopgen_q10history is the clearest single illustration. HEAD measured 2.94s,3.12s, 3.09s across the three sweeps — a tight band. The baseline measured 4.26s,
2.88s, 4.27s — bimodal, and the anomalous value recurred to within 0.01s. The original
−6.0% reading was a slow baseline being read as a fast HEAD.
ClickBench Sorted is the loudest suite (+4.4%, and the only ❌ in the matrix) and also the
least usable:
clickbench-sorted_q23alone drives it, measuring 2.0× faster and 2.6×slower on two formats within sweep 2, then 1.98× and 2.46× slower in sweep 3. Its
DataFusion Parquet control carries a 1.47 🚨, and its
.vortexfiles differ between baseand HEAD — 100 larger, 100 smaller, up to ±2.3% per file — so the two sides decode
materially different bytes. That compressor nondeterminism recurs elsewhere:
Euro2016incompress-bench differed by +18KB in sweep 2 and −234KB in sweep 3.
No SQL suite in any sweep showed a credible regression, and the lowest-noise ones read
zero: Appian NVMe +0.2% / −0.3% with every control row in 0.98–1.02, StatPopGen −0.1%
against a 1.001x control, FineWeb +0.0% where suite and control moved identically (1.003x
each).
The four S3 suites are excluded throughout: all flagged "environment too noisy", with
Parquet controls drifting −3.4%, −8.5% and −11.4%, one control row moving 5.34s → 0.85s,
and different baselines used across runs. Two sweep-3 examples show why they are excluded,
and both happen to point the favourable way:
control of 1.141x — where that control is set by a single row,
fineweb_q06/duckdb:parquet, at 3.43 (1.57s → 5.39s) on a path this change cannottouch.
Its DataFusion rows contradict themselves query by query within the one run:
tpch_q04measured 0.65 🚀 onvortex-compactand 1.62 🚨 onparquet;tpch_q06measured 0.71 and 1.33. Same query, same host, same run, opposite ~1.5x moves — and the
Parquet side cannot be touched by this change at all. These are S3-bound queries whose
cost is dominated by network IO, so a ~150 ns per-split saving cannot register at 11%.
Neither is evidence for this PR, and they are listed here so the two loudest favourable
numbers on the page are not mistaken for one.
Earlier revisions of this description made two claims that did not survive: local
microbenchmark numbers (
clickbench_plan_perf,SCAN_VERSION=v1) of 6–9% at TPC-H SF=10,and a correlation between sorted data layouts and favourable results. Both rested on single
measurements that reversed on repetition, and have been removed rather than reconciled.
Tests
Added
test_stats_pruning_mask_zone_ranges, anrstestover row ranges covering onlypruned zones, only kept zones, a single pruned zone, a mixed partial range, and an empty
range — exercising both new fast paths and the unchanged expansion path.
What APIs are changed? Are there any user-facing changes?
None.
ZonedReader::first_row_offsetwas apub(crate)helper used only within this file;it is now a private free function. No public API or behavior change.
Checks run
cargo test -p vortex-layout -p vortex-file— passcargo clippy -p vortex-layout --all-targets --all-features— cleancargo +nightly fmt --all,git diff --check— cleancargo bench -p vortex-layout --bench zone_mask_expansion— figures aboveNot run: workspace-wide tests, Python/docs checks — the change is confined to one Rust file
plus a new bench target, with no public API surface.