1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
//! Moving hot values out of sparse allocator spans.
//!
//! Demotion frees rows wherever they happen to sit, and an allocator can
//! hand a page back only once nothing on it is live, so a tiered store
//! that demoted a fifth of its rows keeps most of the pages they were on.
//! The allocator knows which of its spans are sparser than their class
//! (it answers through a [`DefragHint`]); only the store knows who owns a
//! value, so the store does the move: it walks its table, copies each hot
//! value the hint names into a fresh allocation — which the allocator
//! places in a denser span — and drops the original. What a span loses
//! that way it gives back whole at the next reclaim.
//!
//! Without a hint (the default, and always under the system allocator)
//! nothing here runs. The walk covers the values whose bytes dominate a
//! row store: strings, hashes, packed rows. A value shared through an
//! `Arc` (a snapshot or a read in flight holds it) stays where it is:
//! copying it would hold both.
use crate::Store;
#[cfg(not(feature = "std"))]
use crate::nostd_prelude::*;
use crate::value::{SmallBytes, Value};
use alloc::sync::Arc;
/// Whether the allocation at `ptr`, made with `(size, align)`, would move
/// to a denser span if copied. The store only asks about allocations its
/// values own, with the layout they were made with; a hint must still
/// answer safely for any address (`kevy_alloc::global::should_move` does).
///
/// ```
/// use kevy_store::{DefragHint, Store};
/// // a hint that never asks for a move: installing it changes nothing
/// let never: DefragHint = |_, _, _| false;
/// let mut s = Store::new();
/// s.set_defrag_hint(Some(never));
/// assert_eq!(s.defrag_step(64).moved, 0);
/// ```
pub type DefragHint = fn(ptr: *const u8, size: usize, align: usize) -> bool;
/// The store's side of a defrag pass: the hint, and where the walk is.
#[derive(Debug, Default)]
pub(crate) struct DefragState {
hint: Option<DefragHint>,
hand: usize,
}
/// What one [`Store::defrag_step`] did.
///
/// ```
/// use kevy_store::{DefragStep, Store};
/// let mut s = Store::new();
/// let step: DefragStep = s.defrag_step(64);
/// assert_eq!(step.moved, 0, "no hint, no move");
/// assert!(step.lap_done);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct DefragStep {
/// Values copied to a fresh allocation.
///
/// ```
/// use kevy_store::{SetCondition, Store};
/// // with every allocation hinted to move, the one heap-held value is copied
/// let mut s = Store::new();
/// s.set_slice(b"k", &[7u8; 64], None, SetCondition::Always);
/// s.set_defrag_hint(Some(|_, _, _| true));
/// let mut moved = 0;
/// while !{ let step = s.defrag_step(64); moved += step.moved; step.lap_done } {}
/// assert_eq!(moved, 1);
/// ```
pub moved: usize,
/// The walk reached the end of the table and starts over next time.
///
/// ```
/// use kevy_store::Store;
/// // an empty table is walked in one step
/// let mut s = Store::new();
/// assert!(s.defrag_step(1).lap_done);
/// ```
pub lap_done: bool,
}
/// Whether a heap-held byte string sits where the hint wants it gone. A
/// heap `SmallBytes` owns a buffer of `heap_bytes()` capacity, alignment 1.
fn bytes_hinted(hint: DefragHint, s: &SmallBytes) -> bool {
let size = s.heap_bytes();
size != 0 && hint(s.as_slice().as_ptr(), size, 1)
}
/// Whether the block an `Arc` keeps its counts and value in should move.
/// The block is laid out as std lays it out — the two counts, then the
/// value — and a hint answers safely for any address, so a layout that
/// ever differed would only make this answer no.
fn arc_hinted<T>(hint: DefragHint, a: &Arc<T>) -> bool {
let counts = core::alloc::Layout::new::<[usize; 2]>();
let Ok((block, offset)) = counts.extend(core::alloc::Layout::new::<T>()) else {
return false;
};
let block = block.pad_to_align();
let start = Arc::as_ptr(a).cast::<u8>().wrapping_sub(offset);
hint(start, block.size(), block.align())
}
/// Whether any of `v`'s dominant allocations should move, for values the
/// walk knows how to copy and nobody shares.
fn wants_move(hint: DefragHint, v: &Value) -> bool {
match v {
Value::Str(s) => bytes_hinted(hint, s),
Value::ArcBulk(b) => {
Arc::strong_count(b) == 1 && !b.is_empty() && hint(b.as_ptr(), b.len(), 1)
}
Value::Hash(h) => {
Arc::strong_count(h) == 1
&& (arc_hinted(hint, h)
|| h.table_allocation().is_some_and(|(p, l)| hint(p, l.size(), l.align()))
|| h.iter().any(|(f, x)| bytes_hinted(hint, f) || bytes_hinted(hint, x)))
}
Value::PackedRow(r) => {
let b = r.buffer();
!b.is_empty() && hint(b.as_ptr(), b.len(), 1)
}
_ => false,
}
}
/// Give `v` fresh allocations with the same contents.
fn rehome(v: &mut Value) {
match v {
Value::Str(s) => *s = s.clone(),
Value::ArcBulk(b) => *b = Arc::new(Box::from(&b[..])),
Value::Hash(h) => *h = Arc::new((**h).clone()),
Value::PackedRow(r) => *r = r.clone(),
_ => {}
}
}
impl Store {
/// Install the allocator's defrag hint, or remove it with `None`. Only
/// the process that installed a hint-capable global allocator can give
/// one; without it [`Self::defrag_step`] moves nothing.
///
/// ```
/// use kevy_store::Store;
/// fn never(_: *const u8, _: usize, _: usize) -> bool { false }
/// let mut s = Store::new();
/// s.set_defrag_hint(Some(never));
/// s.set_slice(b"k", &[b'v'; 100], None, kevy_store::SetCondition::Always);
/// assert_eq!(s.defrag_step(64).moved, 0);
/// ```
pub fn set_defrag_hint(&mut self, hint: Option<DefragHint>) {
self.defrag.hint = hint;
}
/// Walk `buckets` buckets of the table from where the last step
/// stopped, and copy every hot value the hint names to a fresh
/// allocation. Contents, TTLs, access clocks and cold keys are
/// untouched; `used_memory` follows each value's new footprint.
///
/// ```
/// use kevy_store::{SetCondition, Store};
/// // a hint that names every allocation: every hot value is copied
/// fn always(_: *const u8, _: usize, _: usize) -> bool { true }
/// let mut s = Store::new();
/// s.set_slice(b"k", &[b'v'; 100], None, SetCondition::Always);
/// s.set_defrag_hint(Some(always));
/// let mut moved = 0;
/// while !{ let step = s.defrag_step(64); moved += step.moved; step.lap_done } {}
/// assert_eq!(moved, 1);
/// assert_eq!(s.get(b"k")?.as_deref(), Some(&[b'v'; 100][..]));
/// # Ok::<(), kevy_store::StoreError>(())
/// ```
pub fn defrag_step(&mut self, buckets: usize) -> DefragStep {
let cap = self.map.capacity();
let Some(hint) = self.defrag.hint.filter(|_| cap != 0) else {
return DefragStep { moved: 0, lap_done: true };
};
let start = if self.defrag.hand < cap { self.defrag.hand } else { 0 };
let mut picked: Vec<Vec<u8>> = Vec::new();
let next = self.map.scan_buckets(start, buckets.max(1), |k, e| {
if wants_move(hint, &e.value) {
picked.push(k.to_vec());
}
});
for k in &picked {
if let Some(e) = self.map.get_mut_quiet(k) {
rehome(&mut e.value);
}
self.reweigh_entry(k);
}
let lap_done = next >= cap;
self.defrag.hand = if lap_done { 0 } else { next };
DefragStep { moved: picked.len(), lap_done }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SetCondition;
use crate::key_heap_bytes_for;
use core::time::Duration;
fn always(_: *const u8, _: usize, _: usize) -> bool {
true
}
#[test]
fn a_full_lap_moves_every_unshared_value_and_keeps_the_books() {
let mut s = Store::new();
s.set_slice(b"str", &[b's'; 300], Some(Duration::from_secs(600)), SetCondition::Always);
s.set_slice(b"small", b"inline", None, SetCondition::Always);
let long: Vec<u8> = vec![b'v'; 900];
for i in 0..200u32 {
let k = format!("row:{i}");
s.hset(k.as_bytes(), &[(b"pad".as_slice(), long.as_slice()), (b"n", b"1")]).unwrap();
}
// a snapshot holding row:0's hash
let shared = match &s.map.get(b"row:0".as_slice()).unwrap().value {
Value::Hash(h) => Arc::clone(h),
other => panic!("row:0 is {other:?}"),
};
let before: Vec<(Vec<u8>, Vec<Vec<u8>>)> = (0..200u32)
.map(|i| {
(format!("row:{i}").into_bytes(), s.hgetall(format!("row:{i}").as_bytes()).unwrap())
})
.collect();
s.set_defrag_hint(Some(always));
let mut moved = 0;
loop {
let step = s.defrag_step(64);
moved += step.moved;
if step.lap_done {
break;
}
}
assert_eq!(moved, 200, "199 unshared rows and the long string; the shared row stays");
drop(shared);
for (k, fields) in before {
assert_eq!(s.hgetall(&k).unwrap(), fields);
}
assert_eq!(s.get(b"str").unwrap().as_deref(), Some(&[b's'; 300][..]));
assert!(s.pttl(b"str") > 0, "a deadline survives the move");
let mut sum = s.keyspace_bytes;
s.map.scan_buckets(0, usize::MAX, |k, e| {
assert_eq!(e.weight(), key_heap_bytes_for(k.as_slice()) + e.value.weight(), "{k:?}");
sum += e.weight();
});
assert_eq!(s.used_memory(), sum);
}
fn never(_: *const u8, _: usize, _: usize) -> bool {
false
}
/// Names byte buffers only: no `Arc` block and no hash table.
fn bytes_only(_: *const u8, _: usize, align: usize) -> bool {
align == 1
}
fn lap(s: &mut Store) -> usize {
let mut moved = 0;
loop {
let step = s.defrag_step(16);
moved += step.moved;
if step.lap_done {
return moved;
}
}
}
/// One value of every kind the walk looks at, keyed by what it is.
fn mixed() -> Store {
let mut s = Store::new();
s.set_slice(b"str", &[b's'; 40], None, SetCondition::Always);
s.set_slice(b"inline", b"short", None, SetCondition::Always);
s.set_slice(b"int", b"42", None, SetCondition::Always);
s.rpush(b"list", &[b"x"]).unwrap();
s.hset(b"spilled", &[(b"f".as_slice(), [b'v'; 100].as_slice())]).unwrap();
s.hset(b"flat", &[(b"f".as_slice(), b"v".as_slice())]).unwrap();
let wide: Vec<(Vec<u8>, &[u8])> =
(0..200).map(|i| (format!("f{i}").into_bytes(), b"v".as_slice())).collect();
let pairs: Vec<(&[u8], &[u8])> = wide.iter().map(|(f, v)| (&f[..], *v)).collect();
s.hset(b"wide", &pairs).unwrap();
s.hset(b"row", &[(b"id".as_slice(), b"7".as_slice())]).unwrap();
let table: crate::packed_row::ColumnNames = vec![b"id".to_vec()].into();
s.pack_row(b"row", &table);
assert!(s.is_packed(b"row"));
s
}
#[test]
fn without_a_hint_or_a_table_a_step_does_nothing() {
let mut s = mixed();
assert_eq!(s.defrag_step(64), DefragStep { moved: 0, lap_done: true });
let mut empty = Store::new();
empty.set_defrag_hint(Some(always));
assert_eq!(empty.defrag_step(64), DefragStep { moved: 0, lap_done: true });
}
#[test]
fn a_hint_that_names_nothing_moves_nothing() {
let mut s = mixed();
let before = s.used_memory();
s.set_defrag_hint(Some(never));
assert_eq!(lap(&mut s), 0);
assert_eq!(s.used_memory(), before);
}
#[test]
fn a_hint_on_byte_buffers_moves_the_values_that_hold_one() {
let mut s = mixed();
s.set_defrag_hint(Some(bytes_only));
// the heap string, the hash with a spilled value, the packed row
assert_eq!(lap(&mut s), 3);
assert_eq!(s.get(b"str").unwrap().as_deref(), Some(&[b's'; 40][..]));
assert_eq!(s.hget(b"spilled", b"f").unwrap(), Some(&[b'v'; 100][..]));
assert_eq!(s.hget(b"row", b"id").unwrap(), Some(&b"7"[..]));
assert!(s.is_packed(b"row"), "a moved row keeps its form");
assert_eq!(s.hlen(b"wide").unwrap(), 200);
}
#[test]
fn a_walk_whose_table_shrank_starts_over() {
let mut s = Store::new();
for i in 0..300u32 {
s.set_slice(format!("k{i}").as_bytes(), &[b'v'; 40], None, SetCondition::Always);
}
s.set_defrag_hint(Some(always));
assert!(!s.defrag_step(100).lap_done);
drop(s.detach_entries());
s.set_slice(b"k", &[b'v'; 40], None, SetCondition::Always);
assert_eq!(s.defrag_step(64), DefragStep { moved: 1, lap_done: true });
}
}