Skip to content

Commit 133a526

Browse files
authored
refactor(rust): Add statistics to spill contexts (#27859)
1 parent 583ec85 commit 133a526

2 files changed

Lines changed: 189 additions & 25 deletions

File tree

crates/polars-ooc/src/lib.rs

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::pin::Pin;
1212
use std::sync::atomic::{AtomicU64, Ordering};
1313
use std::sync::{Arc, Mutex};
1414
use std::task::{Poll, Waker};
15+
use std::time::Instant;
1516

1617
pub use global_alloc::{Allocator, estimate_memory_usage};
1718
pub use memory_manager::memory_manager;
@@ -24,6 +25,8 @@ pub use spill_context::{
2425
};
2526
pub use spill_frame::SpillFrame;
2627

28+
use crate::spill_context::SpillContextStatistics;
29+
2730
// SpillTokenInner's state
2831
const SPILLED_BIT: u64 = 1; // Set when value = None and spilled is Some.
2932
const DROPPED_BIT: u64 = 2; // Set when the token owner has dropped. Forbids creation of new pins / lock.
@@ -32,13 +35,22 @@ const HAS_WAITERS_BIT: u64 = 8; // Only updated while holding waiters lock.
3235
const RO_PIN_COUNT_UNIT: u64 = 16; // Added to the state for each active pin.
3336
const RO_PIN_MASK: u64 = u64::MAX << 4;
3437

38+
enum ValueSlot<T> {
39+
InMemory(T),
40+
Spilled {
41+
n_bytes: usize,
42+
ctx_stats: Arc<SpillContextStatistics>,
43+
},
44+
Dropped,
45+
}
46+
3547
struct SpillTokenInner<T: Spillable> {
3648
// May be read if holding LOCK_BIT or a pin, may be written while holding
3749
// LOCK_BIT and no pins exist.
38-
value: UnsafeCell<Option<T>>,
50+
value_slot: UnsafeCell<ValueSlot<T>>,
3951

4052
// May be read+written while holding LOCK_BIT (irrespective of pins).
41-
spilled: UnsafeCell<Option<T::Spilled>>,
53+
spilled_value: UnsafeCell<Option<T::Spilled>>,
4254

4355
// Lock should not be held for long, only used to register/wake waiters.
4456
waiters: Mutex<UnitVec<Waker>>,
@@ -192,9 +204,15 @@ impl<T: Spillable> SpillTokenInner<T> {
192204
slf.wake_waiters(slf.state.fetch_and(!LOCK_BIT, Ordering::AcqRel));
193205
});
194206
// TODO: re-register unspilled frame?
195-
let spilled = (*self.spilled.get()).as_ref().unwrap();
207+
let unspill_start = Instant::now();
208+
let spilled = (*self.spilled_value.get()).as_ref().unwrap();
196209
let value = T::unspill(spilled).await;
197-
self.value.get().write(Some(value));
210+
let ValueSlot::Spilled { n_bytes, ctx_stats } =
211+
self.value_slot.get().replace(ValueSlot::InMemory(value))
212+
else {
213+
unreachable!()
214+
};
215+
ctx_stats.add_unspill(n_bytes, unspill_start);
198216

199217
WithDrop::dismiss(lock_guard);
200218
self.wake_waiters(
@@ -222,16 +240,19 @@ impl<T: Spillable> SpillTokenInner<T> {
222240
slf.wake_waiters(slf.state.fetch_and(!LOCK_BIT, Ordering::AcqRel));
223241
});
224242

225-
let value = &mut *self.value.get();
226-
if value.is_none() {
243+
let value_slot = &mut *self.value_slot.get();
244+
if let ValueSlot::Spilled { n_bytes, ctx_stats } = value_slot {
227245
debug_assert!(self.state.load(Ordering::Relaxed) & SPILLED_BIT == SPILLED_BIT);
228246
// TODO: re-register unspilled frame?
229-
let spilled = (*self.spilled.get()).take().unwrap();
230-
*value = Some(T::unspill(&spilled).await);
247+
let unspill_start = Instant::now();
248+
let spilled = (*self.spilled_value.get()).take().unwrap();
249+
let value = T::unspill(&spilled).await;
250+
ctx_stats.add_unspill(*n_bytes, unspill_start);
251+
*value_slot = ValueSlot::InMemory(value);
231252
self.state.fetch_sub(SPILLED_BIT, Ordering::Relaxed);
232253
} else {
233254
// We are about to mutate, making our current spill invalid.
234-
*self.spilled.get() = None;
255+
*self.spilled_value.get() = None;
235256
}
236257

237258
WithDrop::dismiss(lock_guard);
@@ -264,8 +285,8 @@ impl<T: Spillable> SpillTokenInner<T> {
264285
// allowing us to clean up here.
265286
let old_state = self.state.fetch_or(DROPPED_BIT, Ordering::Acquire);
266287
if old_state & (LOCK_BIT | RO_PIN_MASK) == 0 {
267-
self.value.get().replace(None);
268-
self.spilled.get().replace(None);
288+
self.value_slot.get().replace(ValueSlot::Dropped);
289+
self.spilled_value.get().replace(None);
269290
}
270291
self.registration_id.fetch_add(1, Ordering::Relaxed);
271292
}
@@ -294,7 +315,10 @@ trait DynSpillToken: Send + Sync + 'static {
294315
/// May return false if the token is already spilled, or is currently pinned.
295316
/// It may also return None in those cases (to avoid the allocation of the future).
296317
#[expect(unused)]
297-
fn try_spill(&self) -> Option<Pin<Box<dyn Future<Output = bool> + Send + '_>>>;
318+
fn try_spill(
319+
&self,
320+
stats: &Arc<SpillContextStatistics>,
321+
) -> Option<Pin<Box<dyn Future<Output = bool> + Send + '_>>>;
298322
}
299323

300324
impl<T: Spillable> DynSpillToken for SpillTokenInner<T> {
@@ -315,7 +339,10 @@ impl<T: Spillable> DynSpillToken for SpillTokenInner<T> {
315339
self.try_pin().map(|p| p.estimate_byte_size())
316340
}
317341

318-
fn try_spill(&self) -> Option<Pin<Box<dyn Future<Output = bool> + Send + '_>>> {
342+
fn try_spill(
343+
&self,
344+
stats: &Arc<SpillContextStatistics>,
345+
) -> Option<Pin<Box<dyn Future<Output = bool> + Send + '_>>> {
319346
// First, we try setting the lock bit. If anyone else has a pin we don't bother.
320347
let pin_update = self
321348
.state
@@ -329,8 +356,10 @@ impl<T: Spillable> DynSpillToken for SpillTokenInner<T> {
329356
return None;
330357
}
331358

332-
Some(Box::pin(async {
333-
let needs_spill = unsafe { (*self.spilled.get()).is_none() };
359+
let stats = Arc::clone(stats);
360+
Some(Box::pin(async move {
361+
let spill_start = Instant::now();
362+
let needs_spill = unsafe { (*self.spilled_value.get()).is_none() };
334363
let is_exclusive = if needs_spill {
335364
// Relax the lock to a pin while spilling such that new pins can
336365
// come in. After all, we still have it in memory right now.
@@ -347,7 +376,7 @@ impl<T: Spillable> DynSpillToken for SpillTokenInner<T> {
347376
.state
348377
.fetch_add(LOCK_BIT.wrapping_sub(RO_PIN_COUNT_UNIT), Ordering::Acquire);
349378
unsafe {
350-
self.spilled.get().write(Some(spilled));
379+
self.spilled_value.get().write(Some(spilled));
351380
}
352381
old_state & RO_PIN_MASK == RO_PIN_COUNT_UNIT
353382
} else {
@@ -357,10 +386,21 @@ impl<T: Spillable> DynSpillToken for SpillTokenInner<T> {
357386
let state = if is_exclusive {
358387
// We are the only pin and hold the lock meaning no one else can
359388
// access value or create new pins.
360-
unsafe { self.value.get().replace(None) };
389+
let n_bytes = match unsafe { &*self.value_slot.get() } {
390+
ValueSlot::InMemory(val) => val.estimate_byte_size(),
391+
_ => unreachable!(),
392+
};
393+
unsafe {
394+
self.value_slot.get().replace(ValueSlot::Spilled {
395+
n_bytes,
396+
ctx_stats: stats.clone(),
397+
})
398+
};
399+
stats.add_successful_spill(n_bytes, spill_start);
361400
self.state
362401
.fetch_add(SPILLED_BIT.wrapping_sub(LOCK_BIT), Ordering::AcqRel)
363402
} else {
403+
stats.add_failed_spill(spill_start);
364404
self.state.fetch_sub(LOCK_BIT, Ordering::AcqRel)
365405
};
366406
self.wake_waiters(state);
@@ -379,8 +419,8 @@ impl<T: Spillable> SpillToken<T> {
379419
/// Creates a new SpillToken containing the given value.
380420
pub fn new(value: T) -> Self {
381421
let inner = Arc::new(SpillTokenInner {
382-
value: UnsafeCell::new(Some(value)),
383-
spilled: UnsafeCell::new(None),
422+
value_slot: UnsafeCell::new(ValueSlot::InMemory(value)),
423+
spilled_value: UnsafeCell::new(None),
384424
registration_id: AtomicU64::new(0),
385425
state: AtomicU64::new(0),
386426
waiters: Mutex::default(),
@@ -416,13 +456,21 @@ impl<T: Spillable> SpillToken<T> {
416456
/// Consumes this SpillToken, unspilling it if it were spilled.
417457
pub async fn into_inner(mut self) -> T {
418458
let pin = self.get_mut().await;
419-
unsafe { &mut *pin.inner.value.get() }.take().unwrap()
459+
let slot = unsafe { pin.inner.value_slot.get().replace(ValueSlot::Dropped) };
460+
let ValueSlot::InMemory(value) = slot else {
461+
unreachable!()
462+
};
463+
value
420464
}
421465

422466
/// Blocking version of into_inner.
423467
pub fn into_inner_blocking(mut self) -> T {
424468
let pin = self.get_mut_blocking();
425-
unsafe { &mut *pin.inner.value.get() }.take().unwrap()
469+
let slot = unsafe { pin.inner.value_slot.get().replace(ValueSlot::Dropped) };
470+
let ValueSlot::InMemory(value) = slot else {
471+
unreachable!()
472+
};
473+
value
426474
}
427475
}
428476

@@ -440,7 +488,11 @@ impl<'a, T: Spillable> Deref for PinnedRef<'a, T> {
440488
type Target = T;
441489

442490
fn deref(&self) -> &Self::Target {
443-
unsafe { &*self.inner.value.get() }.as_ref().unwrap()
491+
let slot = unsafe { &*self.inner.value_slot.get() };
492+
let ValueSlot::InMemory(value) = slot else {
493+
unreachable!()
494+
};
495+
value
444496
}
445497
}
446498

@@ -458,13 +510,21 @@ impl<'a, T: Spillable> Deref for PinnedMut<'a, T> {
458510
type Target = T;
459511

460512
fn deref(&self) -> &Self::Target {
461-
unsafe { &*self.inner.value.get() }.as_ref().unwrap()
513+
let slot = unsafe { &*self.inner.value_slot.get() };
514+
let ValueSlot::InMemory(value) = slot else {
515+
unreachable!()
516+
};
517+
value
462518
}
463519
}
464520

465521
impl<'a, T: Spillable> DerefMut for PinnedMut<'a, T> {
466522
fn deref_mut(&mut self) -> &mut Self::Target {
467-
unsafe { &mut *self.inner.value.get() }.as_mut().unwrap()
523+
let slot = unsafe { &mut *self.inner.value_slot.get() };
524+
let ValueSlot::InMemory(value) = slot else {
525+
unreachable!()
526+
};
527+
value
468528
}
469529
}
470530

0 commit comments

Comments
 (0)