mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-26 04:24:21 +01:00
Fix and change bitmap, apply suggestions and add some optimizations
This commit is contained in:
@@ -5,7 +5,7 @@ use crate::turso_assert;
|
||||
pub(super) struct PageBitmap {
|
||||
/// 1 = free, 0 = allocated
|
||||
words: Box<[u64]>,
|
||||
/// current number of available pages in the arena
|
||||
/// total capacity of pages in the arena
|
||||
n_pages: u32,
|
||||
/// where single allocations search downward from
|
||||
scan_one_high: u32,
|
||||
@@ -35,6 +35,22 @@ pub(super) struct PageBitmap {
|
||||
/// After freeing pages, we update hints to encourage reuse:
|
||||
/// - If freed pages are below `scan_run_low`, we move it down to include them
|
||||
/// - If freed pages are above `scan_one_high`, we move it up to include them
|
||||
///
|
||||
///```ignore
|
||||
/// let mut bitmap = PageBitmap::new(128);
|
||||
///
|
||||
/// // Single allocations come from high end
|
||||
/// assert_eq!(bitmap.alloc_one(), Some(127));
|
||||
/// assert_eq!(bitmap.alloc_one(), Some(126));
|
||||
///
|
||||
/// // Run allocations come from low end
|
||||
/// assert_eq!(bitmap.alloc_run(10), Some(0));
|
||||
/// assert_eq!(bitmap.alloc_run(10), Some(10));
|
||||
///
|
||||
/// // Free and reallocate
|
||||
/// bitmap.free_run(5, 3);
|
||||
/// assert_eq!(bitmap.alloc_run(3), Some(5)); // Reuses freed space
|
||||
/// ```
|
||||
impl PageBitmap {
|
||||
/// 64 bits per word, so shift by 6 to get page index
|
||||
const WORD_SHIFT: u32 = 6;
|
||||
@@ -43,6 +59,9 @@ impl PageBitmap {
|
||||
const ALL_FREE: u64 = u64::MAX;
|
||||
const ALL_ALLOCATED: u64 = 0u64;
|
||||
|
||||
const ALLOC: bool = false;
|
||||
const FREE: bool = true;
|
||||
|
||||
/// Creates a new `PageBitmap` capable of tracking `n_pages` pages.
|
||||
///
|
||||
/// If `n_pages` is not a multiple of 64, the trailing bits in the last
|
||||
@@ -51,37 +70,34 @@ impl PageBitmap {
|
||||
turso_assert!(n_pages > 0, "PageBitmap must have at least one page");
|
||||
let n_words = n_pages.div_ceil(Self::WORD_BITS) as usize;
|
||||
let mut words = vec![Self::ALL_FREE; n_words].into_boxed_slice();
|
||||
// Mask out bits beyond n_pages as allocated (=0)
|
||||
if let Some(last_word_mask) = Self::last_word_mask(n_pages) {
|
||||
words[n_words - 1] &= last_word_mask;
|
||||
|
||||
let trailing_bits = n_pages % Self::WORD_BITS;
|
||||
if trailing_bits != 0 {
|
||||
words[n_words - 1] &= (1u64 << trailing_bits) - 1;
|
||||
}
|
||||
Self {
|
||||
words,
|
||||
n_pages,
|
||||
scan_run_low: 0,
|
||||
scan_one_high: n_pages.saturating_sub(1),
|
||||
scan_one_high: n_pages - 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Convert word index and bit offset to page index
|
||||
/// Example:
|
||||
/// word_idx: 1, bit: 10
|
||||
/// shift the word index by WORD_SHIFT and OR with `bit` to get the page
|
||||
/// Page number: 1 << 6 | 10 = page index: 74
|
||||
const fn word_and_bit_to_page(word_idx: usize, bit: u32) -> u32 {
|
||||
(word_idx as u32) << Self::WORD_SHIFT | bit
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Get mask for valid bits in the last word
|
||||
const fn last_word_mask(n_pages: u32) -> Option<u64> {
|
||||
let valid_bits = (n_pages as usize) & (Self::WORD_MASK as usize);
|
||||
if valid_bits != 0 {
|
||||
Some((1u64 << valid_bits) - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Convert page index to word index and bit offset
|
||||
/// Example
|
||||
/// Page number: 74
|
||||
/// (74 >> 6, 74 & 63) = (1, 10)
|
||||
const fn page_to_word_and_bit(page_idx: u32) -> (usize, u32) {
|
||||
(
|
||||
(page_idx >> Self::WORD_SHIFT) as usize,
|
||||
@@ -89,198 +105,277 @@ impl PageBitmap {
|
||||
)
|
||||
}
|
||||
|
||||
/// Mask to ignore bits below `bit` inclusive for upward scans.
|
||||
#[inline]
|
||||
const fn mask_from(bit: u32) -> u64 {
|
||||
u64::MAX << bit
|
||||
}
|
||||
|
||||
/// Mask to ignore bits above `bit` for downward scans (keep [0..=bit]).
|
||||
#[inline]
|
||||
const fn mask_through(bit: u32) -> u64 {
|
||||
if bit == 63 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << (bit + 1)) - 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Count consecutive forward free bits starting at `bit` in `word`.
|
||||
/// Returns number of 1s (0..=64-pos).
|
||||
#[inline]
|
||||
const fn run_len_from(word: u64, bit: u32) -> u32 {
|
||||
// Shift so `bit` becomes LSB, then count trailing ones.
|
||||
// trailing ones = trailing_zeros of the inverted value.
|
||||
(!(word >> bit)).trailing_zeros()
|
||||
}
|
||||
|
||||
/// Allocates a single free page from the bitmap.
|
||||
///
|
||||
/// This method scans from high to low addresses to preserve contiguous
|
||||
/// runs of free pages at the low end of the bitmap.
|
||||
pub fn alloc_one(&mut self) -> Option<u32> {
|
||||
if self.n_pages == 0 {
|
||||
return None;
|
||||
}
|
||||
// Try to find the highest free bit at or below scan_one_high
|
||||
let mut search_from = self.scan_one_high;
|
||||
loop {
|
||||
if let Some(page_idx) = self.find_highest_free_at_or_below(search_from) {
|
||||
let (word_idx, bit) = Self::page_to_word_and_bit(page_idx);
|
||||
for start in [self.scan_one_high, self.n_pages - 1] {
|
||||
let (mut word_idx, bit) = Self::page_to_word_and_bit(start);
|
||||
let mut word = self.words[word_idx] & Self::mask_through(bit);
|
||||
if word != Self::ALL_ALLOCATED {
|
||||
// Fast path: pick highest set bit in this masked word
|
||||
let bit = 63 - word.leading_zeros();
|
||||
self.words[word_idx] &= !(1u64 << bit);
|
||||
self.scan_one_high = page_idx.saturating_sub(1);
|
||||
return Some(page_idx);
|
||||
let page = Self::word_and_bit_to_page(word_idx, bit);
|
||||
self.scan_one_high = page.saturating_sub(1);
|
||||
return Some(page);
|
||||
}
|
||||
|
||||
// If nothing found below hint and we haven't searched from the top yet
|
||||
if search_from < self.n_pages - 1 {
|
||||
search_from = self.n_pages - 1;
|
||||
} else {
|
||||
// Walk lower words
|
||||
while word_idx > 0 {
|
||||
word_idx -= 1;
|
||||
word = self.words[word_idx];
|
||||
if word != Self::ALL_ALLOCATED {
|
||||
let bits = 63 - word.leading_zeros();
|
||||
self.words[word_idx] &= !(1u64 << bits);
|
||||
let page = Self::word_and_bit_to_page(word_idx, bits);
|
||||
self.scan_one_high = page.saturating_sub(1);
|
||||
return Some(page);
|
||||
}
|
||||
}
|
||||
if self.scan_one_high == self.n_pages - 1 {
|
||||
// dont try again if we already started there
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the highest free bit at or below `max_idx`
|
||||
fn find_highest_free_at_or_below(&self, max_idx: u32) -> Option<u32> {
|
||||
let (max_word, max_bit) = Self::page_to_word_and_bit(max_idx);
|
||||
// Check the word containing max_idx with appropriate mask
|
||||
let word = self.words[max_word];
|
||||
if word != Self::ALL_ALLOCATED {
|
||||
// Create mask for bits 0..=max_bit
|
||||
let mask = if max_bit == 63 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << (max_bit + 1)) - 1
|
||||
};
|
||||
let masked = word & mask;
|
||||
if masked != 0 {
|
||||
let bit = 63 - masked.leading_zeros();
|
||||
return Some(Self::word_and_bit_to_page(max_word, bit));
|
||||
}
|
||||
}
|
||||
// Check all words below max_word
|
||||
for word_idx in (0..max_word).rev() {
|
||||
if self.words[word_idx] != Self::ALL_ALLOCATED {
|
||||
let bit = 63 - self.words[word_idx].leading_zeros();
|
||||
return Some(Self::word_and_bit_to_page(word_idx, bit));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Allocates a contiguous run of `need` pages from the bitmap.
|
||||
/// This method scans from low to high addresses, starting from the
|
||||
/// `scan_run_low` pointer.
|
||||
/// This method scans from low to high addresses, starting from `scan_run_low`,
|
||||
/// next allocation continues from where we left off by updating the hint.
|
||||
pub fn alloc_run(&mut self, need: u32) -> Option<u32> {
|
||||
if need == 1 {
|
||||
return self.alloc_one();
|
||||
}
|
||||
if need == 0 || need > self.n_pages {
|
||||
return None;
|
||||
}
|
||||
// Two-pass search with scan_hint optimization
|
||||
let mut search_start = self.scan_run_low;
|
||||
for pass in 0..2 {
|
||||
if pass == 1 {
|
||||
search_start = 0;
|
||||
}
|
||||
if let Some(found) = self.find_free_run(search_start, need) {
|
||||
self.mark_run(found, need, false);
|
||||
if need == 1 {
|
||||
return self.alloc_one();
|
||||
}
|
||||
// Try from hint first, then from start if different
|
||||
for &start_pos in &[self.scan_run_low, 0] {
|
||||
if let Some(found) = self.find_free_run_up(start_pos, need) {
|
||||
self.mark_run(found, need, Self::ALLOC);
|
||||
self.scan_run_low = found + need;
|
||||
// Update single-page hint if this run extends beyond it
|
||||
let last_page = found + need - 1;
|
||||
if last_page > self.scan_one_high {
|
||||
self.scan_one_high = last_page.min(self.n_pages - 1);
|
||||
}
|
||||
return Some(found);
|
||||
}
|
||||
if search_start == 0 {
|
||||
// Already searched from beginning
|
||||
// Don't search from 0 if we already started there
|
||||
if start_pos == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Search for a free run of `need` pages beginning from `start`
|
||||
fn find_free_run(&self, start: u32, need: u32) -> Option<u32> {
|
||||
let mut pos = start;
|
||||
let limit = self.n_pages.saturating_sub(need - 1);
|
||||
#[inline]
|
||||
fn masked_word(&self, idx: usize) -> u64 {
|
||||
let word = self.words[idx];
|
||||
if idx + 1 != self.words.len() {
|
||||
// fast path, if it's not the last word we can use it as-is
|
||||
return word;
|
||||
}
|
||||
// otherwise keep invalid tail bits treated as allocated
|
||||
let trailing = self.n_pages % Self::WORD_BITS;
|
||||
if trailing == 0 {
|
||||
word
|
||||
} else {
|
||||
word & ((1u64 << trailing) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_full_free(&self, idx: usize) -> bool {
|
||||
if idx + 1 != self.words.len() {
|
||||
return self.words[idx] == u64::MAX;
|
||||
}
|
||||
|
||||
// handle the case where the last word has some bits that we dont include
|
||||
let trailing = self.n_pages % Self::WORD_BITS;
|
||||
if trailing == 0 {
|
||||
self.words[idx] == u64::MAX
|
||||
} else {
|
||||
self.masked_word(idx) == ((1u64 << trailing) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an unallocated sequence of `need` pages, scanning *upward* from `start`
|
||||
///
|
||||
/// Overview:
|
||||
/// Set limit = n_pages - (need - 1): as the most pages we could iterate through
|
||||
/// and iterate `pos` while `pos < limit`.
|
||||
///
|
||||
/// Split pos into (word_idx, bit_offset) and compute to keep bits at/after `bit_offset`.
|
||||
///
|
||||
/// If zero: no free bit in this word at or after `pos`, so we jump to next word boundary:
|
||||
/// pos = (word_idx + 1) << WORD_SHIFT and continue.
|
||||
///
|
||||
/// Otherwise, locate the first free bit at or after `pos`, and measure the free span in the
|
||||
/// word starting at `first`
|
||||
///
|
||||
/// If `free_in_cur >= need`: the entire run fits in this word, and we return `run_start`.
|
||||
/// If `first + free_in_cur < 64`: the free span ends before the word boundary, the run cannot
|
||||
/// bridge into the next word so we advance to the next.
|
||||
///
|
||||
/// If the span reaches the boundary exactly, we try to extend across words:
|
||||
/// consume subsequent words while `is_full_free(word)` in 64-page chunks and return if `need` is
|
||||
/// satisfied.
|
||||
///
|
||||
/// If still short, then check the tail in the next (partial) word and if tail >= remaining, return.
|
||||
/// otherwise advance `pos = run_start + pages_found + 1`.
|
||||
fn find_free_run_up(&self, start: u32, need: u32) -> Option<u32> {
|
||||
let limit = self.n_pages.saturating_sub(need - 1);
|
||||
let mut pos = start;
|
||||
while pos < limit {
|
||||
if let Some(next_free) = self.next_free_bit_from(pos) {
|
||||
if next_free + need > self.n_pages {
|
||||
break;
|
||||
}
|
||||
if self.check_run_free(next_free, need) {
|
||||
return Some(next_free);
|
||||
}
|
||||
pos = next_free + 1;
|
||||
} else {
|
||||
break;
|
||||
let (word_idx, bit_offset) = Self::page_to_word_and_bit(pos);
|
||||
|
||||
// Current word from bit_offset onward
|
||||
let current_word = self.masked_word(word_idx) & Self::mask_from(bit_offset);
|
||||
if current_word == 0 {
|
||||
// Jump to next word boundary
|
||||
pos = ((word_idx + 1) as u32) << Self::WORD_SHIFT;
|
||||
continue;
|
||||
}
|
||||
|
||||
// First free bit >= pos
|
||||
let first_free_bit = current_word.trailing_zeros();
|
||||
let run_start = ((word_idx as u32) << Self::WORD_SHIFT) + first_free_bit;
|
||||
|
||||
// Free span within this word
|
||||
let free_in_cur = Self::run_len_from(self.masked_word(word_idx), first_free_bit);
|
||||
if free_in_cur >= need {
|
||||
return Some(run_start);
|
||||
}
|
||||
|
||||
// If we didn't reach the word boundary, the run is broken here.
|
||||
if first_free_bit + free_in_cur < Self::WORD_BITS {
|
||||
pos = run_start + free_in_cur + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We exactly reached the boundary, extend across whole free words
|
||||
let mut pages_found = free_in_cur;
|
||||
let mut wi = word_idx + 1;
|
||||
|
||||
while pages_found + Self::WORD_BITS <= need
|
||||
&& wi < self.words.len()
|
||||
&& self.is_full_free(wi)
|
||||
{
|
||||
pages_found += Self::WORD_BITS;
|
||||
wi += 1;
|
||||
}
|
||||
if pages_found >= need {
|
||||
return Some(run_start);
|
||||
}
|
||||
|
||||
// Tail in the next partial word
|
||||
if wi < self.words.len() {
|
||||
let remaining = need - pages_found;
|
||||
let w = self.masked_word(wi);
|
||||
let tail = (!w).trailing_zeros();
|
||||
if tail >= remaining {
|
||||
return Some(run_start);
|
||||
}
|
||||
// Run broken in this word: skip past the failure point to avoid checking again
|
||||
pos = run_start + pages_found + 1;
|
||||
continue;
|
||||
}
|
||||
// No space
|
||||
return None;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Frees a contiguous run of pages, marking them as available for reallocation
|
||||
/// and update the scan hints to potentially reuse the freed space in future allocations.
|
||||
/// and update the scan hints to allocations.
|
||||
pub fn free_run(&mut self, start: u32, count: u32) {
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
turso_assert!(start + count <= self.n_pages, "free_run out of bounds");
|
||||
self.mark_run(start, count, true);
|
||||
// Update scan hint to potentially reuse this space
|
||||
self.mark_run(start, count, Self::FREE);
|
||||
// Update hints based on what we're freeing
|
||||
|
||||
// if this was a single page and higher than current hint, bump the high hint up
|
||||
if count == 1 && start > self.scan_one_high {
|
||||
self.scan_one_high = start;
|
||||
return;
|
||||
}
|
||||
|
||||
if start < self.scan_run_low {
|
||||
self.scan_run_low = start;
|
||||
}
|
||||
if start > self.scan_one_high {
|
||||
// If we freed a run beyond the current scan hint, adjust it
|
||||
let end = start.saturating_add(count).min(self.n_pages);
|
||||
self.scan_one_high = end.saturating_sub(1);
|
||||
// Also update scan_one_high hint if the run extends beyond it
|
||||
let last_page = (start + count - 1).min(self.n_pages - 1);
|
||||
if last_page > self.scan_one_high {
|
||||
self.scan_one_high = last_page;
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether a contiguous run of pages is completely free.
|
||||
/// # Example
|
||||
/// Checking pages 125-134 (10 pages starting at 125)
|
||||
/// This spans from word 1 (bit 61) to word 2 (bit 6)
|
||||
///
|
||||
/// Word 1: ...11111111_11110000 (bits 60-63 must be checked)
|
||||
/// Word 2: 00000000_01111111... (bits 0-6 must be checked)
|
||||
pub fn check_run_free(&self, start: u32, len: u32) -> bool {
|
||||
if start.saturating_add(len) > self.n_pages {
|
||||
if start + len > self.n_pages {
|
||||
return false;
|
||||
}
|
||||
self.inspect_run(start, len, |word, mask| {
|
||||
// Optimize for full-word checks
|
||||
if mask == Self::ALL_FREE {
|
||||
word == Self::ALL_FREE
|
||||
} else {
|
||||
(word & mask) == mask
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Marks a contiguous run of pages as either free or allocated.
|
||||
pub fn mark_run(&mut self, start: u32, len: u32, free: bool) {
|
||||
turso_assert!(start + len <= self.n_pages, "mark_run out of bounds");
|
||||
|
||||
let (mut word_idx, bit_offset) = Self::page_to_word_and_bit(start);
|
||||
let mut remaining = len as usize;
|
||||
let mut pos_in_word = bit_offset as usize;
|
||||
|
||||
// process words until we have checked `len` bits
|
||||
while remaining > 0 {
|
||||
let bits_in_word = (Self::WORD_BITS as usize).saturating_sub(pos_in_word);
|
||||
let bits_to_process = remaining.min(bits_in_word);
|
||||
// Calculate how many bits to check in the current word
|
||||
// This is either the remaining bits needed, or the bits left in the current word
|
||||
// Example: if pos_in_word = 61 and remaining = 10, we can only check 3 bits in this word
|
||||
let bits_to_process = remaining.min((Self::WORD_BITS as usize) - pos_in_word);
|
||||
|
||||
// Create a mask with 1s in the positions we want to check
|
||||
let mask = if bits_to_process == Self::WORD_BITS as usize {
|
||||
Self::ALL_FREE
|
||||
} else {
|
||||
(1u64 << bits_to_process).saturating_sub(1) << pos_in_word
|
||||
// Create mask with `bits_to_process` 1's, shifted to start at `pos_in_word`
|
||||
// Example: bits_to_process = 3, pos_in_word = 61
|
||||
// (1 << 3) - 1 = 0b111
|
||||
// 0b111 << 61 = puts three 1s at positions 61, 62, 63
|
||||
((1u64 << bits_to_process) - 1) << pos_in_word
|
||||
};
|
||||
if free {
|
||||
self.words[word_idx] |= mask;
|
||||
} else {
|
||||
self.words[word_idx] &= !mask;
|
||||
}
|
||||
|
||||
remaining -= bits_to_process;
|
||||
// move to the next word/reset position
|
||||
word_idx += 1;
|
||||
pos_in_word = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a run of bits with a read-only operation
|
||||
fn inspect_run<F>(&self, start: u32, len: u32, mut check: F) -> bool
|
||||
where
|
||||
F: FnMut(u64, u64) -> bool,
|
||||
{
|
||||
let (mut word_idx, bit_offset) = Self::page_to_word_and_bit(start);
|
||||
let mut remaining = len as usize;
|
||||
let mut pos_in_word = bit_offset as usize;
|
||||
|
||||
while remaining > 0 {
|
||||
let bits_in_word = (Self::WORD_BITS as usize).saturating_sub(pos_in_word);
|
||||
let bits_to_process = remaining.min(bits_in_word);
|
||||
|
||||
let mask = if bits_to_process == Self::WORD_BITS as usize {
|
||||
Self::ALL_FREE
|
||||
} else {
|
||||
(1u64 << bits_to_process).saturating_sub(1) << pos_in_word
|
||||
};
|
||||
|
||||
if !check(self.words[word_idx], mask) {
|
||||
// If all bits under the mask are 1 (free), then word & mask == mask
|
||||
if (self.words[word_idx] & mask) != mask {
|
||||
return false;
|
||||
}
|
||||
|
||||
remaining -= bits_to_process;
|
||||
word_idx += 1;
|
||||
pos_in_word = 0;
|
||||
@@ -288,35 +383,36 @@ impl PageBitmap {
|
||||
true
|
||||
}
|
||||
|
||||
/// Try to find next free bit (1) at or after `from` page index.
|
||||
pub fn next_free_bit_from(&self, from: u32) -> Option<u32> {
|
||||
if from >= self.n_pages {
|
||||
return None;
|
||||
}
|
||||
let (mut word_idx, bit_offset) = Self::page_to_word_and_bit(from);
|
||||
/// Marks a contiguous run of pages as either free or allocated.
|
||||
pub fn mark_run(&mut self, start: u32, len: u32, free: bool) {
|
||||
turso_assert!(start + len <= self.n_pages, "mark_run out of bounds");
|
||||
|
||||
// Check current word from bit_offset onward
|
||||
let mask = u64::MAX << bit_offset;
|
||||
let current = self.words[word_idx] & mask;
|
||||
if current != 0 {
|
||||
let bit = current.trailing_zeros();
|
||||
return Some(Self::word_and_bit_to_page(word_idx, bit));
|
||||
}
|
||||
// Check remaining words
|
||||
word_idx += 1;
|
||||
while word_idx < self.words.len() {
|
||||
if self.words[word_idx] != Self::ALL_ALLOCATED {
|
||||
let bit = self.words[word_idx].trailing_zeros();
|
||||
let page_idx = Self::word_and_bit_to_page(word_idx, bit);
|
||||
// Ensure we don't return a page beyond n_pages
|
||||
if page_idx < self.n_pages {
|
||||
return Some(page_idx);
|
||||
}
|
||||
break;
|
||||
let (mut word_idx, mut bit_offset) = Self::page_to_word_and_bit(start);
|
||||
let mut remaining = len as usize;
|
||||
while remaining > 0 {
|
||||
let bits = (Self::WORD_BITS as usize) - bit_offset as usize;
|
||||
let take = remaining.min(bits);
|
||||
let mask = if take == 64 {
|
||||
Self::ALL_FREE
|
||||
} else {
|
||||
((1u64 << take) - 1) << bit_offset
|
||||
};
|
||||
if free {
|
||||
self.words[word_idx] |= mask;
|
||||
} else {
|
||||
self.words[word_idx] &= !mask;
|
||||
}
|
||||
remaining -= take;
|
||||
word_idx += 1;
|
||||
bit_offset = 0;
|
||||
}
|
||||
None
|
||||
// keep last-word tail clamped to the proper amount of pages
|
||||
let last = self.words.len() - 1;
|
||||
self.words[last] &= if (self.n_pages % Self::WORD_BITS) == 0 {
|
||||
u64::MAX
|
||||
} else {
|
||||
(1u64 << (self.n_pages % Self::WORD_BITS)) - 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,16 +440,6 @@ pub mod tests {
|
||||
model.iter().filter(|&&b| b).count()
|
||||
}
|
||||
|
||||
/// Find the first free index >= from in the reference model.
|
||||
fn ref_first_free_from(model: &[bool], from: u32) -> Option<u32> {
|
||||
let from = from as usize;
|
||||
model
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(from)
|
||||
.find_map(|(i, &f)| if f { Some(i as u32) } else { None })
|
||||
}
|
||||
|
||||
/// Check whether [start, start+len) are all free in the reference model.
|
||||
fn ref_check_run_free(model: &[bool], start: u32, len: u32) -> bool {
|
||||
let s = start as usize;
|
||||
@@ -381,16 +467,12 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn new_masks_trailing_bits() {
|
||||
// test weird page counts
|
||||
for n_pages in [1, 63, 64, 65, 127, 128, 129, 255, 256, 1023] {
|
||||
let pb = PageBitmap::new(n_pages);
|
||||
// All valid pages must be free.
|
||||
let free = pb_free_vec(&pb);
|
||||
assert_eq!(free.len(), n_pages as usize);
|
||||
assert!(free.iter().all(|&b| b), "all pages should start free");
|
||||
|
||||
// Bits beyond n_pages must be treated as allocated (masked out).
|
||||
// We check the last word explicitly.
|
||||
if n_pages > 0 {
|
||||
let words_len = pb.words.len();
|
||||
let valid_bits = (n_pages as usize) & 63;
|
||||
@@ -424,7 +506,6 @@ pub mod tests {
|
||||
let mut pb = PageBitmap::new(200);
|
||||
let mut model = vec![true; 200];
|
||||
|
||||
// Allocate a run of 70 crossing word boundaries
|
||||
let need = 70;
|
||||
let start = pb.alloc_run(need).expect("expected a run");
|
||||
assert!(ref_check_run_free(&model, start, need));
|
||||
@@ -432,7 +513,6 @@ pub mod tests {
|
||||
assert!(!pb.check_run_free(start, need));
|
||||
assert_equivalent(&pb, &model);
|
||||
|
||||
// Free it and verify again
|
||||
pb.free_run(start, need);
|
||||
ref_mark_run(&mut model, start, need, true);
|
||||
assert!(pb.check_run_free(start, need));
|
||||
@@ -450,14 +530,14 @@ pub mod tests {
|
||||
let mut model = vec![false; n as usize];
|
||||
assert_equivalent(&pb, &model);
|
||||
|
||||
// heavily fragment, free exactly every other page to create isolated 1-page holes
|
||||
// Fragment: free exactly every other page (isolated 1-page holes)
|
||||
for i in (0..n).step_by(2) {
|
||||
pb.free_run(i, 1);
|
||||
model[i as usize] = true;
|
||||
}
|
||||
assert_equivalent(&pb, &model);
|
||||
|
||||
// no run of 2 should exist
|
||||
// No run of 2 should exist
|
||||
assert!(
|
||||
pb.alloc_run(2).is_none(),
|
||||
"no free run of length 2 should exist"
|
||||
@@ -465,46 +545,17 @@ pub mod tests {
|
||||
assert_equivalent(&pb, &model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_free_bit_from_matches_reference() {
|
||||
let mut pb = PageBitmap::new(130);
|
||||
let mut model = vec![true; 130];
|
||||
|
||||
// Allocate a few arbitrary runs/singles
|
||||
let r1 = pb.alloc_run(10).unwrap();
|
||||
ref_mark_run(&mut model, r1, 10, false);
|
||||
|
||||
let r2 = pb.alloc_run(1).unwrap();
|
||||
model[r2 as usize] = false;
|
||||
|
||||
let r3 = pb.alloc_run(50).unwrap();
|
||||
ref_mark_run(&mut model, r3, 50, false);
|
||||
|
||||
// Check random 'from' points
|
||||
for from in [0, 1, 5, 9, 10, 59, 60, 61, 64, 100, 129] {
|
||||
let expect = ref_first_free_from(&model, from);
|
||||
let got = pb.next_free_bit_from(from);
|
||||
assert_eq!(got, expect, "from={from}");
|
||||
}
|
||||
assert_equivalent(&pb, &model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singles_from_tail_preserve_front_run() {
|
||||
// This asserts the desirable policy, single-page allocations should
|
||||
// not destroy large runs at the front
|
||||
let mut pb = PageBitmap::new(512);
|
||||
let mut model = vec![true; 512];
|
||||
|
||||
// Take 100 single pages, model updates at returned indices
|
||||
for _ in 0..100 {
|
||||
let idx = pb.alloc_one().unwrap();
|
||||
model[idx as usize] = false;
|
||||
}
|
||||
assert_equivalent(&pb, &model);
|
||||
|
||||
// There should still be a long free run near the beginning.
|
||||
// Request 64, it should succeed
|
||||
let r = pb.alloc_run(64).expect("64-run should still be available");
|
||||
assert!(ref_check_run_free(&model, r, 64));
|
||||
ref_mark_run(&mut model, r, 64, false);
|
||||
@@ -525,7 +576,6 @@ pub mod tests {
|
||||
];
|
||||
for &seed in seeds {
|
||||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
// random size including tricky boundaries
|
||||
let n_pages = match rng.gen_range(0..6) {
|
||||
0 => 777,
|
||||
1 => 1,
|
||||
@@ -552,7 +602,6 @@ pub mod tests {
|
||||
model[i as usize] = false;
|
||||
assert_eq!(ref_count_free(&model), before_free - 1);
|
||||
} else {
|
||||
// Then model must have no free bits
|
||||
assert_eq!(before_free, 0, "no free bits if None returned");
|
||||
}
|
||||
}
|
||||
@@ -563,15 +612,11 @@ pub mod tests {
|
||||
let got = pb.alloc_run(need);
|
||||
if let Some(start) = got {
|
||||
assert!(start + need <= n_pages, "within bounds");
|
||||
assert!(
|
||||
ref_check_run_free(&model, start, need),
|
||||
"run must be free in model"
|
||||
);
|
||||
assert!(ref_check_run_free(&model, start, need), "run must be free");
|
||||
ref_mark_run(&mut model, start, need, false);
|
||||
} else {
|
||||
// If None, assert there is no free run of 'need' in the model.
|
||||
let mut exists = false;
|
||||
for s in 0..=n_pages.saturating_sub(need) {
|
||||
for s in 0..=(n_pages.saturating_sub(need)) {
|
||||
if ref_check_run_free(&model, s, need) {
|
||||
exists = true;
|
||||
break;
|
||||
@@ -594,14 +639,8 @@ pub mod tests {
|
||||
ref_mark_run(&mut model, start, len, true);
|
||||
}
|
||||
}
|
||||
// Occasionally check next_free_bit_from correctness against model
|
||||
if rng.gen_bool(0.2) {
|
||||
let from = rng.gen_range(0..n_pages);
|
||||
let got = pb.next_free_bit_from(from);
|
||||
let expect = ref_first_free_from(&model, from);
|
||||
assert_eq!(got, expect, "next_free_bit_from(from={from})");
|
||||
}
|
||||
// Keep both representations in sync
|
||||
|
||||
// Always keep representations in sync
|
||||
assert_equivalent(&pb, &model);
|
||||
}
|
||||
}
|
||||
@@ -610,49 +649,43 @@ pub mod tests {
|
||||
#[test]
|
||||
fn test_run_crossing_word_boundaries_and_edge_cases() {
|
||||
let mut pb = PageBitmap::new(256);
|
||||
// Test runs that cross word boundaries at various positions
|
||||
let test_cases = [
|
||||
(60, 8), // Crosses from word 0 to word 1
|
||||
(62, 4), // Crosses at the 64-bit boundary
|
||||
(120, 16), // Crosses from word 1 to word 2
|
||||
(0, 128), // Spans exactly 2 words
|
||||
(32, 64), // Aligned start, crosses one boundary
|
||||
];
|
||||
let cases = [(60, 8), (62, 4), (120, 16), (0, 128), (32, 64)];
|
||||
|
||||
for (start, len) in test_cases {
|
||||
// Ensure it's free
|
||||
assert!(
|
||||
pb.check_run_free(start, len),
|
||||
"Run at {start} len {len} should be free",
|
||||
);
|
||||
for (start, len) in cases {
|
||||
assert!(pb.check_run_free(start, len));
|
||||
pb.mark_run(start, len, false);
|
||||
assert!(
|
||||
!pb.check_run_free(start, len),
|
||||
"Run at {start} len {len} should be allocated",
|
||||
);
|
||||
assert!(!pb.check_run_free(start, len));
|
||||
pb.mark_run(start, len, true);
|
||||
assert!(
|
||||
pb.check_run_free(start, len),
|
||||
"Run at {start} len {len} should be free again",
|
||||
);
|
||||
assert!(pb.check_run_free(start, len));
|
||||
}
|
||||
|
||||
let mut pb = PageBitmap::new(100);
|
||||
// Test allocating runs at the exact end
|
||||
assert_eq!(pb.alloc_run(100), Some(0));
|
||||
pb.free_run(0, 100);
|
||||
// Test run that would exceed n_pages
|
||||
assert_eq!(pb.alloc_run(101), None);
|
||||
// Fragment the bitmap in a specific pattern
|
||||
// Free: 0-9, Allocated: 10-19, Free: 20-29, etc.
|
||||
|
||||
for i in (10..100).step_by(20) {
|
||||
pb.mark_run(i, 10, false);
|
||||
}
|
||||
// Should be able to allocate 10-page runs
|
||||
assert_eq!(pb.alloc_run(10), Some(0));
|
||||
assert_eq!(pb.alloc_run(10), Some(20));
|
||||
assert_eq!(pb.alloc_run(10), Some(40));
|
||||
// But not 11-page runs
|
||||
assert_eq!(pb.alloc_run(11), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reused_hints() {
|
||||
let mut bitmap = PageBitmap::new(128);
|
||||
// Single allocations come from high end
|
||||
assert_eq!(bitmap.alloc_one(), Some(127));
|
||||
assert_eq!(bitmap.alloc_one(), Some(126));
|
||||
|
||||
// Run allocations come from low end
|
||||
assert_eq!(bitmap.alloc_run(10), Some(0));
|
||||
assert_eq!(bitmap.alloc_run(10), Some(10));
|
||||
|
||||
// Free and reallocate
|
||||
bitmap.free_run(5, 3);
|
||||
assert_eq!(bitmap.alloc_run(3), Some(5)); // Reuses freed space
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user