Skip to content

Commit 10cbfb8

Browse files
committed
Precompute natural split assignment bytes
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 465fab3 commit 10cbfb8

5 files changed

Lines changed: 122 additions & 68 deletions

File tree

vortex-datafusion/src/persistent/opener.rs

Lines changed: 109 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ use futures::FutureExt;
3737
use futures::StreamExt;
3838
use futures::TryStreamExt;
3939
use futures::stream;
40-
use itertools::Itertools;
4140
use object_store::path::Path;
4241
use tracing::Instrument;
4342
use vortex::array::VortexSessionExecute;
@@ -98,8 +97,8 @@ pub(crate) struct VortexOpener {
9897
/// To save on the overhead of reparsing FlatBuffers and rebuilding the layout tree, we cache
9998
/// a file reader the first time we read a file.
10099
pub layout_readers: Arc<DashMap<Path, Weak<dyn LayoutReader>>>,
101-
/// Shared full-file natural split ranges keyed by file path.
102-
pub natural_split_ranges: Arc<DashMap<Path, Arc<[Range<u64>]>>>,
100+
/// Shared full-file natural splits keyed by file path.
101+
pub natural_splits: Arc<DashMap<Path, Arc<NaturalSplits>>>,
103102
/// Whether the query has output ordering specified
104103
pub has_output_ordering: bool,
105104

@@ -140,7 +139,7 @@ impl FileOpener for VortexOpener {
140139
let unified_file_schema = Arc::clone(self.table_schema.file_schema());
141140
let limit = self.limit;
142141
let layout_readers = Arc::clone(&self.layout_readers);
143-
let natural_split_ranges = Arc::clone(&self.natural_split_ranges);
142+
let natural_splits = Arc::clone(&self.natural_splits);
144143
let has_output_ordering = self.has_output_ordering;
145144
let scan_concurrency = self.scan_concurrency;
146145

@@ -370,17 +369,16 @@ impl FileOpener for VortexOpener {
370369
if byte_range.start != 0 || byte_range.end != file.object_meta.size {
371370
// Full-file scans already cover every natural split. Only translate the
372371
// byte range back into row boundaries when DataFusion has trimmed the file.
373-
let natural_split_ranges = natural_split_ranges_for_file(
374-
natural_split_ranges.as_ref(),
372+
let natural_splits = natural_splits_for_file(
373+
natural_splits.as_ref(),
375374
&file.object_meta.location,
376375
&layout_reader,
376+
file.object_meta.size,
377377
)?;
378378

379-
let Some(row_range) = split_aligned_row_range(
380-
byte_range,
381-
file.object_meta.size,
382-
natural_split_ranges.as_ref(),
383-
) else {
379+
let Some(row_range) =
380+
split_aligned_row_range(byte_range, natural_splits.as_ref())
381+
else {
384382
return Ok(stream::empty().boxed());
385383
};
386384

@@ -482,72 +480,108 @@ impl FileOpener for VortexOpener {
482480
}
483481
}
484482

485-
fn natural_split_ranges_for_file(
486-
natural_split_ranges: &DashMap<Path, Arc<[Range<u64>]>>,
483+
#[derive(Debug)]
484+
pub(crate) struct NaturalSplits {
485+
row_boundaries: Arc<[u64]>,
486+
assignment_bytes: Box<[u64]>,
487+
}
488+
489+
impl NaturalSplits {
490+
fn new(row_boundaries: Arc<[u64]>, total_size: u64) -> Self {
491+
let row_count = row_boundaries.last().copied().unwrap_or_default();
492+
let assignment_bytes = if row_count == 0 {
493+
Box::default()
494+
} else {
495+
row_boundaries
496+
.windows(2)
497+
.enumerate()
498+
.map(|(idx, boundaries)| {
499+
split_assignment_byte(
500+
idx,
501+
&(boundaries[0]..boundaries[1]),
502+
row_count,
503+
total_size,
504+
)
505+
})
506+
.collect()
507+
};
508+
509+
debug_assert!(assignment_bytes.is_sorted());
510+
debug_assert_eq!(
511+
assignment_bytes.len() + usize::from(!row_boundaries.is_empty()),
512+
row_boundaries.len()
513+
);
514+
515+
Self {
516+
row_boundaries,
517+
assignment_bytes,
518+
}
519+
}
520+
}
521+
522+
fn natural_splits_for_file(
523+
natural_splits: &DashMap<Path, Arc<NaturalSplits>>,
487524
path: &Path,
488525
layout_reader: &Arc<dyn LayoutReader>,
489-
) -> DFResult<Arc<[Range<u64>]>> {
490-
if let Some(split_ranges) = natural_split_ranges.get(path) {
491-
return Ok(Arc::clone(split_ranges.value()));
526+
total_size: u64,
527+
) -> DFResult<Arc<NaturalSplits>> {
528+
if let Some(splits) = natural_splits.get(path) {
529+
return Ok(Arc::clone(splits.value()));
492530
}
493531

494-
let split_ranges = compute_natural_split_ranges(layout_reader.as_ref())?;
495-
496-
match natural_split_ranges.entry(path.clone()) {
532+
// Compute while holding the entry so concurrent partitions opening the same file wait
533+
// for the winner instead of all walking the layout tree; the redundant walks contend on
534+
// the lazily-initialized layout children and dominate the cost of the computation itself.
535+
match natural_splits.entry(path.clone()) {
497536
Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
498537
Entry::Vacant(entry) => {
499-
entry.insert(Arc::clone(&split_ranges));
500-
Ok(split_ranges)
538+
let splits = compute_natural_splits(layout_reader.as_ref(), total_size)?;
539+
entry.insert(Arc::clone(&splits));
540+
Ok(splits)
501541
}
502542
}
503543
}
504544

505-
fn compute_natural_split_ranges(layout_reader: &dyn LayoutReader) -> DFResult<Arc<[Range<u64>]>> {
545+
fn compute_natural_splits(
546+
layout_reader: &dyn LayoutReader,
547+
total_size: u64,
548+
) -> DFResult<Arc<NaturalSplits>> {
506549
let row_count = layout_reader.row_count();
507550
let row_range = 0..row_count;
508-
let split_points: Vec<_> = SplitBy::Layout
551+
let row_boundaries = SplitBy::Layout
509552
.splits(layout_reader, &row_range, &[FieldMask::All])
510-
.map_err(|e| exec_datafusion_err!("Failed to compute Vortex natural splits: {e}"))?
511-
.into_iter()
512-
.tuple_windows()
513-
.map(|(s, e)| s..e)
514-
.collect::<Vec<_>>();
553+
.map_err(|e| exec_datafusion_err!("Failed to compute Vortex natural splits: {e}"))?;
515554

516-
Ok(split_points.into())
555+
Ok(Arc::new(NaturalSplits::new(
556+
row_boundaries.into(),
557+
total_size,
558+
)))
517559
}
518560

519561
/// Translate a DataFusion byte range to the contiguous natural split ranges it owns.
520562
/// Most splits are assigned by midpoint, but the leading split stays with the range that owns
521563
/// byte 0 so a tiny first byte range still claims the first rows.
522564
fn split_aligned_row_range(
523565
byte_range: Range<u64>,
524-
total_size: u64,
525-
split_ranges: &[Range<u64>],
566+
natural_splits: &NaturalSplits,
526567
) -> Option<Range<u64>> {
527568
if byte_range.start >= byte_range.end {
528569
return None;
529570
}
530571

531-
let row_count = split_ranges.last().map(|split| split.end)?;
532-
if row_count == 0 {
572+
let first_split = natural_splits
573+
.assignment_bytes
574+
.partition_point(|&assignment_byte| assignment_byte < byte_range.start);
575+
let after_last_split = natural_splits
576+
.assignment_bytes
577+
.partition_point(|&assignment_byte| assignment_byte < byte_range.end);
578+
if first_split == after_last_split {
533579
return None;
534580
}
535581

536-
let mut owned_splits = split_ranges
537-
.iter()
538-
.enumerate()
539-
.filter_map(|(idx, split_range)| {
540-
let assignment_byte = split_assignment_byte(idx, split_range, row_count, total_size);
541-
byte_range.contains(&assignment_byte).then_some(split_range)
542-
});
543-
544-
let first_split = owned_splits.next()?;
545-
let mut row_range = first_split.start..first_split.end;
546-
for split_range in owned_splits {
547-
row_range.end = split_range.end;
548-
}
549-
550-
Some(row_range)
582+
Some(
583+
natural_splits.row_boundaries[first_split]..natural_splits.row_boundaries[after_last_split],
584+
)
551585
}
552586

553587
fn split_assignment_byte(
@@ -675,6 +709,15 @@ mod tests {
675709
}
676710
}
677711

712+
fn natural_splits(total_size: u64, split_ranges: &[Range<u64>]) -> NaturalSplits {
713+
let mut row_boundaries = Vec::with_capacity(split_ranges.len() + 1);
714+
if let Some(first) = split_ranges.first() {
715+
row_boundaries.push(first.start);
716+
row_boundaries.extend(split_ranges.iter().map(|range| range.end));
717+
}
718+
NaturalSplits::new(row_boundaries.into(), total_size)
719+
}
720+
678721
#[rstest]
679722
#[case(0..3, 10, vec![0..2, 2..5, 5..10], Some(0..2))]
680723
#[case(3..7, 10, vec![0..2, 2..5, 5..10], Some(2..5))]
@@ -688,7 +731,7 @@ mod tests {
688731
#[case] expected: Option<Range<u64>>,
689732
) {
690733
assert_eq!(
691-
split_aligned_row_range(byte_range, total_size, &split_ranges),
734+
split_aligned_row_range(byte_range, &natural_splits(total_size, &split_ranges)),
692735
expected
693736
);
694737
}
@@ -697,10 +740,11 @@ mod tests {
697740
fn test_split_aligned_ranges_cover_splits_exactly_once() {
698741
let split_ranges = vec![0..1, 1..4, 4..10, 10..13];
699742
let byte_ranges = [0..4, 4..8, 8..12, 12..16];
743+
let natural_splits = natural_splits(16, &split_ranges);
700744

701745
let assigned = byte_ranges
702746
.into_iter()
703-
.filter_map(|byte_range| split_aligned_row_range(byte_range, 16, &split_ranges))
747+
.filter_map(|byte_range| split_aligned_row_range(byte_range, &natural_splits))
704748
.collect::<Vec<_>>();
705749

706750
assert_eq!(assigned, vec![0..4, 4..10, 10..13]);
@@ -731,6 +775,15 @@ mod tests {
731775
}
732776
}
733777

778+
#[test]
779+
fn test_split_aligned_row_range_keeps_colliding_assignments_together() {
780+
let natural_splits = natural_splits(2, &[0..1, 1..2, 2..3, 3..4]);
781+
782+
assert_eq!(natural_splits.assignment_bytes.as_ref(), [0, 0, 1, 1]);
783+
assert_eq!(split_aligned_row_range(0..1, &natural_splits), Some(0..2));
784+
assert_eq!(split_aligned_row_range(1..2, &natural_splits), Some(2..4));
785+
}
786+
734787
async fn write_arrow_to_vortex(
735788
object_store: Arc<dyn ObjectStore>,
736789
path: &str,
@@ -767,7 +820,7 @@ mod tests {
767820
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
768821
df_metrics: ExecutionPlanMetricsSet::new(),
769822
layout_readers: Default::default(),
770-
natural_split_ranges: Default::default(),
823+
natural_splits: Default::default(),
771824
has_output_ordering: false,
772825
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
773826
file_metadata_cache: None,
@@ -1098,7 +1151,7 @@ mod tests {
10981151
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
10991152
df_metrics: ExecutionPlanMetricsSet::new(),
11001153
layout_readers: Default::default(),
1101-
natural_split_ranges: Default::default(),
1154+
natural_splits: Default::default(),
11021155
has_output_ordering: false,
11031156
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
11041157
file_metadata_cache: None,
@@ -1185,7 +1238,7 @@ mod tests {
11851238
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
11861239
df_metrics: ExecutionPlanMetricsSet::new(),
11871240
layout_readers: Default::default(),
1188-
natural_split_ranges: Default::default(),
1241+
natural_splits: Default::default(),
11891242
has_output_ordering: false,
11901243
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
11911244
file_metadata_cache: None,
@@ -1342,7 +1395,7 @@ mod tests {
13421395
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
13431396
df_metrics: ExecutionPlanMetricsSet::new(),
13441397
layout_readers: Default::default(),
1345-
natural_split_ranges: Default::default(),
1398+
natural_splits: Default::default(),
13461399
has_output_ordering: false,
13471400
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
13481401
file_metadata_cache: None,
@@ -1402,7 +1455,7 @@ mod tests {
14021455
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
14031456
df_metrics: ExecutionPlanMetricsSet::new(),
14041457
layout_readers: Default::default(),
1405-
natural_split_ranges: Default::default(),
1458+
natural_splits: Default::default(),
14061459
has_output_ordering: false,
14071460
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
14081461
file_metadata_cache: None,
@@ -1611,7 +1664,7 @@ mod tests {
16111664
metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
16121665
df_metrics: ExecutionPlanMetricsSet::new(),
16131666
layout_readers: Default::default(),
1614-
natural_split_ranges: Default::default(),
1667+
natural_splits: Default::default(),
16151668
has_output_ordering: false,
16161669
expression_convertor: Arc::new(DefaultExpressionConvertor::default()),
16171670
file_metadata_cache: None,

vortex-datafusion/src/persistent/source.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use std::fmt::Formatter;
5-
use std::ops::Range;
65
use std::sync::Arc;
76
use std::sync::Weak;
87

@@ -36,6 +35,7 @@ use vortex::metrics::MetricsRegistry;
3635
use vortex::session::VortexSession;
3736
use vortex_utils::aliases::dash_map::DashMap;
3837

38+
use super::opener::NaturalSplits;
3939
use super::opener::VortexOpener;
4040
use crate::VortexTableOptions;
4141
use crate::convert::exprs::DefaultExpressionConvertor;
@@ -195,8 +195,8 @@ pub struct VortexSource {
195195
///
196196
/// Sharing the readers allows us to only read every layout once from the file, even across partitions.
197197
layout_readers: Arc<DashMap<Path, Weak<dyn LayoutReader>>>,
198-
/// Shared full-file natural split ranges keyed by path.
199-
natural_split_ranges: Arc<DashMap<Path, Arc<[Range<u64>]>>>,
198+
/// Shared full-file natural splits keyed by path.
199+
natural_splits: Arc<DashMap<Path, Arc<NaturalSplits>>>,
200200
expression_convertor: Arc<dyn ExpressionConvertor>,
201201
pub(crate) vortex_reader_factory: Option<Arc<dyn VortexReaderFactory>>,
202202
pub(crate) ordered: bool,
@@ -229,7 +229,7 @@ impl VortexSource {
229229
vortex_predicate: None,
230230
df_metrics: Default::default(),
231231
layout_readers: Arc::new(DashMap::default()),
232-
natural_split_ranges: Arc::new(DashMap::default()),
232+
natural_splits: Arc::new(DashMap::default()),
233233
expression_convertor,
234234
vortex_reader_factory: None,
235235
vx_metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
@@ -356,7 +356,7 @@ impl VortexSource {
356356
metrics_registry: Arc::clone(&self.vx_metrics_registry),
357357
df_metrics: self.df_metrics.clone(),
358358
layout_readers: Arc::clone(&self.layout_readers),
359-
natural_split_ranges: Arc::clone(&self.natural_split_ranges),
359+
natural_splits: Arc::clone(&self.natural_splits),
360360
has_output_ordering: !base_config.output_ordering.is_empty() || self.ordered,
361361
expression_convertor: Arc::clone(&self.expression_convertor),
362362
file_metadata_cache: self.file_metadata_cache.clone(),

vortex-layout/src/scan/repeated_scan.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ impl<A: 'static + Send> RepeatedScan<A> {
144144
if range.is_empty() {
145145
return Ok(Vec::new());
146146
}
147-
let lo = vec.partition_point(|&x| x < range.start);
147+
let lo = vec.partition_point(|&x| x <= range.start);
148148
let hi = vec.partition_point(|&x| x < range.end);
149149
Either::Right(
150150
iter::once(range.start)

vortex-layout/src/scan/scan_builder.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,11 @@ impl<A: 'static + Send> ScanBuilder<A> {
302302
.row_range
303303
.clone()
304304
.unwrap_or_else(|| 0..layout_reader.row_count());
305-
Splits::Natural(self.split_by.splits(
306-
layout_reader.as_ref(),
307-
&split_range,
308-
&field_mask,
309-
)?)
305+
Splits::Natural(
306+
self.split_by
307+
.splits(layout_reader.as_ref(), &split_range, &field_mask)?
308+
.into(),
309+
)
310310
};
311311

312312
Ok(RepeatedScan::new(

vortex-layout/src/scan/splits.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use std::ops::Range;
5+
use std::sync::Arc;
56

67
use vortex_scan::selection::Selection;
78

@@ -19,7 +20,7 @@ pub enum Splits {
1920
/// column chunks).
2021
///
2122
/// The vec is sorted in ascending order and deduplicated.
22-
Natural(Vec<u64>),
23+
Natural(Arc<[u64]>),
2324

2425
/// Exact split ranges.
2526
///

0 commit comments

Comments
 (0)