datafusion_execution/memory_pool/peak_recording.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Records the peak [`MemoryPool`] reservation reached during a benchmark.
19//!
20//! DataFusion's [`MemoryPool`] deliberately accounts for only the "large"
21//! allocations that scale with input size; intermediate batches flowing between
22//! operators are assumed to be small and are left untracked. The [`MemoryPool`]
23//! documentation therefore advises reserving "some overhead (e.g. 10%)" on top
24//! of the configured limit.
25//!
26//! Nothing reports what that overhead actually is, because the peak reservation
27//! itself is never recorded — [`MemoryPool::reserved`] is a live value that has
28//! usually fallen back to zero by the time a query finishes. This module records
29//! the high-water mark so benchmarks can emit it alongside the peak RSS that
30//! `print_memory_stats` already prints, making the gap between the two
31//! measurable.
32//!
33//! This is measurement only: nothing here enforces a relationship between the
34//! two numbers.
35//!
36//! What lands in the peak is whatever the pool accounts for, so this follows
37//! the accounting rather than fixing it in place. Arrow-side reservations made
38//! through `ArrowMemoryPool` are included, because that adapter grows a
39//! DataFusion reservation against the pool it wraps; nothing claims buffers
40//! today, but the peak picks it up when something does.
41
42use std::{
43 fmt::{Debug, Display, Formatter},
44 sync::{
45 Arc,
46 atomic::{AtomicUsize, Ordering},
47 },
48};
49
50use super::{MemoryConsumer, MemoryLimit, MemoryPool, MemoryReservation};
51use datafusion_common::Result;
52
53/// Wraps a [`MemoryPool`], recording the high-water mark of
54/// [`MemoryPool::reserved`] as reservations come and go.
55///
56/// Every method delegates to the wrapped pool, so wrapping does not change how
57/// memory is granted, limited, or reported. The one thing it does change is
58/// downcasting: `rt.memory_pool.downcast_ref::<FairSpillPool>()` now finds this
59/// wrapper instead of the pool it wraps. Nothing in the benchmarks relies on
60/// that, and [`Self::from_pool`] uses the same mechanism to find the recorder.
61///
62/// Both high-water marks are held per instance, so a benchmark that builds a
63/// fresh runtime per query gets a reading scoped to that query without any
64/// coordination.
65///
66/// # Example
67///
68/// ```
69/// # use std::sync::Arc;
70/// # use datafusion_execution::memory_pool::{GreedyMemoryPool, MemoryConsumer, MemoryPool, PeakRecordingPool};
71/// let recording = Arc::new(PeakRecordingPool::new(Arc::new(GreedyMemoryPool::new(1024))));
72/// let pool: Arc<dyn MemoryPool> = Arc::clone(&recording) as _;
73///
74/// let reservation = MemoryConsumer::new("example").register(&pool);
75/// reservation.try_grow(512)?;
76/// reservation.shrink(512);
77///
78/// // The pool is back to empty, but the high-water mark is retained.
79/// assert_eq!(pool.reserved(), 0);
80/// assert_eq!(recording.peak_reserved(), 512);
81///
82/// // The recorder can also be recovered from the pool it was installed as.
83/// assert_eq!(PeakRecordingPool::from_pool(&*pool).unwrap().peak_reserved(), 512);
84/// # Ok::<(), datafusion_common::DataFusionError>(())
85/// ```
86pub struct PeakRecordingPool {
87 inner: Arc<dyn MemoryPool>,
88 /// Running total of everything granted through this wrapper, kept so the
89 /// peak can be maintained without asking `inner` for its total.
90 reserved: AtomicUsize,
91 /// High-water mark since the last [`PeakRecordingPool::reset_peak`].
92 peak: AtomicUsize,
93 /// High-water mark since this pool was created. Never reset.
94 max: AtomicUsize,
95}
96
97impl PeakRecordingPool {
98 /// Wrap `inner`, recording its peak reservation from here on.
99 ///
100 /// `inner` is expected to be empty: the running total starts at zero, so
101 /// anything reserved before wrapping is not counted.
102 pub fn new(inner: Arc<dyn MemoryPool>) -> Self {
103 Self {
104 inner,
105 reserved: AtomicUsize::new(0),
106 peak: AtomicUsize::new(0),
107 max: AtomicUsize::new(0),
108 }
109 }
110
111 /// The recorder installed as `pool`, if there is one.
112 ///
113 /// Returns `None` whenever a benchmark runs without a memory limit, since
114 /// `CommonOpt::runtime_env_builder` only installs the wrapper alongside a
115 /// pool it has a limit for.
116 pub fn from_pool(pool: &dyn MemoryPool) -> Option<&Self> {
117 pool.downcast_ref::<Self>()
118 }
119
120 /// Peak reservation, in bytes, since the last [`Self::reset_peak`].
121 pub fn peak_reserved(&self) -> usize {
122 self.peak.load(Ordering::Relaxed)
123 }
124
125 /// Peak reservation, in bytes, since this pool was created.
126 ///
127 /// Unlike [`Self::peak_reserved`] this is never reset, so it reports the
128 /// peak across every query that shared this pool.
129 pub fn max_reserved(&self) -> usize {
130 self.max.load(Ordering::Relaxed)
131 }
132
133 /// Reset the value returned by [`Self::peak_reserved`] to what is reserved
134 /// right now, so the next reading covers only what follows.
135 ///
136 /// `BenchmarkRun::start_new_case` calls this, giving each benchmark query
137 /// its own reading. Anything still held when a query starts — data the
138 /// benchmark loaded up front, say — stays in the reading, since the query
139 /// runs with those bytes reserved.
140 pub fn reset_peak(&self) {
141 self.peak
142 .store(self.reserved.load(Ordering::Relaxed), Ordering::Relaxed);
143 }
144
145 /// Add `additional` granted bytes to the running total and publish it to
146 /// both high-water marks.
147 ///
148 /// Accumulating deltas rather than reading [`MemoryPool::reserved`] keeps
149 /// the wrapped pool's own bookkeeping off this path: `FairSpillPool` takes
150 /// its state lock to answer `reserved()`, which would double the lock
151 /// traffic of every accounted allocation in the benchmark being measured.
152 /// The total stays exact because the trait grants exactly what is asked
153 /// for — `grow` is infallible and `try_grow` either grants `additional` or
154 /// returns an error, leaving the reservation untouched.
155 fn record(&self, additional: usize) {
156 let reserved =
157 self.reserved.fetch_add(additional, Ordering::Relaxed) + additional;
158 self.peak.fetch_max(reserved, Ordering::Relaxed);
159 self.max.fetch_max(reserved, Ordering::Relaxed);
160 }
161}
162
163impl Debug for PeakRecordingPool {
164 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
165 f.debug_struct("PeakRecordingPool")
166 .field("inner", &self.inner)
167 .field("peak", &self.peak_reserved())
168 .field("max", &self.max_reserved())
169 .finish()
170 }
171}
172
173impl Display for PeakRecordingPool {
174 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
175 // Deferring to the wrapped pool keeps `SHOW ALL`-style output and error
176 // messages identical to running without the wrapper.
177 Display::fmt(&self.inner, f)
178 }
179}
180
181impl MemoryPool for PeakRecordingPool {
182 fn name(&self) -> &str {
183 self.inner.name()
184 }
185
186 fn register(&self, consumer: &MemoryConsumer) {
187 self.inner.register(consumer);
188 }
189
190 fn unregister(&self, consumer: &MemoryConsumer) {
191 self.inner.unregister(consumer);
192 }
193
194 fn grow(&self, reservation: &MemoryReservation, additional: usize) {
195 self.inner.grow(reservation, additional);
196 self.record(additional);
197 }
198
199 fn shrink(&self, reservation: &MemoryReservation, shrink: usize) {
200 self.inner.shrink(reservation, shrink);
201 self.reserved.fetch_sub(shrink, Ordering::Relaxed);
202 }
203
204 fn try_grow(&self, reservation: &MemoryReservation, additional: usize) -> Result<()> {
205 self.inner.try_grow(reservation, additional)?;
206 self.record(additional);
207 Ok(())
208 }
209
210 fn reserved(&self) -> usize {
211 self.inner.reserved()
212 }
213
214 fn memory_limit(&self) -> MemoryLimit {
215 self.inner.memory_limit()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use crate::memory_pool::GreedyMemoryPool;
222
223 use super::*;
224
225 /// A recording pool over a `GreedyMemoryPool`, returned both as the
226 /// recorder (to read the marks) and as the pool reservations register with.
227 fn pool(limit: usize) -> (Arc<PeakRecordingPool>, Arc<dyn MemoryPool>) {
228 let recording = Arc::new(PeakRecordingPool::new(Arc::new(
229 GreedyMemoryPool::new(limit),
230 )));
231 let pool = Arc::clone(&recording) as Arc<dyn MemoryPool>;
232 (recording, pool)
233 }
234
235 #[test]
236 fn records_high_water_mark_across_reservations() {
237 let (recording, pool) = pool(1024);
238
239 let a = MemoryConsumer::new("a").register(&pool);
240 let b = MemoryConsumer::new("b").register(&pool);
241
242 a.try_grow(300).unwrap();
243 b.try_grow(400).unwrap();
244 // Peak of the sum, not the largest single reservation.
245 assert_eq!(recording.peak_reserved(), 700);
246
247 a.shrink(300);
248 b.try_grow(100).unwrap();
249
250 // Falling back below the peak leaves it untouched, and the later growth
251 // does not reach it.
252 assert_eq!(pool.reserved(), 500);
253 assert_eq!(recording.peak_reserved(), 700);
254 }
255
256 #[test]
257 fn failed_growth_does_not_move_the_peak() {
258 let (recording, pool) = pool(1024);
259
260 let reservation = MemoryConsumer::new("a").register(&pool);
261 reservation.try_grow(600).unwrap();
262 reservation
263 .try_grow(600)
264 .expect_err("should exceed the 1024 byte pool");
265
266 assert_eq!(recording.peak_reserved(), 600);
267 }
268
269 #[test]
270 fn reset_clears_the_window_but_not_the_run_maximum() {
271 let (recording, pool) = pool(1024);
272
273 let reservation = MemoryConsumer::new("a").register(&pool);
274 reservation.try_grow(800).unwrap();
275 reservation.shrink(800);
276
277 recording.reset_peak();
278 assert_eq!(recording.peak_reserved(), 0);
279 assert_eq!(recording.max_reserved(), 800);
280
281 reservation.try_grow(100).unwrap();
282 assert_eq!(recording.peak_reserved(), 100);
283 assert_eq!(recording.max_reserved(), 800);
284 }
285
286 #[test]
287 fn reset_keeps_what_is_still_reserved() {
288 let (recording, pool) = pool(1024);
289
290 // Something a benchmark loaded up front and holds across queries.
291 let held = MemoryConsumer::new("held").register(&pool);
292 held.try_grow(300).unwrap();
293
294 recording.reset_peak();
295 assert_eq!(recording.peak_reserved(), 300);
296
297 let query = MemoryConsumer::new("query").register(&pool);
298 query.try_grow(200).unwrap();
299 assert_eq!(recording.peak_reserved(), 500);
300 }
301
302 #[test]
303 fn marks_are_per_instance() {
304 let (one, one_pool) = pool(1024);
305 let (two, _two_pool) = pool(1024);
306
307 MemoryConsumer::new("a")
308 .register(&one_pool)
309 .try_grow(512)
310 .unwrap();
311
312 assert_eq!(one.peak_reserved(), 512);
313 assert_eq!(two.peak_reserved(), 0);
314 }
315
316 #[test]
317 fn is_recoverable_from_the_pool_it_is_installed_as() {
318 let (recording, pool) = pool(1024);
319
320 MemoryConsumer::new("a")
321 .register(&pool)
322 .try_grow(512)
323 .unwrap();
324
325 let found = PeakRecordingPool::from_pool(&*pool).expect("recorder installed");
326 assert_eq!(found.peak_reserved(), recording.peak_reserved());
327
328 // A pool with no recorder in front of it reports nothing.
329 let plain: Arc<dyn MemoryPool> = Arc::new(GreedyMemoryPool::new(1024));
330 assert!(PeakRecordingPool::from_pool(&*plain).is_none());
331 }
332
333 #[test]
334 fn delegates_limit_and_name_to_the_wrapped_pool() {
335 let inner: Arc<dyn MemoryPool> = Arc::new(GreedyMemoryPool::new(4096));
336 let wrapped = PeakRecordingPool::new(Arc::clone(&inner));
337
338 assert_eq!(wrapped.name(), inner.name());
339 assert_eq!(wrapped.to_string(), inner.to_string());
340 assert!(matches!(wrapped.memory_limit(), MemoryLimit::Finite(4096)));
341 }
342
343 /// Arrow-side reservations reach the recorder too.
344 ///
345 /// [`ArrowMemoryPool`] implements Arrow's `MemoryPool` by growing a
346 /// DataFusion [`MemoryReservation`] against the pool it wraps, so a buffer
347 /// claimed through it lands in `grow` here. Nothing in DataFusion claims
348 /// buffers yet (see apache/datafusion#22898), but when something does, the
349 /// bytes show up in this peak without further changes — as long as the
350 /// adapter is built from the `RuntimeEnv`'s pool, which is the wrapped one.
351 /// This test pins that.
352 ///
353 /// Only compiled with `--features arrow_buffer_pool`, since that's what
354 /// gates `crate::memory_pool::arrow` and `arrow_buffer::MemoryPool` in the
355 /// first place; not part of this crate's default feature set.
356 #[cfg(feature = "arrow_buffer_pool")]
357 #[test]
358 fn records_reservations_arriving_through_the_arrow_adapter() {
359 use crate::memory_pool::arrow::ArrowMemoryPool;
360 use arrow_buffer::MemoryPool as ArrowMemoryPoolTrait;
361
362 let (recording, pool) = pool(4096);
363
364 let arrow_pool =
365 ArrowMemoryPool::new(Arc::clone(&pool), MemoryConsumer::new("arrow"));
366 let reservation = arrow_pool.reserve(1024);
367
368 // The Arrow-side reservation is visible as DataFusion pool usage...
369 assert_eq!(pool.reserved(), 1024);
370 assert_eq!(recording.peak_reserved(), 1024);
371
372 // ...and dropping it releases the bytes while the peak is retained.
373 drop(reservation);
374 assert_eq!(pool.reserved(), 0);
375 assert_eq!(recording.peak_reserved(), 1024);
376 }
377}