Skip to content

Commit 02429ca

Browse files
adriangbclaude
andcommitted
account Parquet read-ahead in the MemoryPool
Each read-ahead stream registers a `ParquetReadAhead[partition]` consumer. The reservation follows the bytes the decoder holds plus the bytes in flight. Bytes the decoder asks for are always reserved (grow). Speculative read-ahead takes only what the pool can grant (try_grow); ranges that do not fit stay pending. `FileSource::create_morselizer_with_context` gives the source the scan's `TaskContext`. The default delegates to `create_morselizer`. `ParquetSource` uses it to get the memory pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 667c4dc commit 02429ca

5 files changed

Lines changed: 482 additions & 29 deletions

File tree

‎datafusion/datasource-parquet/src/opener/mod.rs‎

Lines changed: 255 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::metrics::{ByteProgress, RowFilterSkippedFullyMatchedMetric};
2929
use crate::page_filter::PagePruningAccessPlanFilter;
3030
use crate::push_decoder::{
3131
DecoderBuilderConfig, InitialDecoderState, PushDecoderStreamState, ReadAhead,
32-
RgPlanEntry, RowFilterContext, RowGroupPruner,
32+
ReadAheadMemory, RgPlanEntry, RowFilterContext, RowGroupPruner,
3333
};
3434
use crate::row_group_filter::{RowGroupAccessPlanFilter, row_group_in_range};
3535
use crate::{
@@ -58,6 +58,7 @@ use datafusion_common::{
5858
ColumnStatistics, HashSet, Result, ScalarValue, Statistics, exec_err, internal_err,
5959
};
6060
use datafusion_datasource::{PartitionedFile, TableSchema};
61+
use datafusion_execution::memory_pool::MemoryPool;
6162
use datafusion_physical_expr::expressions::{Column, DynamicFilterTracking, Literal};
6263
use datafusion_physical_expr::simplifier::PhysicalExprSimplifier;
6364
use 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

314317
impl 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

Comments
 (0)