Skip to content

Commit 0e4d1ad

Browse files
committed
chore: remove PeakRecordingPool and size spill pools from the peak_mem_used metric
1 parent 2865851 commit 0e4d1ad

3 files changed

Lines changed: 73 additions & 110 deletions

File tree

datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs

Lines changed: 23 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use std::fmt::{self, Display};
19-
use std::sync::atomic::{AtomicUsize, Ordering};
2018
use std::{cmp, sync::Arc};
2119

2220
use datafusion::{
@@ -26,9 +24,7 @@ use datafusion::{
2624
use datafusion_catalog::TableProvider;
2725
use datafusion_common::ScalarValue;
2826
use datafusion_common::{error::Result, utils::get_available_parallelism};
29-
use datafusion_execution::memory_pool::{
30-
FairSpillPool, MemoryConsumer, MemoryPool, MemoryReservation, UnboundedMemoryPool,
31-
};
27+
use datafusion_execution::memory_pool::{FairSpillPool, MemoryPool};
3228
use datafusion_execution::runtime_env::RuntimeEnvBuilder;
3329
use datafusion_expr::col;
3430
use rand::{Rng, rng};
@@ -45,68 +41,6 @@ const SPILL_POOL_PEAK_FRACTIONS: [(usize, usize); 4] = [(2, 1), (1, 2), (2, 5),
4541
/// small enough to fit under the pool.
4642
const SPILL_BATCH_SIZE_CAP: usize = 256;
4743

48-
/// Unbounded pool that records the peak total reservation.
49-
///
50-
/// The baseline runs under it to measure the aggregate's real footprint, which
51-
/// sizes the spill pools (see `generate`). Unlike `TrackConsumersPool`, the peak
52-
/// survives consumer deregistration, so it is readable after the query.
53-
#[derive(Debug, Default)]
54-
pub(crate) struct PeakRecordingPool {
55-
inner: UnboundedMemoryPool,
56-
peak: AtomicUsize,
57-
}
58-
59-
impl PeakRecordingPool {
60-
/// Peak total reservation seen so far, in bytes.
61-
pub(crate) fn peak(&self) -> usize {
62-
self.peak.load(Ordering::Relaxed)
63-
}
64-
65-
fn record_peak(&self) {
66-
self.peak
67-
.fetch_max(self.inner.reserved(), Ordering::Relaxed);
68-
}
69-
}
70-
71-
impl Display for PeakRecordingPool {
72-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73-
write!(f, "PeakRecordingPool")
74-
}
75-
}
76-
77-
impl MemoryPool for PeakRecordingPool {
78-
fn name(&self) -> &str {
79-
"PeakRecordingPool"
80-
}
81-
82-
fn register(&self, consumer: &MemoryConsumer) {
83-
self.inner.register(consumer);
84-
}
85-
86-
fn unregister(&self, consumer: &MemoryConsumer) {
87-
self.inner.unregister(consumer);
88-
}
89-
90-
fn grow(&self, reservation: &MemoryReservation, additional: usize) {
91-
self.inner.grow(reservation, additional);
92-
self.record_peak();
93-
}
94-
95-
fn shrink(&self, reservation: &MemoryReservation, shrink: usize) {
96-
self.inner.shrink(reservation, shrink);
97-
}
98-
99-
fn try_grow(&self, reservation: &MemoryReservation, additional: usize) -> Result<()> {
100-
self.inner.try_grow(reservation, additional)?;
101-
self.record_peak();
102-
Ok(())
103-
}
104-
105-
fn reserved(&self) -> usize {
106-
self.inner.reserved()
107-
}
108-
}
109-
11044
/// SessionContext generator
11145
///
11246
/// During testing, `generate_baseline` will be called firstly to generate a standard [`SessionContext`],
@@ -165,11 +99,10 @@ impl SessionContextGenerator {
16599
impl SessionContextGenerator {
166100
/// Generate the `SessionContext` for the baseline run.
167101
///
168-
/// Runs under a `PeakRecordingPool` so the aggregate's peak memory can be
169-
/// read after the query (see `generate`, which sizes the spill pools from it).
170-
pub fn generate_baseline(
171-
&self,
172-
) -> Result<(SessionContextWithParams, Arc<PeakRecordingPool>)> {
102+
/// Runs under the default unbounded pool, so it never spills (it is the
103+
/// oracle). The caller reads the aggregate's peak from the plan metrics of
104+
/// this run (see `run_sql_capturing_peak`) to size the spilling pools.
105+
pub fn generate_baseline(&self) -> Result<SessionContextWithParams> {
173106
let schema = self.dataset.batches[0].schema();
174107
let batches = self.dataset.batches.clone();
175108
let provider = MemTable::try_new(schema, vec![batches])?;
@@ -181,23 +114,19 @@ impl SessionContextGenerator {
181114
let skip_partial_params = SkipPartialParams::ensure_not_trigger();
182115
let enable_migration_aggregate = false;
183116

184-
// Records peak usage; unbounded, so the baseline (the oracle) never spills.
185-
let tracker = Arc::new(PeakRecordingPool::default());
186-
187117
let builder = GeneratedSessionContextBuilder {
188118
batch_size,
189119
target_partitions,
190120
skip_partial_params,
191121
enable_migration_aggregate,
192122
sort_hint: false,
193-
memory_pool: Some(Arc::clone(&tracker) as Arc<dyn MemoryPool>),
194-
// Baseline: never spill.
123+
memory_pool: None,
195124
memory_limit: None,
196125
table_name: self.table_name.clone(),
197126
table_provider: Arc::new(provider),
198127
};
199128

200-
Ok((builder.build()?, tracker))
129+
builder.build()
201130
}
202131

203132
/// Randomly generate a session context.
@@ -229,6 +158,9 @@ impl SessionContextGenerator {
229158
rng.random_range(1..=self.max_batch_size)
230159
};
231160

161+
// Single partition when spilling. `FairSpillPool` splits across the per-partition aggregate consumers,
162+
// so with many partitions each share is too small and hits `ResourcesExhausted` before it can spill. Multi
163+
// partition stays covered by the unbounded rounds.
232164
// Single partition when spilling. `FairSpillPool` splits across the per-partition aggregate consumers,
233165
// so with many partitions each share is too small and hits `ResourcesExhausted` before it can spill. Multi
234166
// partition stays covered by the unbounded rounds.
@@ -417,7 +349,9 @@ mod test {
417349
use arrow::util::pretty::pretty_format_batches;
418350
use datafusion_common::DataFusionError;
419351

420-
use crate::fuzz_cases::aggregation_fuzzer::check_equality_of_batches;
352+
use crate::fuzz_cases::aggregation_fuzzer::{
353+
check_equality_of_batches, run_sql_capturing_peak,
354+
};
421355

422356
use super::*;
423357

@@ -473,20 +407,14 @@ mod test {
473407
let ctx_generator = SessionContextGenerator::new(Arc::new(dataset), "fuzz_table");
474408

475409
let query = "select b, count(a) from fuzz_table group by b";
476-
let (baseline_wrapped_ctx, tracker) = ctx_generator.generate_baseline().unwrap();
410+
let baseline_wrapped_ctx = ctx_generator.generate_baseline().unwrap();
477411

478-
// Run the baseline first to record the peak, then size the spilling
412+
// Run the baseline first to capture the peak, then size the spilling
479413
// contexts from it.
480-
let base_result = baseline_wrapped_ctx
481-
.ctx
482-
.sql(query)
483-
.await
484-
.unwrap()
485-
.collect()
486-
.await
487-
.unwrap();
488-
489-
let agg_peak = tracker.peak();
414+
let (base_result, agg_peak) =
415+
run_sql_capturing_peak(query, &baseline_wrapped_ctx.ctx)
416+
.await
417+
.unwrap();
490418

491419
let mut random_wrapped_ctxs = Vec::with_capacity(8);
492420
for _ in 0..8 {
@@ -536,17 +464,10 @@ mod test {
536464
let ctx_generator = SessionContextGenerator::new(Arc::new(dataset), "fuzz_table");
537465
let query = "select k, count(*) from fuzz_table group by k";
538466

539-
// Run the baseline to record the peak that sizes the spill pools.
540-
let (baseline, tracker) = ctx_generator.generate_baseline().unwrap();
541-
baseline
542-
.ctx
543-
.sql(query)
544-
.await
545-
.unwrap()
546-
.collect()
547-
.await
548-
.unwrap();
549-
let agg_peak = tracker.peak();
467+
// Run the baseline to capture the peak that sizes the spill pools.
468+
let baseline = ctx_generator.generate_baseline().unwrap();
469+
let (_baseline_result, agg_peak) =
470+
run_sql_capturing_peak(query, &baseline.ctx).await.unwrap();
550471
assert!(agg_peak > 0, "baseline should have reserved memory");
551472

552473
// Generate contexts until a bounded one actually spills. Spilling is a

datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::fuzz_cases::aggregation_fuzzer::{
2828
check_equality_of_batches,
2929
context_generator::{SessionContextGenerator, SessionContextWithParams},
3030
data_generator::{Dataset, DatasetGenerator, DatasetGeneratorConfig},
31-
run_sql,
31+
run_sql, run_sql_capturing_peak,
3232
};
3333

3434
/// Rounds to call `generate` of [`SessionContextGenerator`]
@@ -213,16 +213,18 @@ impl AggregationFuzzer {
213213
let ctx_generator =
214214
SessionContextGenerator::new(dataset_ref.clone(), &self.table_name);
215215

216-
// Generate the baseline context, and get the baseline result firstly
217-
let (baseline_ctx_with_params, tracker) = ctx_generator
216+
// Generate the baseline context, and get the baseline result firstly.
217+
// The baseline run also gives the aggregate's peak memory, which sizes
218+
// the spill pools for the randomized contexts.
219+
let baseline_ctx_with_params = ctx_generator
218220
.generate_baseline()
219221
.expect("should succeed to generate baseline session context");
220-
let baseline_result = run_sql(&sql, &baseline_ctx_with_params.ctx)
221-
.await
222-
.expect("should succeed to run baseline sql");
222+
223+
let (baseline_result, agg_peak) =
224+
run_sql_capturing_peak(&sql, &baseline_ctx_with_params.ctx)
225+
.await
226+
.expect("should succeed to run baseline sql");
223227
let baseline_result = Arc::new(baseline_result);
224-
// Baseline peak (dominated by the aggregate) sizes the spill pools.
225-
let agg_peak = tracker.peak();
226228
// Generate test tasks
227229
for _ in 0..CTX_GEN_ROUNDS {
228230
let ctx_with_params = ctx_generator

datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@
3535
//! different configuration parameters to control the execution path of aggregate
3636
//! queries.
3737
38+
use std::sync::Arc;
39+
3840
use arrow::array::RecordBatch;
3941
use arrow::util::pretty::pretty_format_batches;
4042
use datafusion::prelude::SessionContext;
4143
use datafusion_common::error::Result;
44+
use datafusion_physical_plan::{ExecutionPlan, collect};
4245

4346
mod context_generator;
4447
mod data_generator;
@@ -89,3 +92,40 @@ pub(crate) fn check_equality_of_batches(
8992
pub(crate) async fn run_sql(sql: &str, ctx: &SessionContext) -> Result<Vec<RecordBatch>> {
9093
ctx.sql(sql).await?.collect().await
9194
}
95+
96+
/// Run `sql` and return the result along with the aggregate's peak memory
97+
/// reservation, read from the `peak_mem_used` metric the aggregate publishes.
98+
/// The peak sizes the spilling pools for the randomized contexts.
99+
///
100+
/// We read the metric rather than the memory pool because the pool cannot report
101+
/// a peak: it only tracks current usage, which drops back to zero as reservations
102+
/// are freed, so it reads as ~0 once the query finishes.
103+
pub(crate) async fn run_sql_capturing_peak(
104+
sql: &str,
105+
ctx: &SessionContext,
106+
) -> Result<(Vec<RecordBatch>, usize)> {
107+
let plan = ctx.sql(sql).await?.create_physical_plan().await?;
108+
let result = collect(Arc::clone(&plan), ctx.task_ctx()).await?;
109+
110+
Ok((result, peak_mem_used(plan.as_ref())))
111+
}
112+
113+
/// Sum the `peak_mem_used` metric over the whole plan tree. Metrics are per
114+
/// node, so this walks children and adds up the aggregate nodes. Returns 0 if
115+
/// no node reports it (e.g. a query with no grouping).
116+
///
117+
/// If this metric name drifts, the peak silently becomes 0 and nothing spills;
118+
/// `test_generated_context_spills` guards against that.
119+
fn peak_mem_used(plan: &dyn ExecutionPlan) -> usize {
120+
let here = plan
121+
.metrics()
122+
.and_then(|m| m.sum_by_name("peak_mem_used"))
123+
.map(|v| v.as_usize())
124+
.unwrap_or(0);
125+
126+
here + plan
127+
.children()
128+
.iter()
129+
.map(|child| peak_mem_used(child.as_ref()))
130+
.sum::<usize>()
131+
}

0 commit comments

Comments
 (0)