use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq)]
enum Val {
#[default]
Absent,
Inline {
len: u8,
buf: [u8; 23],
},
Heap(Vec<u8>),
}
impl Val {
fn store(v: &[u8]) -> Val {
if v.len() <= 23 {
let mut buf = [0u8; 23];
buf[..v.len()].copy_from_slice(v);
Val::Inline { len: v.len() as u8, buf }
} else {
Val::Heap(v.to_vec())
}
}
fn bytes(&self) -> Option<&[u8]> {
match self {
Val::Absent => None,
Val::Inline { len, buf } => Some(&buf[..*len as usize]),
Val::Heap(v) => Some(v),
}
}
fn heap(&self) -> u64 {
match self {
Val::Heap(v) => (v.len().max(1) as u64).next_multiple_of(16) + 16,
_ => 0,
}
}
}
fn row_bytes(key_len: usize, slots: &[Val]) -> u64 {
key_len as u64
+ slots.len() as u64 * std::mem::size_of::<Val>() as u64
+ slots.iter().map(Val::heap).sum::<u64>()
}
#[derive(Debug)]
pub(crate) struct RowValues {
n: usize,
rows: HashMap<Vec<u8>, Box<[Val]>>,
heap: u64,
}
impl RowValues {
pub(crate) fn new(n: usize) -> Self {
Self { n, rows: HashMap::new(), heap: 0 }
}
pub(crate) fn set(&mut self, key: &[u8], values: &[Option<&[u8]>]) {
let mut slots = vec![Val::Absent; self.n].into_boxed_slice();
for (f, slot) in slots.iter_mut().enumerate() {
if let Some(v) = values.get(f).copied().flatten() {
*slot = Val::store(v);
}
}
self.heap += row_bytes(key.len(), &slots);
if let Some(old) = self.rows.insert(key.to_vec(), slots) {
self.heap = self.heap.saturating_sub(row_bytes(key.len(), &old));
}
}
pub(crate) fn clear(&mut self, key: &[u8]) {
if let Some(old) = self.rows.remove(key) {
self.heap = self.heap.saturating_sub(row_bytes(key.len(), &old));
}
}
pub(crate) fn get(&self, key: &[u8], field: usize) -> Option<&[u8]> {
self.rows.get(key)?.get(field)?.bytes()
}
pub(crate) fn approx_bytes(&self) -> u64 {
self.heap
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_value_slot_is_thirty_two_bytes_either_way() {
assert_eq!(std::mem::size_of::<Val>(), 32);
}
#[test]
fn short_values_stay_inline_long_ones_spill() {
assert!(matches!(Val::store(b"42"), Val::Inline { .. }));
assert!(matches!(Val::store(&[b'x'; 24]), Val::Heap(_)));
assert_eq!(Val::store(b"").bytes(), Some(&b""[..]), "empty is a value");
assert_eq!(Val::Absent.bytes(), None, "absent is not");
}
#[test]
fn set_get_clear_across_the_stride() {
let mut rv = RowValues::new(2);
rv.set(b"u:1", &[Some(b"active"), Some(&[b'y'; 40])]);
assert_eq!(rv.get(b"u:1", 0), Some(&b"active"[..]));
assert_eq!(rv.get(b"u:1", 1), Some(&[b'y'; 40][..]), "long values round-trip");
assert_eq!(rv.get(b"u:1", 2), None, "past the declared arity");
assert_eq!(rv.get(b"u:2", 0), None, "a row that was never set");
rv.set(b"u:1", &[Some(b"gone")]);
assert_eq!(rv.get(b"u:1", 1), None, "not carried over from the last write");
rv.clear(b"u:1");
assert_eq!(rv.get(b"u:1", 0), None);
}
#[test]
fn approx_bytes_counts_the_spill() {
let mut rv = RowValues::new(1);
rv.set(b"k", &[Some(b"short")]);
let inline_only = rv.approx_bytes();
rv.set(b"k", &[Some(&[b'z'; 200])]);
assert!(rv.approx_bytes() > inline_only, "a spilled value costs heap");
}
#[test]
fn incremental_heap_matches_a_full_rescan() {
let recompute = |rv: &RowValues| -> u64 {
rv.rows
.iter()
.map(|(k, slots)| row_bytes(k.len(), slots))
.sum()
};
let mut rv = RowValues::new(2);
rv.set(b"a", &[Some(b"x"), Some(&[b'z'; 40])]);
rv.set(b"bb", &[Some(&[b'y'; 100]), None]);
rv.set(b"a", &[Some(b"short"), None]); rv.set(b"ccc", &[Some(b"1"), Some(b"2")]);
rv.clear(b"bb");
rv.clear(b"missing"); assert_eq!(rv.approx_bytes(), recompute(&rv), "counter drifted from truth");
rv.clear(b"a");
rv.clear(b"ccc");
assert_eq!(rv.approx_bytes(), recompute(&rv));
assert_eq!(rv.approx_bytes(), 0, "an empty side-channel costs nothing");
}
}