Skip to main content

kevy_store/
bio_drop.rs

1//! Bio-drop batching: heavy `Value`s displaced by SET
2//! overwrites are shipped to the runtime's bio thread in per-iteration
3//! batches instead of dropping inline on the reactor.
4
5use crate::value::{self, Value};
6use crate::Store;
7
8/// Maximum [`Store::pending_drops`] depth before forcing a flush
9/// inside `maybe_offload_drop` (rather than waiting for the reactor's
10/// per-iter `flush_pending_drops`). Caps memory held in the batch
11/// buffer at ≤ 64 × sizeof(Box<Value>) (≤ 512 B of pointers + whatever
12/// the boxed payloads weigh — which we WANT to ship anyway, since
13/// holding the bio-bound batch defeats the point of off-reactor frees).
14/// 64 picked as: amortises mpsc send cost (~few hundred ns) across
15/// enough drops that per-drop overhead is ≤ 10 ns, while staying small
16/// enough that worst-case bunch-up latency at the bio thread is bounded.
17pub(crate) const MAX_PENDING_DROPS: usize = 64;
18
19impl Store {
20    /// Install the runtime's bio-drop channel. Called
21    /// once from `kevy-rt::Runtime::run` per shard before the reactor
22    /// loop starts. After install, [`Self::maybe_offload_drop`] (invoked
23    /// from the SET overwrite fast path) accumulates oversize `Value`s
24    /// into a per-shard batch; the reactor calls
25    /// [`Self::flush_pending_drops`] at the end of every iter to ship
26    /// the batch in one mpsc send. Bounds the 10 KB-SET p999/max
27    /// latency blow-up that synchronous `Box::<[u8]>::drop` of an
28    /// allocator large-class slot causes (see `kevy_rt::bio`).
29    #[inline]
30    pub fn set_bio_drop_sender(&mut self, sender: value::BioDropSender) {
31        self.bio_drop_sender = Some(sender);
32    }
33
34    /// Accumulate `old` into the per-shard bio-drop batch buffer
35    /// ([`Store::pending_drops`]) if it's heap-heavy AND a bio channel
36    /// is installed. Otherwise drop inline. The hot path is one branch
37    /// on `bio_drop_sender.is_none()` followed by the variant-cheap
38    /// [`Value::is_heap_heavy`] check; for the `Value::Str(SmallBytes)`
39    /// steady state of typical bench shapes the inline-drop path is
40    /// preserved unchanged.
41    ///
42    /// **Batch model**: per-send mpsc cost (atomic +
43    /// cross-thread cacheline) is amortised across the batch by
44    /// [`Self::flush_pending_drops`], which the reactor calls once per
45    /// iter. Force-flushes here when the buffer hits
46    /// [`MAX_PENDING_DROPS`] to bound RAM in-flight.
47    #[inline]
48    pub(crate) fn maybe_offload_drop(&mut self, old: Value) {
49        if self.bio_drop_sender.is_none() {
50            // No channel (bare Store / embedded reaper / tests): the
51            // Value falls out of scope and drops inline.
52            drop(old);
53            return;
54        }
55        if !old.is_heap_heavy() {
56            // Under-threshold: jemalloc small-class free is sub-µs.
57            // The Vec::push + force-flush branch costs more than the
58            // inline free for this size — leave it inline.
59            drop(old);
60            return;
61        }
62        self.pending_drops.push(old);
63        if self.pending_drops.len() >= MAX_PENDING_DROPS {
64            self.flush_pending_drops();
65        }
66    }
67
68    /// Ship the per-shard bio-drop batch buffer to the bio thread in
69    /// one mpsc send. Called from `kevy-rt`'s reactor loop at the end
70    /// of every iteration (both the epoll `Shard::run` and the io_uring
71    /// `Shard::run_uring` paths, just before the AOF fsync window so a
72    /// pending fsync stall doesn't pin a batch-ful of heavy values in
73    /// per-shard memory).
74    ///
75    /// Empty-buffer fast path: zero work, predictable not-taken
76    /// branch. Reactor calls this unconditionally per iter; the steady-
77    /// state cost for a no-SET-overwrite iter is one length check.
78    ///
79    /// `SendError` here means the bio thread has exited (shutdown
80    /// territory — `Runtime::run` has dropped its sender AFTER the
81    /// shard threads joined). Drop the batch inline; the `SendError`
82    /// payload carries the `Vec` back so its `Box<Value>`s run their
83    /// Drop here, preserving correctness.
84    #[inline]
85    pub fn flush_pending_drops(&mut self) {
86        if self.pending_drops.is_empty() {
87            return;
88        }
89        let tx = match self.bio_drop_sender.as_ref() {
90            Some(tx) => tx,
91            // Shouldn't happen — caller (`maybe_offload_drop`) only
92            // pushes when the sender exists. Defensive: if a future
93            // refactor invokes `flush_pending_drops` from somewhere
94            // unconditional, drop the batch inline.
95            None => {
96                self.pending_drops.clear();
97                return;
98            }
99        };
100        let batch = core::mem::take(&mut self.pending_drops);
101        if let Err(_send_err) = tx.send(batch) {
102            // Bio thread is gone (shutdown). The SendError carries
103            // the Vec, which drops here — every Box<Value> runs its
104            // Drop inline. Benign one-time stall during tear-down.
105        }
106    }
107}