@@ -37,7 +37,6 @@ use futures::FutureExt;
3737use futures:: StreamExt ;
3838use futures:: TryStreamExt ;
3939use futures:: stream;
40- use itertools:: Itertools ;
4140use object_store:: path:: Path ;
4241use tracing:: Instrument ;
4342use 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.
522564fn 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
553587fn 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 ,
0 commit comments