Skip to main content

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}