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}