Skip to content

Commit d445ce4

Browse files
committed
perf(scan): bound V2 row chunks by serialized size
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent 9859da6 commit d445ce4

2 files changed

Lines changed: 57 additions & 11 deletions

File tree

‎vortex-layout/src/scan/v2/mod.rs‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
//! Import this module's [`ScanBuilder`] to select V2 explicitly. It accepts a [`ScanFile`]
1212
//! directly and supports projection, row ranges, and selections. Unsupported layouts return an
1313
//! error; filters and limits are not part of this builder's API. Work is partitioned into fixed
14-
//! 65,536-row chunks, independently of the stored layout's chunk boundaries.
14+
//! chunks of up to 65,536 rows, independently of the stored layout's chunk boundaries.
15+
//! Full-file projections use smaller 8,192-row multiples when serialized bytes per row are high.
1516
1617
mod io;
1718
mod split;
@@ -63,19 +64,34 @@ use crate::scan::v2::io::segment_ranges;
6364
use crate::scan::v2::split::SplitPlan;
6465
use crate::segments::SegmentSource;
6566

66-
/// Eight of the default writer's 8,192-row blocks per scan task.
67-
const ROWS_PER_CHUNK: usize = 8 * 8192;
67+
const ROW_BLOCK_SIZE: usize = 8192;
68+
const MAX_ROWS_PER_CHUNK: usize = 8 * ROW_BLOCK_SIZE;
69+
const TARGET_BYTES_PER_CHUNK: u64 = 8 * 1024 * 1024;
70+
71+
/// Limit estimated bytes per task without changing the writer-aligned row granularity.
72+
fn rows_per_chunk(row_count: u64, locations: &[SegmentLocation]) -> usize {
73+
if row_count <= MAX_ROWS_PER_CHUNK as u64 {
74+
return MAX_ROWS_PER_CHUNK;
75+
}
76+
let bytes: u64 = locations
77+
.iter()
78+
.map(|location| u64::from(location.length))
79+
.sum();
80+
let bytes_per_row = bytes.div_ceil(row_count).max(1);
81+
let blocks = TARGET_BYTES_PER_CHUNK / bytes_per_row / ROW_BLOCK_SIZE as u64;
82+
blocks.clamp(1, 8) as usize * ROW_BLOCK_SIZE
83+
}
6884

6985
/// Fixed boundaries in the file's row domain, clipped to the requested range.
70-
fn row_chunks(rows: Range<u64>) -> Vec<Range<u64>> {
86+
fn row_chunks(rows: Range<u64>, rows_per_chunk: usize) -> Vec<Range<u64>> {
7187
if rows.is_empty() {
7288
return Vec::new();
7389
}
74-
let first = rows.start / ROWS_PER_CHUNK as u64 * ROWS_PER_CHUNK as u64;
90+
let first = rows.start / rows_per_chunk as u64 * rows_per_chunk as u64;
7591
(first..rows.end)
76-
.step_by(ROWS_PER_CHUNK)
92+
.step_by(rows_per_chunk)
7793
.map(|start| {
78-
start.max(rows.start)..start.saturating_add(ROWS_PER_CHUNK as u64).min(rows.end)
94+
start.max(rows.start)..start.saturating_add(rows_per_chunk as u64).min(rows.end)
7995
})
8096
.collect()
8197
}
@@ -282,7 +298,12 @@ impl<A: 'static + Send> PreparedScan<A> {
282298
} else {
283299
optimize(plan_row_idx_expression(builder.projection, root)?)?
284300
};
285-
let splits = row_chunks(builder.row_range.unwrap_or(0..row_count));
301+
let chunk_rows = if identity {
302+
rows_per_chunk(row_count, &file.locations)
303+
} else {
304+
MAX_ROWS_PER_CHUNK
305+
};
306+
let splits = row_chunks(builder.row_range.unwrap_or(0..row_count), chunk_rows);
286307
let io = Arc::new(SegmentScanIo::new(
287308
file.segments,
288309
segment_ranges(&file.locations),

‎vortex-layout/src/scan/v2/tests.rs‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ use vortex_scan::strict_sorted_buffer::StrictSortedBuffer;
3838
use vortex_session::VortexSession;
3939

4040
use super::PreparedScan;
41-
use super::ROWS_PER_CHUNK;
41+
use super::MAX_ROWS_PER_CHUNK;
4242
use super::ScanBuilder;
4343
use super::ScanFile;
4444
use super::row_chunks;
45+
use super::rows_per_chunk;
4546
use crate::LayoutRef;
4647
use crate::LayoutStrategy;
4748
use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
@@ -309,7 +310,31 @@ async fn arrow_stream_projects_across_chunks() -> VortexResult<()> {
309310
#[case::empty(5..5, vec![])]
310311
#[case::near_max(u64::MAX - 3..u64::MAX, vec![u64::MAX - 3..u64::MAX])]
311312
fn fixed_row_chunks(#[case] rows: Range<u64>, #[case] expected: Vec<Range<u64>>) {
312-
assert_eq!(row_chunks(rows), expected);
313+
assert_eq!(row_chunks(rows, 65_536), expected);
314+
}
315+
316+
#[rstest]
317+
#[case::empty(0, 0, 65_536)]
318+
#[case::single_chunk(1000, 100_000_000, 65_536)]
319+
#[case::no_bytes(100_000, 0, 65_536)]
320+
#[case::small_rows(1_000_000, 64_000_000, 65_536)]
321+
#[case::large_rows(1_000_000, 384_000_000, 16_384)]
322+
#[case::minimum(1_000_000, 2_000_000_000, 8192)]
323+
fn chunk_byte_budget(#[case] rows: u64, #[case] bytes: u32, #[case] expected: usize) {
324+
let locations = [SegmentLocation {
325+
offset: 0,
326+
length: bytes,
327+
alignment: Alignment::none(),
328+
}];
329+
assert_eq!(rows_per_chunk(rows, &locations), expected);
330+
}
331+
332+
#[test]
333+
fn smaller_chunks_keep_file_alignment() {
334+
assert_eq!(
335+
row_chunks(7500..33_000, 16_384),
336+
[7500..16_384, 16_384..32_768, 32_768..33_000]
337+
);
313338
}
314339

315340
struct CountingSegments {
@@ -362,7 +387,7 @@ async fn dictionary_values_are_reused_between_splits() -> VortexResult<()> {
362387
let splits = scan.split_plans();
363388
assert_eq!(
364389
splits.len(),
365-
(STORED_CHUNKS * STORED_CHUNK_ROWS).div_ceil(ROWS_PER_CHUNK)
390+
(STORED_CHUNKS * STORED_CHUNK_ROWS).div_ceil(MAX_ROWS_PER_CHUNK)
366391
);
367392
for (index, split) in splits.into_iter().enumerate() {
368393
let rows = split.scope.rows.clone();

0 commit comments

Comments
 (0)