Skip to content

Commit f880575

Browse files
authored
fix(bench): stabilize random access timing modes (#9631)
PR #9385 added an OS cache drop before each random-access benchmark combination. That change exposed page-cache state in cached mode. It caused large run-to-run variance, especially for Lance `feature-vectors/correlated`. This PR defines the two random-access modes explicitly. Cached mode performs a one-second untimed warm-up before measurements. The warm-up establishes a consistent cached baseline after each OS cache drop. Reopen mode includes file open and metadata parsing inside every timed iteration. Both modes prepare missing benchmark data before timing. This also fixes `-footer` measurements that previously excluded their advertised open cost. --------- Signed-off-by: Will Manning <will@willmanning.io>
1 parent 1fe8dda commit f880575

2 files changed

Lines changed: 31 additions & 14 deletions

File tree

benchmarks/random-access-bench/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ Two access patterns are generated with a fixed seed (see [`src/main.rs`](./src/m
1717
simulating lookups with no locality.
1818

1919
Each pattern runs over four datasets (`taxi`, `feature-vectors`, `nested-lists`,
20-
`nested-structs`) in Arrow IPC, Parquet, Lance, and Vortex. Each format uses a cached open file
21-
handle and a per-lookup reopen mode. CI drives the full matrix via
20+
`nested-structs`) in Arrow IPC, Parquet, Lance, and Vortex. Cached mode performs a one-second
21+
untimed warm-up, then reuses the open file handle. Reopen mode includes file open and metadata
22+
work in each timed iteration. CI drives the full matrix via
2223
[`scripts/random-access-split.py`](../../scripts/random-access-split.py).
2324

2425
## Running locally

benchmarks/random-access-bench/src/lib.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ const CLUSTER_SIZE: usize = 20;
6666
/// Expected number of indices for the Poisson (uniform) pattern.
6767
const POISSON_EXPECTED_COUNT: usize = 100;
6868

69+
/// Untimed warm-up duration for cached accessors.
70+
const CACHED_WARMUP_DURATION: Duration = Duration::from_secs(1);
71+
6972
/// Generate indices for the given dataset and access pattern.
7073
fn generate_indices(dataset: &dyn BenchDataset, pattern: AccessPattern) -> Vec<u64> {
7174
let row_count = dataset.row_count();
@@ -115,7 +118,7 @@ fn generate_indices(dataset: &dyn BenchDataset, pattern: AccessPattern) -> Vec<u
115118
/// Controls whether the file handle is reused or reopened each iteration.
116119
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
117120
pub enum OpenMode {
118-
/// Reuse the file handle across iterations (cached metadata).
121+
/// Warm and reuse the file handle across timed iterations.
119122
#[clap(name = "cached")]
120123
Cached,
121124
/// Reopen the file each iteration (includes footer parsing).
@@ -132,10 +135,11 @@ pub enum OpenMode {
132135

133136
/// Run a random access benchmark.
134137
///
135-
/// Runs the take operation repeatedly until the time limit is reached,
136-
/// collecting timing for each run. When `reopen` is true, the accessor is
137-
/// recreated from scratch before each iteration so that file metadata
138-
/// parsing is included in the timing.
138+
/// Runs the take operation repeatedly until the time limit is reached and
139+
/// collects timing for each run. Cached mode performs an untimed warm-up first
140+
/// so that host page-cache state does not affect the recorded measurements.
141+
/// Both modes prepare the format file before timing starts. Reopen mode creates
142+
/// the accessor inside each timed iteration.
139143
#[expect(clippy::too_many_arguments)]
140144
async fn benchmark_random_access(
141145
dataset: &dyn BenchDataset,
@@ -148,23 +152,35 @@ async fn benchmark_random_access(
148152
reopen: bool,
149153
) -> Result<RandomAccessRun> {
150154
let time_limit = Duration::from_secs(time_limit_secs);
151-
let overall_start = Instant::now();
152155
let mut runs = Vec::new();
153-
let mut accessor = open_accessor(dataset, format).await?;
156+
let prepared_accessor = open_accessor(dataset, format).await?;
157+
let cached_accessor = (!reopen).then_some(prepared_accessor);
158+
159+
if let Some(accessor) = &cached_accessor {
160+
let warmup_start = Instant::now();
161+
loop {
162+
drop(accessor.take(indices).await?);
163+
if warmup_start.elapsed() >= CACHED_WARMUP_DURATION {
164+
break;
165+
}
166+
}
167+
}
168+
169+
let overall_start = Instant::now();
154170

155171
loop {
156172
let start = Instant::now();
157-
let arr = accessor.take(indices).await?;
173+
let arr = if let Some(accessor) = &cached_accessor {
174+
accessor.take(indices).await?
175+
} else {
176+
open_accessor(dataset, format).await?.take(indices).await?
177+
};
158178
runs.push(start.elapsed());
159179
drop(arr);
160180

161181
if overall_start.elapsed() >= time_limit {
162182
break;
163183
}
164-
165-
if reopen {
166-
accessor = open_accessor(dataset, format).await?;
167-
}
168184
}
169185

170186
let timing = TimingMeasurement {

0 commit comments

Comments
 (0)