@@ -29,7 +29,7 @@ use crate::metrics::{ByteProgress, RowFilterSkippedFullyMatchedMetric};
2929use crate :: page_filter:: PagePruningAccessPlanFilter ;
3030use crate :: push_decoder:: {
3131 DecoderBuilderConfig , InitialDecoderState , PushDecoderStreamState , ReadAhead ,
32- RgPlanEntry , RowFilterContext , RowGroupPruner ,
32+ ReadAheadMemory , RgPlanEntry , RowFilterContext , RowGroupPruner ,
3333} ;
3434use crate :: row_group_filter:: { RowGroupAccessPlanFilter , row_group_in_range} ;
3535use crate :: {
@@ -58,6 +58,7 @@ use datafusion_common::{
5858 ColumnStatistics , HashSet , Result , ScalarValue , Statistics , exec_err, internal_err,
5959} ;
6060use datafusion_datasource:: { PartitionedFile , TableSchema } ;
61+ use datafusion_execution:: memory_pool:: MemoryPool ;
6162use datafusion_physical_expr:: expressions:: { Column , DynamicFilterTracking , Literal } ;
6263use datafusion_physical_expr:: simplifier:: PhysicalExprSimplifier ;
6364use datafusion_physical_expr:: utils:: collect_columns;
@@ -309,6 +310,8 @@ pub(super) struct ParquetMorselizer {
309310 /// Per-scan virtual-column state (validation already performed). `None`
310311 /// when no virtual columns are requested — the common path.
311312 pub ( crate ) virtual_state : Option < Arc < VirtualColumnsState > > ,
313+ /// Pool that accounts read-ahead buffers. `None` uses an unbounded pool.
314+ pub ( crate ) memory_pool : Option < Arc < dyn MemoryPool > > ,
312315}
313316
314317impl fmt:: Debug for ParquetMorselizer {
@@ -463,6 +466,7 @@ struct PreparedParquetOpen {
463466 /// the logical-with-virtual schema. `None` when no virtual columns were
464467 /// requested.
465468 virtual_state : Option < Arc < VirtualColumnsState > > ,
469+ memory_pool : Option < Arc < dyn MemoryPool > > ,
466470 reorder_predicates : bool ,
467471 pushdown_filters : bool ,
468472 force_filter_selections : bool ,
@@ -997,6 +1001,7 @@ impl ParquetMorselizer {
9971001 projection,
9981002 predicate,
9991003 virtual_state : self . virtual_state . as_ref ( ) . map ( Arc :: clone) ,
1004+ memory_pool : self . memory_pool . clone ( ) ,
10001005 reorder_predicates : self . reorder_filters ,
10011006 pushdown_filters : self . pushdown_filters ,
10021007 force_filter_selections : self . force_filter_selections ,
@@ -1898,8 +1903,11 @@ impl RowGroupsPrunedParquetOpen {
18981903 . file_metrics
18991904 . row_groups_pruned_dynamic_filter
19001905 . clone ( ) ;
1901- let read_ahead =
1902- read_ahead_bytes. map ( |window| ReadAhead :: new ( window, decoder. scan_plan ( ) ) ) ;
1906+ let read_ahead = read_ahead_bytes. map ( |window| {
1907+ let memory =
1908+ ReadAheadMemory :: new ( prepared. memory_pool , prepared. partition_index ) ;
1909+ ReadAhead :: new ( window, decoder. scan_plan ( ) , memory)
1910+ } ) ;
19031911 let stream = PushDecoderStreamState {
19041912 decoder : Some ( decoder) ,
19051913 active_reader : None ,
@@ -2219,6 +2227,7 @@ mod test {
22192227 max_predicate_cache_size : Option < usize > ,
22202228 max_in_list_size : usize ,
22212229 read_ahead_bytes : Option < u64 > ,
2230+ memory_pool : Option < Arc < dyn MemoryPool > > ,
22222231 reverse_row_groups : bool ,
22232232 preserve_order : bool ,
22242233 }
@@ -2443,6 +2452,7 @@ mod test {
24432452 max_predicate_cache_size : None ,
24442453 max_in_list_size : MAX_IN_LIST_SIZE ,
24452454 read_ahead_bytes : None ,
2455+ memory_pool : None ,
24462456 reverse_row_groups : false ,
24472457 preserve_order : false ,
24482458 }
@@ -2531,6 +2541,18 @@ mod test {
25312541 self
25322542 }
25332543
2544+ /// Set the read-ahead window (`read_ahead_bytes`).
2545+ fn with_read_ahead_bytes ( mut self , bytes : u64 ) -> Self {
2546+ self . read_ahead_bytes = Some ( bytes) ;
2547+ self
2548+ }
2549+
2550+ /// Set the pool that accounts read-ahead buffers.
2551+ fn with_memory_pool ( mut self , pool : Arc < dyn MemoryPool > ) -> Self {
2552+ self . memory_pool = Some ( pool) ;
2553+ self
2554+ }
2555+
25342556 /// Set a row limit.
25352557 fn with_limit ( mut self , limit : usize ) -> Self {
25362558 self . limit = Some ( limit) ;
@@ -2632,6 +2654,7 @@ mod test {
26322654 reverse_row_groups : self . reverse_row_groups ,
26332655 sort_order_for_reorder : None ,
26342656 virtual_state,
2657+ memory_pool : self . memory_pool ,
26352658 } )
26362659 }
26372660 }
@@ -5743,4 +5766,233 @@ mod test {
57435766 assert_eq ! ( rows, 5 ) ;
57445767 }
57455768 }
5769+
5770+ /// Read-ahead (`read_ahead_bytes`) and its accounting in the memory pool.
5771+ mod read_ahead {
5772+ use super :: * ;
5773+ use std:: ops:: Range ;
5774+ use std:: sync:: Mutex ;
5775+
5776+ use bytes:: Bytes ;
5777+
5778+ use arrow:: array:: Int64Array ;
5779+ use arrow:: compute:: concat_batches;
5780+ use datafusion_execution:: memory_pool:: {
5781+ GreedyMemoryPool , MemoryPool , PeakRecordingPool ,
5782+ } ;
5783+ use futures:: future:: BoxFuture ;
5784+ use parquet:: arrow:: arrow_reader:: ArrowReaderOptions ;
5785+ use parquet:: arrow:: async_reader:: AsyncFileReader ;
5786+
5787+ const WINDOW : u64 = 1024 * 1024 ;
5788+ const ROWS : i64 = 400_000 ;
5789+
5790+ /// Records the bytes of every data fetch of the readers it creates.
5791+ #[ derive( Debug ) ]
5792+ struct RecordingReaderFactory {
5793+ inner : DefaultParquetFileReaderFactory ,
5794+ fetches : Arc < Mutex < Vec < u64 > > > ,
5795+ }
5796+
5797+ struct RecordingReader {
5798+ inner : Box < dyn AsyncFileReader + Send > ,
5799+ fetches : Arc < Mutex < Vec < u64 > > > ,
5800+ }
5801+
5802+ impl ParquetFileReaderFactory for RecordingReaderFactory {
5803+ fn create_reader (
5804+ & self ,
5805+ partition_index : usize ,
5806+ partitioned_file : PartitionedFile ,
5807+ metadata_size_hint : Option < usize > ,
5808+ metrics : & ExecutionPlanMetricsSet ,
5809+ ) -> Result < Box < dyn AsyncFileReader + Send > > {
5810+ Ok ( Box :: new ( RecordingReader {
5811+ inner : self . inner . create_reader (
5812+ partition_index,
5813+ partitioned_file,
5814+ metadata_size_hint,
5815+ metrics,
5816+ ) ?,
5817+ fetches : Arc :: clone ( & self . fetches ) ,
5818+ } ) )
5819+ }
5820+ }
5821+
5822+ impl AsyncFileReader for RecordingReader {
5823+ fn get_bytes (
5824+ & mut self ,
5825+ range : Range < u64 > ,
5826+ ) -> BoxFuture < ' _ , parquet:: errors:: Result < Bytes > > {
5827+ self . fetches . lock ( ) . unwrap ( ) . push ( range. end - range. start ) ;
5828+ self . inner . get_bytes ( range)
5829+ }
5830+
5831+ fn get_byte_ranges (
5832+ & mut self ,
5833+ ranges : Vec < Range < u64 > > ,
5834+ ) -> BoxFuture < ' _ , parquet:: errors:: Result < Vec < Bytes > > > {
5835+ let bytes = ranges. iter ( ) . map ( |r| r. end - r. start ) . sum ( ) ;
5836+ self . fetches . lock ( ) . unwrap ( ) . push ( bytes) ;
5837+ self . inner . get_byte_ranges ( ranges)
5838+ }
5839+
5840+ fn get_metadata < ' a > (
5841+ & ' a mut self ,
5842+ options : Option < & ' a ArrowReaderOptions > ,
5843+ ) -> BoxFuture < ' a , parquet:: errors:: Result < Arc < ParquetMetaData > > >
5844+ {
5845+ self . inner . get_metadata ( options)
5846+ }
5847+ }
5848+
5849+ /// A file of several MB: `a` is `0..ROWS`, `b` does not compress.
5850+ /// Ten row groups of several pages each.
5851+ async fn write_file ( store : & Arc < dyn ObjectStore > ) -> ( SchemaRef , usize ) {
5852+ let schema = Arc :: new ( Schema :: new ( vec ! [
5853+ Field :: new( "a" , DataType :: Int64 , false ) ,
5854+ Field :: new( "b" , DataType :: Int64 , false ) ,
5855+ ] ) ) ;
5856+ let a: Vec < i64 > = ( 0 ..ROWS ) . collect ( ) ;
5857+ let b: Vec < i64 > = a
5858+ . iter ( )
5859+ . map ( |v| v. wrapping_mul ( 0x9E37_79B9_7F4A_7C15_u64 as i64 ) )
5860+ . collect ( ) ;
5861+ let batch = RecordBatch :: try_new (
5862+ Arc :: clone ( & schema) ,
5863+ vec ! [ Arc :: new( Int64Array :: from( a) ) , Arc :: new( Int64Array :: from( b) ) ] ,
5864+ )
5865+ . unwrap ( ) ;
5866+ let props = WriterProperties :: builder ( )
5867+ . set_dictionary_enabled ( false )
5868+ . set_max_row_group_row_count ( Some ( ROWS as usize / 10 ) )
5869+ . set_data_page_row_count_limit ( 4096 )
5870+ . set_write_batch_size ( 4096 )
5871+ . build ( ) ;
5872+ let len = write_parquet_batches (
5873+ Arc :: clone ( store) ,
5874+ "read_ahead.parquet" ,
5875+ vec ! [ batch] ,
5876+ Some ( props) ,
5877+ )
5878+ . await ;
5879+ ( schema, len)
5880+ }
5881+
5882+ struct Scan {
5883+ batches : Vec < RecordBatch > ,
5884+ /// Bytes of each data fetch.
5885+ fetches : Vec < u64 > ,
5886+ /// `pool.reserved()` after each batch.
5887+ reserved : Vec < usize > ,
5888+ }
5889+
5890+ /// Scan the file, with read-ahead if `pool` is set.
5891+ async fn scan (
5892+ pool : Option < Arc < dyn MemoryPool > > ,
5893+ predicate : Option < Arc < dyn PhysicalExpr > > ,
5894+ ) -> Scan {
5895+ let store = Arc :: new ( InMemory :: new ( ) ) as Arc < dyn ObjectStore > ;
5896+ let ( schema, len) = write_file ( & store) . await ;
5897+ let fetches = Arc :: new ( Mutex :: new ( vec ! [ ] ) ) ;
5898+ let factory = RecordingReaderFactory {
5899+ inner : DefaultParquetFileReaderFactory :: new ( Arc :: clone ( & store) ) ,
5900+ fetches : Arc :: clone ( & fetches) ,
5901+ } ;
5902+ let mut builder = ParquetMorselizerBuilder :: new ( )
5903+ . with_store ( Arc :: clone ( & store) )
5904+ . with_schema ( Arc :: clone ( & schema) )
5905+ . with_projection_indices ( & [ 0 , 1 ] )
5906+ . with_parquet_file_reader_factory ( Arc :: new ( factory) ) ;
5907+ if let Some ( predicate) = predicate {
5908+ builder = builder
5909+ . with_predicate ( predicate)
5910+ . with_pushdown_filters ( true ) ;
5911+ }
5912+ if let Some ( pool) = & pool {
5913+ builder = builder
5914+ . with_read_ahead_bytes ( WINDOW )
5915+ . with_memory_pool ( Arc :: clone ( pool) ) ;
5916+ }
5917+ let morselizer = builder. build ( ) ;
5918+ let file = PartitionedFile :: new ( "read_ahead.parquet" . to_string ( ) , len as u64 ) ;
5919+ let mut stream = open_file ( & morselizer, file) . await . unwrap ( ) ;
5920+ let mut batches = vec ! [ ] ;
5921+ let mut reserved = vec ! [ ] ;
5922+ while let Some ( batch) = stream. next ( ) . await {
5923+ batches. push ( batch. unwrap ( ) ) ;
5924+ reserved. extend ( pool. as_ref ( ) . map ( |p| p. reserved ( ) ) ) ;
5925+ }
5926+ drop ( stream) ;
5927+ drop ( morselizer) ;
5928+ if let Some ( pool) = & pool {
5929+ assert_eq ! ( pool. reserved( ) , 0 , "the stream returns all it reserved" ) ;
5930+ }
5931+ let fetches = fetches. lock ( ) . unwrap ( ) . clone ( ) ;
5932+ Scan {
5933+ batches,
5934+ fetches,
5935+ reserved,
5936+ }
5937+ }
5938+
5939+ fn concat ( batches : & [ RecordBatch ] ) -> RecordBatch {
5940+ concat_batches ( & batches[ 0 ] . schema ( ) , batches) . unwrap ( )
5941+ }
5942+
5943+ fn a_lt ( value : i64 ) -> Arc < dyn PhysicalExpr > {
5944+ let schema = Schema :: new ( vec ! [ Field :: new( "a" , DataType :: Int64 , false ) ] ) ;
5945+ logical2physical ( & col ( "a" ) . lt ( lit ( value) ) , & schema)
5946+ }
5947+
5948+ /// A pool far smaller than the file cannot stop the scan: the bytes
5949+ /// the decoder needs are always granted.
5950+ #[ tokio:: test]
5951+ async fn small_pool_scan_is_correct_and_releases_all ( ) {
5952+ let expected = scan ( None , None ) . await ;
5953+ let pool: Arc < dyn MemoryPool > = Arc :: new ( GreedyMemoryPool :: new ( 64 * 1024 ) ) ;
5954+ let actual = scan ( Some ( pool) , None ) . await ;
5955+ assert_eq ! ( concat( & actual. batches) , concat( & expected. batches) ) ;
5956+ assert_eq ! ( concat( & actual. batches) . num_rows( ) , ROWS as usize ) ;
5957+ }
5958+
5959+ /// With room in the pool, the reservation follows what read-ahead
5960+ /// holds and stays within the window plus one required fetch.
5961+ #[ tokio:: test]
5962+ async fn reservation_is_bounded_by_window ( ) {
5963+ let recorder = Arc :: new ( PeakRecordingPool :: new ( Arc :: new (
5964+ GreedyMemoryPool :: new ( 1 << 30 ) ,
5965+ ) ) ) ;
5966+ let pool: Arc < dyn MemoryPool > = Arc :: clone ( & recorder) as _ ;
5967+ let actual = scan ( Some ( pool) , None ) . await ;
5968+ // A required fetch is at most the largest fetch.
5969+ let largest_fetch = * actual. fetches . iter ( ) . max ( ) . unwrap ( ) ;
5970+ let peak = recorder. max_reserved ( ) as u64 ;
5971+ assert ! ( actual. reserved. iter( ) . any( |& r| r > 0 ) ) ;
5972+ assert ! (
5973+ peak <= WINDOW + largest_fetch,
5974+ "peak {peak}, largest fetch {largest_fetch}"
5975+ ) ;
5976+ }
5977+
5978+ /// A filter that rejects every row: without read-ahead the scan never
5979+ /// reads `b`. Read-ahead fetches `b` ahead (a conditional range),
5980+ /// unless the pool has no room for it.
5981+ #[ tokio:: test]
5982+ async fn conditional_ranges_are_read_ahead_within_the_pool ( ) {
5983+ let bytes = |scan : & Scan | scan. fetches . iter ( ) . sum :: < u64 > ( ) ;
5984+ let baseline = scan ( None , Some ( a_lt ( -1 ) ) ) . await ;
5985+ assert_eq ! (
5986+ baseline. batches. iter( ) . map( |b| b. num_rows( ) ) . sum:: <usize >( ) ,
5987+ 0
5988+ ) ;
5989+
5990+ let unbounded: Arc < dyn MemoryPool > = Arc :: new ( GreedyMemoryPool :: new ( 1 << 30 ) ) ;
5991+ let ahead = scan ( Some ( unbounded) , Some ( a_lt ( -1 ) ) ) . await ;
5992+ let full: Arc < dyn MemoryPool > = Arc :: new ( GreedyMemoryPool :: new ( 1 ) ) ;
5993+ let required_only = scan ( Some ( full) , Some ( a_lt ( -1 ) ) ) . await ;
5994+ assert ! ( bytes( & ahead) > bytes( & baseline) ) ;
5995+ assert_eq ! ( bytes( & required_only) , bytes( & baseline) ) ;
5996+ }
5997+ }
57465998}
0 commit comments