diff --git a/core/storage/page_bitmap.rs b/core/storage/page_bitmap.rs index f657ad2b6..354b82997 100644 --- a/core/storage/page_bitmap.rs +++ b/core/storage/page_bitmap.rs @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { + 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(&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 { - 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 { - 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 + } }