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