mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-12-23 08:21:09 +00:00
224 lines
6.9 KiB
Rust
224 lines
6.9 KiB
Rust
use crate::turso_assert;
|
|
|
|
#[derive(Debug)]
|
|
/// Fixed-size bitmap for tracking allocated slots in an arena.
|
|
///
|
|
/// Bit meaning:
|
|
/// - 1 = free
|
|
/// - 0 = allocated
|
|
pub(super) struct SlotBitmap {
|
|
words: Box<[u64]>,
|
|
n_slots: u32,
|
|
/// Hint for where the next allocation scan should start (word index).
|
|
next_word: usize,
|
|
}
|
|
|
|
impl SlotBitmap {
|
|
/// 64 bits per word, so shift by 6 to get slot index.
|
|
const WORD_SHIFT: u32 = 6;
|
|
const WORD_BITS: u32 = 64;
|
|
const WORD_MASK: u32 = 63;
|
|
|
|
const ALL_FREE: u64 = u64::MAX;
|
|
const ALL_ALLOCATED: u64 = 0u64;
|
|
|
|
/// Creates a new `SlotBitmap` capable of tracking `n_slots` slots.
|
|
pub fn new(n_slots: u32) -> Self {
|
|
turso_assert!(n_slots > 0, "number of slots must be non-zero");
|
|
turso_assert!(
|
|
n_slots % Self::WORD_BITS == 0,
|
|
"number of slots in map must be a multiple of 64"
|
|
);
|
|
let n_words = (n_slots / Self::WORD_BITS) as usize;
|
|
let words = vec![Self::ALL_FREE; n_words].into_boxed_slice();
|
|
Self {
|
|
words,
|
|
n_slots,
|
|
next_word: 0,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
const fn word_and_bit_to_slot(word_idx: usize, bit: u32) -> u32 {
|
|
(word_idx as u32) << Self::WORD_SHIFT | bit
|
|
}
|
|
|
|
#[inline]
|
|
const fn slot_to_word_and_bit(slot_idx: u32) -> (usize, u32) {
|
|
(
|
|
(slot_idx >> Self::WORD_SHIFT) as usize,
|
|
slot_idx & Self::WORD_MASK,
|
|
)
|
|
}
|
|
|
|
/// Returns whether a slot is currently free.
|
|
pub(super) fn is_free(&self, slot: u32) -> bool {
|
|
if slot >= self.n_slots {
|
|
return false;
|
|
}
|
|
let (word_idx, bit) = Self::slot_to_word_and_bit(slot);
|
|
(self.words[word_idx] & (1u64 << bit)) != 0
|
|
}
|
|
|
|
/// Allocates a single free slot from the bitmap.
|
|
pub fn alloc_one(&mut self) -> Option<u32> {
|
|
let n_words = self.words.len();
|
|
if n_words == 0 {
|
|
return None;
|
|
}
|
|
|
|
let mut start = self.next_word.min(n_words - 1);
|
|
for _pass in 0..2 {
|
|
for word_idx in start..n_words {
|
|
let word = self.words[word_idx];
|
|
if word == Self::ALL_ALLOCATED {
|
|
continue;
|
|
}
|
|
let bit = word.trailing_zeros();
|
|
self.words[word_idx] &= !(1u64 << bit);
|
|
|
|
if self.words[word_idx] == Self::ALL_ALLOCATED {
|
|
self.next_word = (word_idx + 1) % n_words;
|
|
} else {
|
|
self.next_word = word_idx;
|
|
}
|
|
|
|
return Some(Self::word_and_bit_to_slot(word_idx, bit));
|
|
}
|
|
start = 0;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Frees a previously allocated slot.
|
|
pub fn free_one(&mut self, slot: u32) {
|
|
turso_assert!(slot < self.n_slots, "free_one out of bounds");
|
|
let (word_idx, bit) = Self::slot_to_word_and_bit(slot);
|
|
let mask = 1u64 << bit;
|
|
turso_assert!((self.words[word_idx] & mask) == 0, "slot already free");
|
|
self.words[word_idx] |= mask;
|
|
if word_idx < self.next_word {
|
|
self.next_word = word_idx;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
use super::*;
|
|
use rand::{rngs::StdRng, Rng, SeedableRng};
|
|
|
|
fn pb_free_vec(pb: &SlotBitmap) -> Vec<bool> {
|
|
let mut v = vec![false; pb.n_slots as usize];
|
|
v.iter_mut()
|
|
.enumerate()
|
|
.take(pb.n_slots as usize)
|
|
.map(|(i, v)| {
|
|
let w = pb.words[i >> 6];
|
|
let bit = i & 63;
|
|
*v = (w & (1u64 << bit)) != 0;
|
|
*v
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Returns `true` if the bitmap's notion of free bits equals the reference model exactly.
|
|
fn assert_equivalent(pb: &SlotBitmap, model: &[bool]) {
|
|
let pv = pb_free_vec(pb);
|
|
assert_eq!(pv, model, "bitmap bits disagree with reference model");
|
|
}
|
|
|
|
#[test]
|
|
fn alloc_one_exhausts_all() {
|
|
let mut pb = SlotBitmap::new(256);
|
|
let mut model = vec![true; 256];
|
|
|
|
let mut count = 0;
|
|
while let Some(idx) = pb.alloc_one() {
|
|
assert!(model[idx as usize], "must be free in model");
|
|
model[idx as usize] = false;
|
|
count += 1;
|
|
}
|
|
assert_eq!(count, 256, "should allocate all slots once");
|
|
assert!(pb.alloc_one().is_none(), "no slots left");
|
|
assert_equivalent(&pb, &model);
|
|
}
|
|
|
|
#[test]
|
|
fn free_one_allows_reuse() {
|
|
let mut pb = SlotBitmap::new(128);
|
|
let mut model = vec![true; 128];
|
|
|
|
let a = pb.alloc_one().unwrap();
|
|
let b = pb.alloc_one().unwrap();
|
|
model[a as usize] = false;
|
|
model[b as usize] = false;
|
|
|
|
pb.free_one(a);
|
|
model[a as usize] = true;
|
|
assert_equivalent(&pb, &model);
|
|
|
|
let c = pb.alloc_one().unwrap();
|
|
model[c as usize] = false;
|
|
assert_equivalent(&pb, &model);
|
|
}
|
|
|
|
#[test]
|
|
fn freeing_earlier_slot_updates_hint() {
|
|
let mut pb = SlotBitmap::new(64);
|
|
let mut allocated = Vec::new();
|
|
while let Some(s) = pb.alloc_one() {
|
|
allocated.push(s);
|
|
}
|
|
let freed = allocated[0];
|
|
pb.free_one(freed);
|
|
assert_eq!(pb.alloc_one(), Some(freed));
|
|
}
|
|
|
|
#[test]
|
|
fn fuzz_alloc_free_compare_with_reference_model() {
|
|
let seeds: &[u64] = &[
|
|
std::time::SystemTime::UNIX_EPOCH
|
|
.elapsed()
|
|
.unwrap_or_default()
|
|
.as_secs(),
|
|
1234567890,
|
|
0x69420,
|
|
94822,
|
|
165029,
|
|
];
|
|
for &seed in seeds {
|
|
let mut rng = StdRng::seed_from_u64(seed);
|
|
let n_slots = rng.random_range(1..10) * 64;
|
|
let mut pb = SlotBitmap::new(n_slots);
|
|
let mut model = vec![true; n_slots as usize];
|
|
|
|
for _ in 0..2000usize {
|
|
match rng.random_range(0..100) {
|
|
0..=59 => {
|
|
let got = pb.alloc_one();
|
|
if let Some(i) = got {
|
|
assert!(i < n_slots, "index in range");
|
|
assert!(model[i as usize], "bit must be free");
|
|
model[i as usize] = false;
|
|
} else {
|
|
assert!(
|
|
!model.iter().any(|&b| b),
|
|
"allocator returned None but a free slot exists"
|
|
);
|
|
}
|
|
}
|
|
_ => {
|
|
// Free a random allocated slot (if any).
|
|
let idx = rng.random_range(0..model.len());
|
|
if !model[idx] {
|
|
pb.free_one(idx as u32);
|
|
model[idx] = true;
|
|
}
|
|
}
|
|
}
|
|
assert_equivalent(&pb, &model);
|
|
}
|
|
}
|
|
}
|
|
}
|