Skip to main content

kevy_store/
value_weight.rs

1//! `Value`'s two accounting questions: what does it weigh, and is freeing it
2//! heavy enough to hand to the bio thread.
3//!
4//! Split from `value.rs` for the file-length rule. Both are pure per-variant
5//! tables over the enum defined there, so they move as a pair and nothing in
6//! `value.rs` calls them.
7
8use crate::value::{
9    HASH_SLOT_BYTES, HEAP_HEAVY_BYTES, LIST_SLOT_BYTES, RANKTREE_SLOT_BYTES, SET_SLOT_BYTES, Value,
10    collection_overhead,
11};
12
13impl Value {
14    /// Approximate heap bytes the value owns. Excludes the inline `Entry` /
15    /// bucket slot — that's a separate per-entry constant accounted by the
16    /// store. Walks collections, so prefer the cached `Entry::weight` for
17    /// hot-path accounting and only call this when bootstrapping or after a
18    /// load-from-snapshot.
19    // LOC-WAIVER: pure per-variant weight table — one arm per stored encoding, no control flow
20    pub fn weight(&self) -> u64 {
21        match self {
22            Value::Str(s) => s.heap_bytes() as u64,
23            // i64 fits in the enum tag's space; no heap.
24            Value::Int(_) => 0,
25            // One payload buffer plus the boxed inner; nothing per column.
26            Value::PackedRow(r) => r.heap_bytes() as u64,
27            // Arc<[u8]> heap = the byte slice itself (refcount overhead
28            // is amortised across shared clones).
29            Value::ArcBulk(a) => a.len() as u64,
30            Value::Hash(h) => {
31                collection_overhead(h.capacity(), HASH_SLOT_BYTES)
32                    + h.iter()
33                        .map(|(f, v)| f.heap_bytes() as u64 + v.heap_bytes() as u64)
34                        .sum::<u64>()
35            }
36            Value::List(l) => {
37                (l.capacity() as u64).saturating_mul(LIST_SLOT_BYTES)
38                    + l.iter().map(|v| v.capacity() as u64).sum::<u64>()
39            }
40            // Segments charge like flat lists; the outer deque-of-Arcs
41            // adds one pointer slot per segment.
42            Value::SegList(l) => {
43                (l.seg_count() as u64).saturating_mul(8)
44                    + (l.len() as u64).saturating_mul(LIST_SLOT_BYTES)
45                    + l.iter().map(|v| v.capacity() as u64).sum::<u64>()
46            }
47            Value::Set(s) => {
48                collection_overhead(s.capacity(), SET_SLOT_BYTES)
49                    + s.iter().map(|m| m.heap_bytes() as u64).sum::<u64>()
50            }
51            Value::SegHash(h) => h.weight_as_hash(),
52            Value::SegSet(s) => s.weight_as_set(),
53            Value::SegZSet(z) => z.weight_as_zset(),
54            // Inline collections live entirely in the Value variant
55            // body — zero heap, zero bucket overhead. Accounting matches
56            // `Value::Int` / inline `Value::Str` (both also return 0).
57            Value::SmallSetInline(_)
58            | Value::SmallHashInline(_)
59            | Value::SmallListInline(_)
60            | Value::SmallZSetInline(_) => 0,
61            // The stub owns no heap — its 24 bytes live inline in the
62            // Entry. The reclaimed value bytes are exactly the point:
63            // a cold key weighs key-heap + ENTRY_OVERHEAD only (B7).
64            Value::Cold(_) => 0,
65            // Each member's bytes live twice when they spill to heap (>22 B):
66            // once as the `by_member` key, once inside the rank tree's
67            // `(Score, SmallBytes)` key — hence the ×2 on `heap_bytes`.
68            // Members ≤22 B are inline in both slots (heap_bytes = 0).
69            Value::ZSet(z) => {
70                collection_overhead(z.by_member.capacity(), HASH_SLOT_BYTES)
71                    + z.by_member.iter().map(|(m, _)| 2 * m.heap_bytes() as u64).sum::<u64>()
72                    + (z.by_score.len() as u64).saturating_mul(RANKTREE_SLOT_BYTES)
73            }
74            Value::Stream(s) => s.weight(),
75        }
76    }
77
78    /// Whether this value's `Drop` is heavy enough to deserve being
79    /// shipped to the bio thread instead of freed inline. Fast: every
80    /// variant decides off a sub-field cheap to inspect (no recursive
81    /// walk), so it's safe to call on every overwrite-SET on the hot
82    /// path. The threshold is intentionally conservative — small Arcs
83    /// and every short string stay on inline-drop where jemalloc small-
84    /// class is sub-µs and a cross-thread hand-off would lose.
85    #[inline]
86    // LOC-WAIVER: pure per-variant predicate table — one arm per stored encoding, no control flow
87    pub fn is_heap_heavy(&self) -> bool {
88        match self {
89            // Inline 22 B / heap ≤ small-class — fast to free inline.
90            Value::Str(_)
91            | Value::Int(_)
92            | Value::SmallSetInline(_)
93            | Value::SmallHashInline(_)
94            | Value::SmallListInline(_)
95            | Value::SmallZSetInline(_) => false,
96            // 24 inline bytes; dropping a stub frees nothing.
97            Value::Cold(_) => false,
98            // Lazy-drop's primary case: the large-value SET tail culprit.
99            Value::ArcBulk(a) => a.len() >= HEAP_HEAVY_BYTES,
100            // One buffer: the same size test, one deallocation.
101            Value::PackedRow(r) => r.heap_bytes() >= HEAP_HEAVY_BYTES,
102            // Collection drops walk every element + the bucket array;
103            // worst-case microseconds on a multi-KB hash/zset. Send to
104            // bio so a SET that overwrites a collection-typed key (the
105            // Redis polymorphic case) doesn't stall the reactor.
106            //
107            // The check uses `Arc::strong_count == 1` to avoid sending
108            // a still-shared Arc: another holder (a SnapshotView in
109            // flight, a same-shard live read) would force the bio
110            // thread to only do a refcount-decrement, which is wasted
111            // cross-thread traffic. A unique Arc IS the case where
112            // drop is expensive (it really frees the inner payload).
113            Value::Hash(a) => alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty(),
114            Value::SegHash(a) => {
115                alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty() && a.all_unique()
116            }
117            Value::SegSet(a) => {
118                alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty() && a.all_unique()
119            }
120            Value::SegZSet(a) => {
121                alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty() && a.all_unique()
122            }
123            Value::List(a) => alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty(),
124            // Bio-drop only pays off when the drop really frees: outer
125            // AND every segment unique. A view-shared SegList's drop is
126            // refcount decrements — cheap enough inline.
127            Value::SegList(a) => {
128                alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty() && a.all_unique()
129            }
130            Value::Set(a) => alloc::sync::Arc::strong_count(a) == 1 && !a.is_empty(),
131            Value::ZSet(a) => alloc::sync::Arc::strong_count(a) == 1 && !a.by_member.is_empty(),
132            Value::Stream(a) => alloc::sync::Arc::strong_count(a) == 1 && a.length() > 0,
133        }
134    }
135}