1use crate::config::{SQLITE_PAGE_SIZE, STABLE_PAGE_SIZE, SUPERBLOCK_SIZE};
7use crate::sqlite_vfs::overlay::{self, Overlay};
8#[cfg(test)]
9use crate::stable::memory::ContextId;
10use crate::stable::memory::{self, StableMemoryError};
11use crate::stable::meta::{
12 fnv1a64, Superblock, ZeroExtent, CURRENT_LAYOUT_VERSION, FLAG_CHECKSUM_REFRESHING,
13 FLAG_CHECKSUM_STALE, FLAG_IMPORTING, MAX_ZERO_EXTENTS,
14};
15#[cfg(test)]
16use std::cell::RefCell;
17#[cfg(test)]
18use std::collections::BTreeMap;
19
20const CHECKSUM_CHUNK_LEN: u64 = 16 * 1024;
21#[cfg(test)]
22const FAR_PAGE_NO: u64 = 257;
23const FILE_PAGE_OFFSET_CACHE_CAPACITY: usize = 64;
24const FILE_PAGE_DATA_CACHE_CAPACITY: usize = 8;
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct ChecksumRefresh {
28 pub complete: bool,
29 pub checksum: u64,
30 pub scanned_bytes: u64,
31 pub db_size: u64,
32}
33
34#[allow(dead_code)]
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct StorageStats {
37 pub layout_version: u64,
38 pub page_count: u64,
39 pub page_table_bytes: u64,
40 pub zero_extent_count: u64,
41 pub active_bytes: u64,
42 pub allocated_bytes: u64,
43 pub orphan_bytes_estimate: u64,
44 pub orphan_ratio_basis_points: u64,
45 pub compact_recommended: bool,
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub(crate) enum StableBlobFailpoint {
50 OverlayWrite,
51 OverlayTruncate,
52 CommitCapacity,
53 CommitChunkWrite,
54 CommitSuperblockStore,
55}
56
57thread_local! {
58 #[cfg(test)]
59 static FAILPOINTS: RefCell<BTreeMap<ContextId, StableBlobFailpoint>> = const { RefCell::new(BTreeMap::new()) };
60}
61
62#[derive(Debug)]
63pub(crate) struct PageOffsetCache {
64 entries: Vec<(u64, u64)>,
65 pages: Vec<(u64, Vec<u8>)>,
66}
67
68impl PageOffsetCache {
69 pub(crate) fn new() -> Self {
70 Self {
71 entries: Vec::with_capacity(FILE_PAGE_OFFSET_CACHE_CAPACITY),
72 pages: Vec::new(),
73 }
74 }
75
76 fn get(&self, page_no: u64) -> Option<u64> {
77 match self.entries.as_slice() {
78 [] => None,
79 [(cached_page, physical)] => (*cached_page == page_no).then_some(*physical),
80 entries => {
81 for (cached_page, physical) in entries {
82 if *cached_page == page_no {
83 return Some(*physical);
84 }
85 }
86 None
87 }
88 }
89 }
90
91 fn insert(&mut self, page_no: u64, physical: u64) {
92 if self.entries.len() == FILE_PAGE_OFFSET_CACHE_CAPACITY {
93 self.entries.remove(0);
94 }
95 self.entries.push((page_no, physical));
96 }
97
98 #[inline(always)]
99 fn copy_page_slice(&self, page_no: u64, in_page: usize, dst: &mut [u8]) -> bool {
100 if self.pages.is_empty() {
101 return false;
102 }
103 if self.pages.len() == 1 {
104 let (cached_page, page) = &self.pages[0];
105 if *cached_page == page_no {
106 let end = in_page + dst.len();
107 dst.copy_from_slice(&page[in_page..end]);
108 return true;
109 }
110 return false;
111 }
112 for (cached_page, page) in &self.pages {
113 if *cached_page == page_no {
114 let end = in_page + dst.len();
115 dst.copy_from_slice(&page[in_page..end]);
116 return true;
117 }
118 }
119 false
120 }
121
122 fn insert_page(&mut self, page_no: u64, page: Vec<u8>) {
123 if self.pages.len() == FILE_PAGE_DATA_CACHE_CAPACITY {
124 self.pages.remove(0);
125 }
126 self.pages.push((page_no, page));
127 }
128}
129
130#[cfg(test)]
131pub(crate) fn set_failpoint(failpoint: StableBlobFailpoint) {
132 if let Ok(context) = memory::active_context_id() {
133 FAILPOINTS.with(|slot| {
134 slot.borrow_mut().insert(context, failpoint);
135 });
136 }
137}
138
139#[cfg(test)]
140pub(crate) fn clear_failpoint() {
141 FAILPOINTS.with(|slot| slot.borrow_mut().clear());
142}
143
144pub(crate) fn ensure_current_layout() -> Result<(), StableMemoryError> {
145 let block = Superblock::load()?;
146 if block.layout_version == CURRENT_LAYOUT_VERSION {
147 return Ok(());
148 }
149 Err(StableMemoryError::UnsupportedLayoutVersion(
150 block.layout_version,
151 ))
152}
153
154pub(crate) fn begin_update() -> Result<u64, StableMemoryError> {
155 let block = Superblock::load()?;
156 if block.layout_version != CURRENT_LAYOUT_VERSION {
157 return Err(StableMemoryError::UnsupportedLayoutVersion(
158 block.layout_version,
159 ));
160 }
161 if block.is_importing() {
162 return Err(StableMemoryError::ImportAlreadyStarted);
163 }
164 overlay::begin(block.db_size)?;
165 Ok(block.db_size)
166}
167
168pub(crate) fn rollback_update() {
169 overlay::rollback();
170}
171
172pub(crate) fn commit_update() -> Result<(), StableMemoryError> {
173 let Some(overlay) = overlay::take() else {
174 return Ok(());
175 };
176 if overlay.is_empty() {
177 return Ok(());
178 }
179 commit_overlay(overlay, true)
180}
181
182pub(crate) fn read_at(offset: u64, dst: &mut [u8]) -> Result<bool, StableMemoryError> {
183 if let Some(result) = overlay::read_at(offset, dst) {
184 return result;
185 }
186 read_base_at(offset, dst)
187}
188
189pub(crate) fn read_base_at(offset: u64, dst: &mut [u8]) -> Result<bool, StableMemoryError> {
190 if dst.is_empty() {
191 return Ok(true);
192 }
193 let block = Superblock::load()?;
194 read_base_at_with_block(&block, offset, dst)
195}
196
197pub(crate) fn read_base_at_with_block(
198 block: &Superblock,
199 offset: u64,
200 dst: &mut [u8],
201) -> Result<bool, StableMemoryError> {
202 if dst.is_empty() {
203 return Ok(true);
204 }
205 if offset >= block.db_size {
206 dst.fill(0);
207 return Ok(false);
208 }
209 let requested = u64::try_from(dst.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
210 if requested <= block.db_size - offset {
211 read_logical_range(block, offset, dst)?;
212 return Ok(true);
213 }
214 let copied = requested.min(block.db_size - offset);
215 let copied_len = usize::try_from(copied).map_err(|_| StableMemoryError::OffsetOverflow)?;
216 read_logical_range(block, offset, &mut dst[..copied_len])?;
217 dst[copied_len..].fill(0);
218 Ok(copied == requested)
219}
220
221#[inline(always)]
222pub(crate) fn read_base_at_with_page_cache(
223 block: &Superblock,
224 offset: u64,
225 dst: &mut [u8],
226 page_offsets: &mut PageOffsetCache,
227) -> Result<bool, StableMemoryError> {
228 if dst.is_empty() {
229 return Ok(true);
230 }
231 if offset >= block.db_size {
232 dst.fill(0);
233 return Ok(false);
234 }
235 let requested = u64::try_from(dst.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
236 if requested <= block.db_size - offset {
237 read_logical_range_with_page_cache(block, offset, dst, page_offsets)?;
238 return Ok(true);
239 }
240 let copied = requested.min(block.db_size - offset);
241 let copied_len = usize::try_from(copied).map_err(|_| StableMemoryError::OffsetOverflow)?;
242 read_logical_range_with_page_cache(block, offset, &mut dst[..copied_len], page_offsets)?;
243 dst[copied_len..].fill(0);
244 Ok(copied == requested)
245}
246
247pub(crate) fn read_base_page(page_no: u64) -> Result<Vec<u8>, StableMemoryError> {
248 let block = Superblock::load()?;
249 let mut page = zero_page();
250 read_base_at_with_block(
251 &block,
252 page_no
253 .checked_mul(page_size())
254 .ok_or(StableMemoryError::OffsetOverflow)?,
255 &mut page,
256 )?;
257 Ok(page)
258}
259
260pub(crate) fn write_at(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
261 if let Some(result) = overlay::write_at(offset, bytes) {
262 hit_failpoint(StableBlobFailpoint::OverlayWrite)?;
263 return result;
264 }
265 if bytes.is_empty() {
266 return Ok(());
267 }
268 ensure_current_layout()?;
269 let mut direct = Overlay::new(Superblock::load()?.db_size);
270 direct.write_at(offset, bytes)?;
271 commit_overlay(direct, false)
272}
273
274pub(crate) fn truncate(size: u64) -> Result<(), StableMemoryError> {
275 if let Some(result) = overlay::truncate(size) {
276 hit_failpoint(StableBlobFailpoint::OverlayTruncate)?;
277 return result;
278 }
279 ensure_current_layout()?;
280 let mut direct = Overlay::new(Superblock::load()?.db_size);
281 direct.truncate(size)?;
282 if direct.is_empty() {
283 return Ok(());
284 }
285 commit_overlay(direct, false)
286}
287
288pub(crate) fn file_size() -> Result<u64, StableMemoryError> {
289 if let Some(size) = overlay::file_size() {
290 return Ok(size);
291 }
292 Ok(Superblock::load()?.db_size)
293}
294
295#[allow(dead_code)]
296pub fn export_chunk(offset: u64, len: u64) -> Result<Vec<u8>, StableMemoryError> {
297 reject_during_update()?;
298 let block = Superblock::load()?;
299 if offset >= block.db_size {
300 return Ok(Vec::new());
301 }
302 let copied = len.min(block.db_size - offset);
303 let copied_len = usize::try_from(copied).map_err(|_| StableMemoryError::OffsetOverflow)?;
304 let mut out = vec![0_u8; copied_len];
305 read_logical_range(&block, offset, &mut out)?;
306 Ok(out)
307}
308
309#[allow(dead_code)]
310pub fn import_chunk(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
311 reject_during_update()?;
312 let mut block = Superblock::load()?;
313 if !block.is_importing() {
314 return Err(StableMemoryError::ImportNotStarted);
315 }
316 let len = u64::try_from(bytes.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
317 if offset != block.import_written_until {
318 return Err(StableMemoryError::ImportOutOfOrder {
319 offset,
320 expected: block.import_written_until,
321 });
322 }
323 let end = checked_add(offset, len)?;
324 if end > block.import_total_size {
325 return Err(StableMemoryError::ImportOutOfBounds {
326 offset,
327 len,
328 db_size: block.import_total_size,
329 });
330 }
331 memory::write(import_offset(&block, offset)?, bytes)?;
332 block.import_written_until = end;
333 block.store()?;
334 Ok(())
335}
336
337#[allow(dead_code)]
338pub fn begin_import(total_size: u64, expected_checksum: u64) -> Result<(), StableMemoryError> {
339 reject_during_update()?;
340 let mut block = Superblock::load()?;
341 if block.is_importing() {
342 return Err(StableMemoryError::ImportAlreadyStarted);
343 }
344 let import_base_offset = append_base()?;
345 checked_add(import_base_offset, total_size)?;
346 block.flags |= FLAG_IMPORTING;
347 block.clear_checksum_refresh();
348 block.import_expected_checksum = expected_checksum;
349 block.import_written_until = 0;
350 block.import_total_size = total_size;
351 block.import_base_offset = import_base_offset;
352 block.store()?;
353 Ok(())
354}
355
356#[allow(dead_code)]
357pub fn finish_import() -> Result<(), StableMemoryError> {
358 reject_during_update()?;
359 let mut block = Superblock::load()?;
360 if !block.is_importing() {
361 return Err(StableMemoryError::ImportNotStarted);
362 }
363 if block.import_written_until != block.import_total_size {
364 return Err(StableMemoryError::ImportIncomplete {
365 written_until: block.import_written_until,
366 db_size: block.import_total_size,
367 });
368 }
369 let checksum = checksum_physical_range(block.import_base_offset, block.import_total_size)?;
370 if checksum != block.import_expected_checksum {
371 let expected = block.import_expected_checksum;
372 clear_import(&mut block)?;
373 return Err(StableMemoryError::ChecksumMismatch {
374 expected,
375 actual: checksum,
376 });
377 }
378 block.db_size = block.import_total_size;
379 block.db_base_offset = block.import_base_offset;
380 block.page_table_offset = 0;
381 block.page_count = page_count_for_size(block.db_size)?;
382 block.layout_version = CURRENT_LAYOUT_VERSION;
383 block.schema_version = 0;
384 block.flags &= !FLAG_IMPORTING;
385 block.flags &= !FLAG_CHECKSUM_STALE;
386 block.clear_zero_extents();
387 block.clear_checksum_refresh();
388 block.checksum = checksum;
389 block.import_expected_checksum = 0;
390 block.import_written_until = 0;
391 block.import_total_size = 0;
392 block.import_base_offset = 0;
393 block.store()?;
394 Ok(())
395}
396
397#[allow(dead_code)]
398pub fn cancel_import() -> Result<(), StableMemoryError> {
399 reject_during_update()?;
400 let mut block = Superblock::load()?;
401 if !block.is_importing() {
402 return Err(StableMemoryError::ImportNotStarted);
403 }
404 clear_import(&mut block)
405}
406
407pub fn refresh_checksum() -> Result<u64, StableMemoryError> {
408 reject_during_update()?;
409 let checksum = checksum()?;
410 let mut block = Superblock::load()?;
411 block.checksum = checksum;
412 block.flags &= !FLAG_CHECKSUM_STALE;
413 block.clear_checksum_refresh();
414 block.store()?;
415 Ok(checksum)
416}
417
418pub fn refresh_checksum_chunk(max_bytes: u64) -> Result<ChecksumRefresh, StableMemoryError> {
419 reject_during_update()?;
420 if max_bytes == 0 {
421 return Err(StableMemoryError::ChecksumRefreshChunkEmpty);
422 }
423
424 let mut block = Superblock::load()?;
425 if block.is_importing() {
426 return Err(StableMemoryError::ImportAlreadyStarted);
427 }
428 if !block.is_checksum_refreshing() {
429 block.flags |= FLAG_CHECKSUM_REFRESHING;
430 block.checksum_refresh_offset = 0;
431 block.checksum_refresh_hash = fnv1a64(&[]);
432 block.checksum_refresh_tx_id = block.last_tx_id;
433 }
434 if block.checksum_refresh_tx_id != block.last_tx_id {
435 block.clear_checksum_refresh();
436 block.store()?;
437 return refresh_checksum_chunk(max_bytes);
438 }
439
440 let start = block.checksum_refresh_offset;
441 let end = block.db_size.min(start.saturating_add(max_bytes));
442 let mut offset = start;
443 let mut hash = block.checksum_refresh_hash;
444 while offset < end {
445 let len = (end - offset).min(CHECKSUM_CHUNK_LEN);
446 let copied_len = usize::try_from(len).map_err(|_| StableMemoryError::OffsetOverflow)?;
447 let mut bytes = vec![0_u8; copied_len];
448 read_logical_range(&block, offset, &mut bytes)?;
449 hash = fold_fnv1a64(hash, &bytes);
450 offset += len;
451 }
452
453 block.checksum_refresh_offset = offset;
454 block.checksum_refresh_hash = hash;
455 if offset == block.db_size {
456 block.checksum = hash;
457 block.flags &= !FLAG_CHECKSUM_STALE;
458 block.clear_checksum_refresh();
459 }
460 let out = ChecksumRefresh {
461 complete: offset == block.db_size,
462 checksum: hash,
463 scanned_bytes: offset,
464 db_size: block.db_size,
465 };
466 block.store()?;
467 Ok(out)
468}
469
470pub fn checksum() -> Result<u64, StableMemoryError> {
471 reject_during_update()?;
472 let block = Superblock::load()?;
473 checksum_logical_range(&block, block.db_size)
474}
475
476#[allow(dead_code)]
477pub fn compact() -> Result<(), StableMemoryError> {
478 reject_during_update()?;
479 ensure_current_layout()?;
480 Ok(())
481}
482
483#[allow(dead_code)]
484pub fn storage_stats() -> Result<StorageStats, StableMemoryError> {
485 let block = Superblock::load()?;
486 ensure_current_layout()?;
487 let page_table_bytes = 0;
488 let zero_extent_count =
489 u64::try_from(block.zero_extent_count()).map_err(|_| StableMemoryError::OffsetOverflow)?;
490 let active_bytes = SUPERBLOCK_SIZE
491 .checked_add(block.db_size)
492 .ok_or(StableMemoryError::OffsetOverflow)?;
493 let allocated_bytes = memory::size_pages()
494 .checked_mul(STABLE_PAGE_SIZE)
495 .ok_or(StableMemoryError::OffsetOverflow)?;
496 let orphan_bytes_estimate = allocated_bytes.saturating_sub(active_bytes);
497 let orphan_ratio_basis_points = orphan_bytes_estimate
498 .saturating_mul(10_000)
499 .checked_div(active_bytes)
500 .unwrap_or(0);
501 Ok(StorageStats {
502 layout_version: block.layout_version,
503 page_count: active_page_count(&block)?,
504 page_table_bytes,
505 zero_extent_count,
506 active_bytes,
507 allocated_bytes,
508 orphan_bytes_estimate,
509 orphan_ratio_basis_points,
510 compact_recommended: false,
511 })
512}
513
514pub(crate) fn page_count_for_size(size: u64) -> Result<u64, StableMemoryError> {
515 Ok(size.div_ceil(page_size()))
516}
517
518fn commit_overlay(overlay: Overlay, advance_tx: bool) -> Result<(), StableMemoryError> {
519 hit_failpoint(StableBlobFailpoint::CommitCapacity)?;
520 let profile_enabled = commit_profile_enabled();
521 let profile_start = commit_profile_start(profile_enabled);
522 let block = Superblock::load()?;
523 commit_profile_record_load(profile_start);
524 commit_overlay_in_place(&block, overlay, advance_tx, profile_enabled)
525}
526
527fn commit_overlay_in_place(
528 block: &Superblock,
529 overlay: Overlay,
530 advance_tx: bool,
531 profile_enabled: bool,
532) -> Result<(), StableMemoryError> {
533 let overlay_size = overlay.size();
534 let final_page_count = page_count_for_size(overlay_size)?;
535 debug_assert!(overlay
536 .dirty_pages()
537 .iter()
538 .all(|(page_no, _)| *page_no < final_page_count));
539 let dirty_pages = overlay.dirty_pages();
540 let zero_extents = zero_extents_after_commit(block, &overlay, final_page_count)?;
541
542 let mut required_end = checked_add(block.db_base_offset, overlay_size)?;
543 for (page_no, _) in dirty_pages {
544 if *page_no >= final_page_count {
545 continue;
546 }
547 required_end = required_end.max(checked_add(
548 page_physical_offset(block, *page_no)?,
549 page_size(),
550 )?);
551 }
552 let profile_start = commit_profile_start(profile_enabled);
553 memory::ensure_capacity(required_end)?;
554 commit_profile_record_capacity(profile_start);
555
556 let profile_start = commit_profile_start(profile_enabled);
557 for (page_no, page) in dirty_pages {
558 if *page_no >= final_page_count {
559 continue;
560 }
561 hit_failpoint(StableBlobFailpoint::CommitChunkWrite)?;
562 write_commit_page(
563 page_physical_offset(block, *page_no)?,
564 page,
565 profile_enabled,
566 )?;
567 }
568 commit_profile_record_page_write(profile_start);
569
570 if let Err(error) = hit_failpoint(StableBlobFailpoint::CommitSuperblockStore) {
571 panic!("failed to publish in-place commit after page writes: {error}");
572 }
573 let profile_start = commit_profile_start(profile_enabled);
574 let result =
575 store_commit_db_image(advance_tx, block.db_base_offset, overlay_size, zero_extents);
576 commit_profile_record_superblock_store(profile_start);
577 if let Err(error) = result {
578 panic!("failed to publish in-place commit after page writes: {error}");
579 }
580 Ok(())
581}
582
583#[cfg(any(test, debug_assertions, feature = "bench-profile"))]
584#[inline(always)]
585fn commit_profile_enabled() -> bool {
586 crate::read_metrics::metrics_enabled()
587}
588
589#[cfg(not(any(test, debug_assertions, feature = "bench-profile")))]
590#[inline(always)]
591fn commit_profile_enabled() -> bool {
592 false
593}
594
595#[cfg(any(test, debug_assertions, feature = "bench-profile"))]
596#[inline(always)]
597fn commit_profile_start(enabled: bool) -> Option<u64> {
598 if enabled {
599 Some(crate::read_metrics::instruction_counter())
600 } else {
601 None
602 }
603}
604
605#[cfg(not(any(test, debug_assertions, feature = "bench-profile")))]
606#[inline(always)]
607fn commit_profile_start(_enabled: bool) -> Option<u64> {
608 None
609}
610
611macro_rules! commit_profile_recorder {
612 ($name:ident, $record:ident) => {
613 #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
614 #[inline(always)]
615 fn $name(start: Option<u64>) {
616 if let Some(start) = start {
617 crate::read_metrics::$record(
618 crate::read_metrics::instruction_counter().saturating_sub(start),
619 );
620 }
621 }
622
623 #[cfg(not(any(test, debug_assertions, feature = "bench-profile")))]
624 #[inline(always)]
625 fn $name(_start: Option<u64>) {}
626 };
627}
628
629commit_profile_recorder!(commit_profile_record_capacity, record_commit_capacity);
630commit_profile_recorder!(commit_profile_record_load, record_commit_load);
631commit_profile_recorder!(commit_profile_record_page_write, record_commit_page_write);
632commit_profile_recorder!(
633 commit_profile_record_superblock_store,
634 record_commit_superblock_store
635);
636
637#[inline(always)]
638fn write_commit_page(
639 offset: u64,
640 page: &[u8],
641 profile_enabled: bool,
642) -> Result<(), StableMemoryError> {
643 if profile_enabled {
644 memory::write_prechecked(offset, page)
645 } else {
646 memory::write_prechecked_unmetered(offset, page)
647 }
648}
649
650fn store_commit_db_image(
651 advance_tx: bool,
652 db_base_offset: u64,
653 overlay_size: u64,
654 zero_extents: Vec<ZeroExtent>,
655) -> Result<(), StableMemoryError> {
656 match advance_tx {
657 true => Superblock::commit_db_image(db_base_offset, overlay_size, zero_extents),
658 false => Superblock::store_db_image_without_tx(db_base_offset, overlay_size, zero_extents),
659 }
660}
661
662fn zero_extents_after_commit(
663 block: &Superblock,
664 overlay: &Overlay,
665 final_page_count: u64,
666) -> Result<Vec<ZeroExtent>, StableMemoryError> {
667 let mut extents = block.zero_extents().to_vec();
668 let old_page_count = page_count_for_size(block.db_size)?;
669 let dirty_page_slack = overlay
670 .dirty_pages()
671 .iter()
672 .filter(|(page_no, _)| *page_no < final_page_count)
673 .count();
674 let temporary_limit = MAX_ZERO_EXTENTS.checked_add(dirty_page_slack).ok_or(
675 StableMemoryError::ZeroExtentLimitExceeded {
676 limit: MAX_ZERO_EXTENTS,
677 },
678 )?;
679 if overlay.size() > block.db_size {
683 let first_new_page = page_count_for_size(block.db_size)?;
684 add_zero_extent_without_limit(&mut extents, first_new_page, final_page_count);
685 enforce_temporary_zero_extent_limit(&extents, temporary_limit)?;
686 }
687 if overlay.size() < block.db_size {
688 let first_zero_page = page_count_for_size(overlay.size())?;
689 add_zero_extent_without_limit(&mut extents, first_zero_page, old_page_count);
690 enforce_temporary_zero_extent_limit(&extents, temporary_limit)?;
691 }
692 for extent in overlay.zero_extents() {
693 add_zero_extent_without_limit(&mut extents, extent.start_page, extent.end_page);
694 enforce_temporary_zero_extent_limit(&extents, temporary_limit)?;
695 }
696 for (page_no, _) in overlay.dirty_pages() {
697 if *page_no < final_page_count {
698 subtract_zero_extent_without_limit(&mut extents, *page_no, page_no.saturating_add(1));
699 }
700 }
701 enforce_zero_extent_limit(&extents)?;
702 Ok(extents)
703}
704
705fn enforce_temporary_zero_extent_limit(
706 extents: &[ZeroExtent],
707 temporary_limit: usize,
708) -> Result<(), StableMemoryError> {
709 if extents.len() > temporary_limit {
710 return Err(StableMemoryError::ZeroExtentLimitExceeded {
711 limit: MAX_ZERO_EXTENTS,
712 });
713 }
714 Ok(())
715}
716
717#[cfg(test)]
718fn add_zero_extent(
719 extents: &mut Vec<ZeroExtent>,
720 start_page: u64,
721 end_page: u64,
722) -> Result<(), StableMemoryError> {
723 if start_page >= end_page {
724 return Ok(());
725 }
726 extents.push(ZeroExtent {
727 start_page,
728 end_page,
729 });
730 normalize_zero_extents(extents)
731}
732
733fn add_zero_extent_without_limit(extents: &mut Vec<ZeroExtent>, start_page: u64, end_page: u64) {
734 if start_page >= end_page {
735 return;
736 }
737 extents.push(ZeroExtent {
738 start_page,
739 end_page,
740 });
741 normalize_zero_extents_without_limit(extents);
742}
743
744#[cfg(test)]
745fn subtract_zero_extent(
746 extents: &mut Vec<ZeroExtent>,
747 start_page: u64,
748 end_page: u64,
749) -> Result<(), StableMemoryError> {
750 if start_page >= end_page || extents.is_empty() {
751 return Ok(());
752 }
753 let mut next = Vec::with_capacity(extents.len() + 1);
754 for extent in extents.iter() {
755 if end_page <= extent.start_page || start_page >= extent.end_page {
756 next.push(extent.clone());
757 continue;
758 }
759 if extent.start_page < start_page {
760 next.push(ZeroExtent {
761 start_page: extent.start_page,
762 end_page: start_page,
763 });
764 }
765 if end_page < extent.end_page {
766 next.push(ZeroExtent {
767 start_page: end_page,
768 end_page: extent.end_page,
769 });
770 }
771 }
772 *extents = next;
773 normalize_zero_extents(extents)
774}
775
776fn subtract_zero_extent_without_limit(
777 extents: &mut Vec<ZeroExtent>,
778 start_page: u64,
779 end_page: u64,
780) {
781 if start_page >= end_page || extents.is_empty() {
782 return;
783 }
784 let mut next = Vec::with_capacity(extents.len() + 1);
785 for extent in extents.iter() {
786 if end_page <= extent.start_page || start_page >= extent.end_page {
787 next.push(extent.clone());
788 continue;
789 }
790 if extent.start_page < start_page {
791 next.push(ZeroExtent {
792 start_page: extent.start_page,
793 end_page: start_page,
794 });
795 }
796 if end_page < extent.end_page {
797 next.push(ZeroExtent {
798 start_page: end_page,
799 end_page: extent.end_page,
800 });
801 }
802 }
803 *extents = next;
804 normalize_zero_extents_without_limit(extents);
805}
806
807#[cfg(test)]
808fn normalize_zero_extents(extents: &mut Vec<ZeroExtent>) -> Result<(), StableMemoryError> {
809 normalize_zero_extents_without_limit(extents);
810 enforce_zero_extent_limit(extents)
811}
812
813fn normalize_zero_extents_without_limit(extents: &mut Vec<ZeroExtent>) {
814 extents.retain(|extent| extent.start_page < extent.end_page);
815 extents.sort_by_key(|extent| extent.start_page);
816 let mut normalized: Vec<ZeroExtent> = Vec::with_capacity(extents.len());
817 for extent in extents.drain(..) {
818 if let Some(last) = normalized.last_mut() {
819 if extent.start_page <= last.end_page {
820 last.end_page = last.end_page.max(extent.end_page);
821 continue;
822 }
823 }
824 normalized.push(extent);
825 }
826 *extents = normalized;
827}
828
829fn enforce_zero_extent_limit(extents: &[ZeroExtent]) -> Result<(), StableMemoryError> {
830 if extents.len() > MAX_ZERO_EXTENTS {
831 return Err(StableMemoryError::ZeroExtentLimitExceeded {
832 limit: MAX_ZERO_EXTENTS,
833 });
834 }
835 Ok(())
836}
837
838fn reject_during_update() -> Result<(), StableMemoryError> {
839 if overlay::is_active() {
840 Err(StableMemoryError::UpdateInProgress)
841 } else {
842 Ok(())
843 }
844}
845
846fn read_logical_range(
847 block: &Superblock,
848 offset: u64,
849 dst: &mut [u8],
850) -> Result<(), StableMemoryError> {
851 if dst.is_empty() {
852 return Ok(());
853 }
854 let in_page =
855 usize::try_from(offset % page_size()).map_err(|_| StableMemoryError::OffsetOverflow)?;
856 if dst.len() <= page_len() - in_page {
857 return read_logical_page_slice(block, offset / page_size(), in_page, dst);
858 }
859
860 let mut copied_total = 0_usize;
861 while copied_total < dst.len() {
862 let absolute = checked_add(
863 offset,
864 u64::try_from(copied_total).map_err(|_| StableMemoryError::OffsetOverflow)?,
865 )?;
866 let page_no = absolute / page_size();
867 let in_page = usize::try_from(absolute % page_size())
868 .map_err(|_| StableMemoryError::OffsetOverflow)?;
869 let copied = (page_len() - in_page).min(dst.len() - copied_total);
870 read_logical_page_slice(
871 block,
872 page_no,
873 in_page,
874 &mut dst[copied_total..copied_total + copied],
875 )?;
876 copied_total += copied;
877 }
878 Ok(())
879}
880
881fn read_logical_range_with_page_cache(
882 block: &Superblock,
883 offset: u64,
884 dst: &mut [u8],
885 page_offsets: &mut PageOffsetCache,
886) -> Result<(), StableMemoryError> {
887 let in_page =
888 usize::try_from(offset % page_size()).map_err(|_| StableMemoryError::OffsetOverflow)?;
889 if dst.len() <= page_len() - in_page {
890 return read_logical_page_slice_with_page_cache(
891 block,
892 offset / page_size(),
893 in_page,
894 dst,
895 page_offsets,
896 );
897 }
898
899 let mut copied_total = 0_usize;
900 while copied_total < dst.len() {
901 let absolute = checked_add(
902 offset,
903 u64::try_from(copied_total).map_err(|_| StableMemoryError::OffsetOverflow)?,
904 )?;
905 let page_no = absolute / page_size();
906 let in_page = usize::try_from(absolute % page_size())
907 .map_err(|_| StableMemoryError::OffsetOverflow)?;
908 let copied = (page_len() - in_page).min(dst.len() - copied_total);
909 read_logical_page_slice_with_page_cache(
910 block,
911 page_no,
912 in_page,
913 &mut dst[copied_total..copied_total + copied],
914 page_offsets,
915 )?;
916 copied_total += copied;
917 }
918 Ok(())
919}
920
921fn read_logical_page_slice(
922 block: &Superblock,
923 page_no: u64,
924 in_page: usize,
925 dst: &mut [u8],
926) -> Result<(), StableMemoryError> {
927 if page_is_zero_masked(block, page_no) {
928 dst.fill(0);
929 return Ok(());
930 }
931 let physical = page_offset_for(block, page_no)?;
932 if physical == 0 {
933 dst.fill(0);
934 return Ok(());
935 }
936 let stable_offset = checked_add(
937 physical,
938 u64::try_from(in_page).map_err(|_| StableMemoryError::OffsetOverflow)?,
939 )?;
940 #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
941 crate::read_metrics::record_stable_data_read(dst.len());
942 memory::read_preallocated(stable_offset, dst)
943}
944
945#[inline(always)]
946fn read_logical_page_slice_with_page_cache(
947 block: &Superblock,
948 page_no: u64,
949 in_page: usize,
950 dst: &mut [u8],
951 page_offsets: &mut PageOffsetCache,
952) -> Result<(), StableMemoryError> {
953 if page_is_zero_masked(block, page_no) {
954 dst.fill(0);
955 return Ok(());
956 }
957 if dst.len() < page_len() && page_offsets.copy_page_slice(page_no, in_page, dst) {
958 return Ok(());
959 }
960 let physical = match page_offsets.get(page_no) {
961 Some(physical) => physical,
962 None => {
963 let physical = page_offset_for(block, page_no)?;
964 page_offsets.insert(page_no, physical);
965 physical
966 }
967 };
968 if physical == 0 {
969 dst.fill(0);
970 return Ok(());
971 }
972 if in_page == 0 && dst.len() == page_len() {
973 #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
974 crate::read_metrics::record_stable_data_read(dst.len());
975 return memory::read_preallocated(physical, dst);
976 }
977 if dst.len() < page_len() {
978 let mut page = zero_page();
979 #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
980 crate::read_metrics::record_stable_data_read(page.len());
981 memory::read_preallocated(physical, &mut page)?;
982 let end = in_page + dst.len();
983 dst.copy_from_slice(&page[in_page..end]);
984 page_offsets.insert_page(page_no, page);
985 return Ok(());
986 }
987 let stable_offset = checked_add(
988 physical,
989 u64::try_from(in_page).map_err(|_| StableMemoryError::OffsetOverflow)?,
990 )?;
991 #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
992 crate::read_metrics::record_stable_data_read(dst.len());
993 memory::read_preallocated(stable_offset, dst)
994}
995
996fn page_offset_for(block: &Superblock, page_no: u64) -> Result<u64, StableMemoryError> {
997 if page_no >= active_page_count(block)? {
998 return Ok(0);
999 }
1000 page_physical_offset(block, page_no)
1001}
1002
1003fn page_is_zero_masked(block: &Superblock, page_no: u64) -> bool {
1004 block
1005 .zero_extents()
1006 .iter()
1007 .any(|extent| page_no >= extent.start_page && page_no < extent.end_page)
1008}
1009
1010fn page_physical_offset(block: &Superblock, page_no: u64) -> Result<u64, StableMemoryError> {
1011 checked_add(
1012 block.db_base_offset,
1013 page_no
1014 .checked_mul(page_size())
1015 .ok_or(StableMemoryError::OffsetOverflow)?,
1016 )
1017}
1018
1019fn checksum_logical_range(block: &Superblock, len: u64) -> Result<u64, StableMemoryError> {
1020 let mut offset = 0_u64;
1021 let mut hash = fnv1a64(&[]);
1022 while offset < len {
1023 let chunk_len = (len - offset).min(CHECKSUM_CHUNK_LEN);
1024 let copied_len =
1025 usize::try_from(chunk_len).map_err(|_| StableMemoryError::OffsetOverflow)?;
1026 let mut bytes = vec![0_u8; copied_len];
1027 read_logical_range(block, offset, &mut bytes)?;
1028 hash = fold_fnv1a64(hash, &bytes);
1029 offset += chunk_len;
1030 }
1031 Ok(hash)
1032}
1033
1034#[allow(dead_code)]
1035fn checksum_physical_range(base_offset: u64, len: u64) -> Result<u64, StableMemoryError> {
1036 let mut offset = 0_u64;
1037 let mut hash = fnv1a64(&[]);
1038 while offset < len {
1039 let chunk_len = (len - offset).min(CHECKSUM_CHUNK_LEN);
1040 let copied_len =
1041 usize::try_from(chunk_len).map_err(|_| StableMemoryError::OffsetOverflow)?;
1042 let mut bytes = vec![0_u8; copied_len];
1043 memory::read_preallocated(checked_add(base_offset, offset)?, &mut bytes)?;
1044 hash = fold_fnv1a64(hash, &bytes);
1045 offset += chunk_len;
1046 }
1047 Ok(hash)
1048}
1049
1050#[allow(dead_code)]
1051fn clear_import(block: &mut Superblock) -> Result<(), StableMemoryError> {
1052 block.flags &= !FLAG_IMPORTING;
1053 block.import_expected_checksum = 0;
1054 block.import_written_until = 0;
1055 block.import_total_size = 0;
1056 block.import_base_offset = 0;
1057 block.store()?;
1058 Ok(())
1059}
1060
1061#[allow(dead_code)]
1062fn import_offset(block: &Superblock, offset: u64) -> Result<u64, StableMemoryError> {
1063 checked_add(block.import_base_offset, offset)
1064}
1065
1066fn active_page_count(block: &Superblock) -> Result<u64, StableMemoryError> {
1067 page_count_for_size(block.db_size)
1068}
1069
1070#[allow(dead_code)]
1071fn append_base() -> Result<u64, StableMemoryError> {
1072 memory::size_pages()
1074 .checked_mul(STABLE_PAGE_SIZE)
1075 .ok_or(StableMemoryError::OffsetOverflow)
1076}
1077
1078fn page_size() -> u64 {
1079 u64::from(SQLITE_PAGE_SIZE)
1080}
1081
1082fn page_len() -> usize {
1083 usize::try_from(SQLITE_PAGE_SIZE).expect("SQLite page size fits usize")
1084}
1085
1086fn zero_page() -> Vec<u8> {
1087 vec![0_u8; page_len()]
1088}
1089
1090fn checked_add(left: u64, right: u64) -> Result<u64, StableMemoryError> {
1091 left.checked_add(right)
1092 .ok_or(StableMemoryError::OffsetOverflow)
1093}
1094
1095fn fold_fnv1a64(mut hash: u64, bytes: &[u8]) -> u64 {
1096 for byte in bytes {
1097 hash ^= u64::from(*byte);
1098 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1099 }
1100 hash
1101}
1102
1103#[cfg(test)]
1104fn hit_failpoint(failpoint: StableBlobFailpoint) -> Result<(), StableMemoryError> {
1105 let Ok(context) = memory::active_context_id() else {
1106 return Ok(());
1107 };
1108 FAILPOINTS.with(|slot| {
1109 let mut slot = slot.borrow_mut();
1110 if slot.get(&context).copied() == Some(failpoint) {
1111 slot.remove(&context);
1112 Err(StableMemoryError::Failpoint(failpoint.name()))
1113 } else {
1114 Ok(())
1115 }
1116 })
1117}
1118
1119#[cfg(not(test))]
1120fn hit_failpoint(_failpoint: StableBlobFailpoint) -> Result<(), StableMemoryError> {
1121 Ok(())
1122}
1123
1124#[cfg(test)]
1125impl StableBlobFailpoint {
1126 fn name(self) -> &'static str {
1127 match self {
1128 Self::OverlayWrite => "before overlay write",
1129 Self::OverlayTruncate => "before overlay truncate",
1130 Self::CommitCapacity => "before commit capacity",
1131 Self::CommitChunkWrite => "before commit page write",
1132 Self::CommitSuperblockStore => "before commit superblock store",
1133 }
1134 }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140 use proptest::prelude::*;
1141 use proptest::test_runner::{Config, TestRunner};
1142 use std::collections::BTreeSet;
1143
1144 #[test]
1145 fn layout_math_matches_expected_boundaries() {
1146 assert_eq!(page_count_for_size(0).unwrap(), 0);
1147 assert_eq!(page_count_for_size(1).unwrap(), 1);
1148 assert_eq!(page_count_for_size(page_size()).unwrap(), 1);
1149 assert_eq!(page_count_for_size(page_size() + 1).unwrap(), 2);
1150 }
1151
1152 #[test]
1153 fn layout_math_rejects_u64_max_overflow_boundaries() {
1154 assert!(matches!(
1155 checked_add(u64::MAX, 1),
1156 Err(StableMemoryError::OffsetOverflow)
1157 ));
1158
1159 let mut block = Superblock::fresh();
1160 block.import_base_offset = u64::MAX;
1161 assert!(matches!(
1162 import_offset(&block, 1),
1163 Err(StableMemoryError::OffsetOverflow)
1164 ));
1165
1166 block.db_base_offset = u64::MAX - page_size() + 1;
1167 assert!(matches!(
1168 page_physical_offset(&block, 1),
1169 Err(StableMemoryError::OffsetOverflow)
1170 ));
1171 }
1172
1173 #[test]
1174 fn pbt_layout_math_matches_verus_model() {
1175 let mut runner = TestRunner::new(Config {
1176 cases: 512,
1177 ..Config::default()
1178 });
1179
1180 runner
1181 .run(
1182 &(boundary_size_strategy(), boundary_page_strategy()),
1183 |(size, page_no)| {
1184 let page_count = page_count_for_size(size).unwrap();
1185 let page_size = u128::from(page_size());
1186 if size == 0 {
1187 prop_assert_eq!(page_count, 0);
1188 } else {
1189 prop_assert!(u128::from(page_count - 1) * page_size < u128::from(size));
1190 prop_assert!(u128::from(size) <= u128::from(page_count) * page_size);
1191 }
1192
1193 let mut block = Superblock::fresh();
1194 block.db_base_offset = SUPERBLOCK_SIZE;
1195 let expected = u128::from(SUPERBLOCK_SIZE) + u128::from(page_no) * page_size;
1196 match page_physical_offset(&block, page_no) {
1197 Ok(physical) => prop_assert_eq!(u128::from(physical), expected),
1198 Err(StableMemoryError::OffsetOverflow) => {
1199 prop_assert!(expected > u128::from(u64::MAX));
1200 }
1201 Err(error) => return Err(TestCaseError::fail(error.to_string())),
1202 }
1203 Ok(())
1204 },
1205 )
1206 .unwrap();
1207 }
1208
1209 fn boundary_size_strategy() -> impl Strategy<Value = u64> {
1210 let page = page_size();
1211 let far_page_bytes = FAR_PAGE_NO * page;
1212 prop_oneof![
1213 any::<u64>(),
1214 prop::sample::select(boundary_values(&[
1215 0,
1216 1,
1217 page - 1,
1218 page,
1219 page + 1,
1220 far_page_bytes - 1,
1221 far_page_bytes,
1222 far_page_bytes + 1,
1223 u64::MAX,
1224 ])),
1225 ]
1226 }
1227
1228 fn boundary_page_strategy() -> impl Strategy<Value = u64> {
1229 prop_oneof![
1230 any::<u64>(),
1231 prop::sample::select(boundary_values(&[
1232 0,
1233 1,
1234 FAR_PAGE_NO - 1,
1235 FAR_PAGE_NO,
1236 FAR_PAGE_NO + 1,
1237 u64::MAX,
1238 ])),
1239 ]
1240 }
1241
1242 fn boundary_values(values: &[u64]) -> Vec<u64> {
1243 values
1244 .iter()
1245 .flat_map(|value| [value.saturating_sub(1), *value, value.saturating_add(1)])
1246 .collect()
1247 }
1248
1249 #[test]
1250 fn fnv_fold_matches_one_pass_for_multiple_partitions() {
1251 let bytes: Vec<u8> = (0..97)
1252 .map(|index| (index as u8).wrapping_mul(37).wrapping_add(11))
1253 .collect();
1254 let expected = fnv1a64(&bytes);
1255
1256 for split in [0_usize, 1, 2, 7, 31, 64, bytes.len()] {
1257 let split = split.min(bytes.len());
1258 let mut hash = fnv1a64(&[]);
1259 hash = fold_fnv1a64(hash, &bytes[..split]);
1260 hash = fold_fnv1a64(hash, &bytes[split..]);
1261 assert_eq!(hash, expected);
1262 }
1263
1264 let mut hash = fnv1a64(&[]);
1265 for chunk in bytes.chunks(13) {
1266 hash = fold_fnv1a64(hash, chunk);
1267 }
1268 assert_eq!(hash, expected);
1269 }
1270
1271 #[test]
1272 #[serial_test::serial]
1273 fn in_place_commit_keeps_dirty_page_offsets_stable() {
1274 crate::stable::memory::reset_for_tests();
1275 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1276
1277 let page_zero = vec![1_u8; page_len()];
1278 let page_later = vec![2_u8; page_len()];
1279 let later_page_no = FAR_PAGE_NO;
1280 write_at(0, &page_zero).unwrap();
1281 write_at(later_page_no * page_size(), &page_later).unwrap();
1282
1283 let block = Superblock::load().unwrap();
1284 let old_page_zero_offset = page_physical_offset(&block, 0).unwrap();
1285 let old_later_offset = page_physical_offset(&block, later_page_no).unwrap();
1286
1287 let updated_page_zero = vec![3_u8; page_len()];
1288 write_at(0, &updated_page_zero).unwrap();
1289 let updated_block = Superblock::load().unwrap();
1290 let mut out = vec![0_u8; page_len()];
1291 read_base_at(0, &mut out).unwrap();
1292
1293 assert_eq!(
1294 page_physical_offset(&updated_block, 0).unwrap(),
1295 old_page_zero_offset
1296 );
1297 assert_eq!(
1298 page_physical_offset(&updated_block, later_page_no).unwrap(),
1299 old_later_offset
1300 );
1301 assert_eq!(out, updated_page_zero);
1302 }
1303
1304 #[test]
1305 #[serial_test::serial]
1306 fn in_place_commit_tracks_multi_segment_dirty_and_clean_pages() {
1307 crate::stable::memory::reset_for_tests();
1308 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1309
1310 let clean_page_no = 1;
1311 let later_page_no = FAR_PAGE_NO;
1312 write_at(0, &vec![1_u8; page_len()]).unwrap();
1313 write_at(clean_page_no * page_size(), &vec![2_u8; page_len()]).unwrap();
1314 write_at(later_page_no * page_size(), &vec![3_u8; page_len()]).unwrap();
1315
1316 let before = Superblock::load().unwrap();
1317 let before_first = page_physical_offset(&before, 0).unwrap();
1318 let before_clean = page_physical_offset(&before, clean_page_no).unwrap();
1319 let before_later = page_physical_offset(&before, later_page_no).unwrap();
1320
1321 begin_update().unwrap();
1322 write_at(0, &vec![4_u8; page_len()]).unwrap();
1323 write_at(later_page_no * page_size(), &vec![5_u8; page_len()]).unwrap();
1324 commit_update().unwrap();
1325
1326 let after = Superblock::load().unwrap();
1327
1328 assert_eq!(after.page_count, active_page_count(&after).unwrap());
1329 assert_eq!(page_physical_offset(&after, 0).unwrap(), before_first);
1330 assert_eq!(
1331 page_physical_offset(&after, clean_page_no).unwrap(),
1332 before_clean
1333 );
1334 assert_eq!(
1335 page_physical_offset(&after, later_page_no).unwrap(),
1336 before_later
1337 );
1338 }
1339
1340 #[test]
1341 #[serial_test::serial]
1342 fn in_place_truncate_hides_old_tail_pages() {
1343 crate::stable::memory::reset_for_tests();
1344 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1345
1346 write_at(0, &vec![1_u8; page_len()]).unwrap();
1347 write_at(page_size(), &vec![2_u8; page_len()]).unwrap();
1348 write_at(2 * page_size(), &vec![3_u8; page_len()]).unwrap();
1349 truncate(page_size()).unwrap();
1350
1351 let block = Superblock::load().unwrap();
1352 let mut hidden = vec![1_u8; page_len()];
1353 read_base_at(page_size(), &mut hidden).unwrap();
1354
1355 assert_eq!(block.db_size, page_size());
1356 assert_eq!(block.page_count, 1);
1357 assert_eq!(block.zero_extent_count(), 1);
1358 assert_eq!(hidden, vec![0_u8; page_len()]);
1359 }
1360
1361 #[test]
1362 #[serial_test::serial]
1363 fn same_overlay_truncate_reextend_zeroes_active_gap() {
1364 crate::stable::memory::reset_for_tests();
1365 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1366
1367 write_at(0, &vec![1_u8; page_len()]).unwrap();
1368 write_at(page_size(), &vec![0xAA; page_len()]).unwrap();
1369 write_at(2 * page_size(), &vec![3_u8; page_len()]).unwrap();
1370
1371 begin_update().unwrap();
1372 truncate(page_size()).unwrap();
1373 truncate(2 * page_size()).unwrap();
1374 let mut overlay_gap = vec![1_u8; page_len()];
1375 read_at(page_size(), &mut overlay_gap).unwrap();
1376 commit_update().unwrap();
1377
1378 let committed_gap = export_chunk(page_size(), page_size()).unwrap();
1379
1380 assert_eq!(overlay_gap, vec![0_u8; page_len()]);
1381 assert_eq!(committed_gap, vec![0_u8; page_len()]);
1382 }
1383
1384 #[test]
1385 #[serial_test::serial]
1386 fn compact_is_noop_for_in_place_layout() {
1387 crate::stable::memory::reset_for_tests();
1388 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1389
1390 let later_page_no = FAR_PAGE_NO;
1391 let first_page = vec![7_u8; page_len()];
1392 let later_page = vec![9_u8; page_len()];
1393 write_at(0, &first_page).unwrap();
1394 write_at(later_page_no * page_size(), &later_page).unwrap();
1395
1396 compact().unwrap();
1397
1398 let block = Superblock::load().unwrap();
1399 let mut first_out = vec![0_u8; page_len()];
1400 let mut later_out = vec![0_u8; page_len()];
1401
1402 read_base_at(0, &mut first_out).unwrap();
1403 read_base_at(later_page_no * page_size(), &mut later_out).unwrap();
1404
1405 assert_eq!(
1406 page_physical_offset(&block, later_page_no).unwrap(),
1407 block.db_base_offset + later_page_no * page_size()
1408 );
1409 assert_eq!(first_out, first_page);
1410 assert_eq!(later_out, later_page);
1411 }
1412
1413 #[test]
1414 #[serial_test::serial]
1415 fn in_place_expand_only_commit_zeroes_new_logical_tail() {
1416 crate::stable::memory::reset_for_tests();
1417 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1418
1419 write_at(0, &[0]).unwrap();
1420 truncate(page_size() * 4).unwrap();
1421 truncate(page_size() * 4 + 1).unwrap();
1422
1423 let block = Superblock::load().unwrap();
1424 let mut first = [1_u8; 1];
1425 let mut expanded_tail = [1_u8; 1];
1426
1427 read_base_at(0, &mut first).unwrap();
1428 read_base_at(page_size() * 4, &mut expanded_tail).unwrap();
1429
1430 assert_eq!(block.db_size, page_size() * 4 + 1);
1431 assert_eq!(
1432 page_physical_offset(&block, 0).unwrap(),
1433 block.db_base_offset
1434 );
1435 assert_eq!(
1436 page_physical_offset(&block, 4).unwrap(),
1437 block.db_base_offset + 4 * page_size()
1438 );
1439 assert_eq!(first, [0]);
1440 assert_eq!(expanded_tail, [0]);
1441 }
1442
1443 #[test]
1444 #[serial_test::serial]
1445 fn truncate_grow_zeroes_stale_physical_gap() {
1446 crate::stable::memory::reset_for_tests();
1447 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1448
1449 let old_size = page_size() + 17;
1450 let grow_len = page_size() * 2 + 111;
1451 let grow_len_usize = usize::try_from(grow_len).unwrap();
1452 write_at(old_size - 1, b"a").unwrap();
1453 let block = Superblock::load().unwrap();
1454 crate::stable::memory::write(block.db_base_offset + old_size, &vec![0xAA; grow_len_usize])
1455 .unwrap();
1456
1457 truncate(old_size + grow_len).unwrap();
1458 let gap = export_chunk(old_size, grow_len).unwrap();
1459
1460 assert!(gap.iter().all(|byte| *byte == 0));
1461 }
1462
1463 #[test]
1464 #[serial_test::serial]
1465 fn sparse_write_zeroes_stale_physical_gap() {
1466 crate::stable::memory::reset_for_tests();
1467 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1468
1469 let old_size = page_size() + 17;
1470 let write_offset = old_size + page_size() * 2 + 33;
1471 let gap_len = write_offset - old_size;
1472 let gap_len_usize = usize::try_from(gap_len).unwrap();
1473 write_at(old_size - 1, b"a").unwrap();
1474 let block = Superblock::load().unwrap();
1475 crate::stable::memory::write(block.db_base_offset + old_size, &vec![0xAA; gap_len_usize])
1476 .unwrap();
1477
1478 write_at(write_offset, b"z").unwrap();
1479 let gap = export_chunk(old_size, gap_len).unwrap();
1480 let written = export_chunk(write_offset, 1).unwrap();
1481
1482 assert!(gap.iter().all(|byte| *byte == 0));
1483 assert_eq!(written, b"z");
1484 }
1485
1486 #[test]
1487 #[serial_test::serial]
1488 fn failed_import_slack_stays_hidden_after_normal_grow() {
1489 crate::stable::memory::reset_for_tests();
1490 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1491
1492 write_at(0, b"a").unwrap();
1493 begin_import(page_size(), 0).unwrap();
1494 let importing = Superblock::load().unwrap();
1495 import_chunk(0, &vec![0xAA; page_len()]).unwrap();
1496 assert!(finish_import().is_err());
1497
1498 let block = Superblock::load().unwrap();
1499 let logical_import_offset = importing.import_base_offset - block.db_base_offset;
1500 truncate(logical_import_offset + page_size()).unwrap();
1501 let grown_over_import_slack = export_chunk(logical_import_offset, page_size()).unwrap();
1502
1503 assert!(grown_over_import_slack.iter().all(|byte| *byte == 0));
1504 }
1505
1506 #[test]
1507 #[serial_test::serial]
1508 fn zero_mask_hides_truncated_tail_without_moving_base() {
1509 crate::stable::memory::reset_for_tests();
1510 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1511
1512 write_at(0, &vec![b'a'; page_len()]).unwrap();
1513 write_at(page_size(), &vec![b'b'; page_len()]).unwrap();
1514 let old_base = Superblock::load().unwrap().db_base_offset;
1515
1516 truncate(1).unwrap();
1517 let mut stale_tail = [0_u8; 1];
1518 crate::stable::memory::read_preallocated(old_base + page_size(), &mut stale_tail).unwrap();
1519 assert_eq!(stale_tail, [b'b']);
1520
1521 let grow_offset = 2 * page_size() + 2;
1522 write_at(grow_offset, b"z").unwrap();
1523
1524 let block = Superblock::load().unwrap();
1525 let image = export_chunk(0, block.db_size).unwrap();
1526 let z_index = usize::try_from(grow_offset).unwrap();
1527
1528 assert_eq!(block.db_base_offset, old_base);
1529 assert_eq!(block.db_size, grow_offset + 1);
1530 assert_eq!(block.zero_extent_count(), 1);
1531 assert_eq!(image[0], b'a');
1532 assert!(image[1..z_index].iter().all(|byte| *byte == 0));
1533 assert_eq!(image[z_index], b'z');
1534 }
1535
1536 #[test]
1537 fn zero_extent_add_subtract_merges_and_splits_ranges() {
1538 let mut extents = Vec::new();
1539 add_zero_extent(&mut extents, 10, 20).unwrap();
1540 add_zero_extent(&mut extents, 30, 40).unwrap();
1541 add_zero_extent(&mut extents, 20, 30).unwrap();
1542 assert_eq!(
1543 extents,
1544 vec![ZeroExtent {
1545 start_page: 10,
1546 end_page: 40
1547 }]
1548 );
1549
1550 subtract_zero_extent(&mut extents, 15, 17).unwrap();
1551 assert_eq!(
1552 extents,
1553 vec![
1554 ZeroExtent {
1555 start_page: 10,
1556 end_page: 15
1557 },
1558 ZeroExtent {
1559 start_page: 17,
1560 end_page: 40
1561 }
1562 ]
1563 );
1564
1565 subtract_zero_extent(&mut extents, 8, 18).unwrap();
1566 assert_eq!(
1567 extents,
1568 vec![ZeroExtent {
1569 start_page: 18,
1570 end_page: 40
1571 }]
1572 );
1573 }
1574
1575 #[test]
1576 fn zero_extent_truncate_tail_merges_with_existing_range() {
1577 let mut extents = vec![ZeroExtent {
1578 start_page: 10,
1579 end_page: 20,
1580 }];
1581
1582 add_zero_extent(&mut extents, 10, 40).unwrap();
1583
1584 assert_eq!(
1585 extents,
1586 vec![ZeroExtent {
1587 start_page: 10,
1588 end_page: 40
1589 }]
1590 );
1591 }
1592
1593 #[test]
1594 fn zero_extent_commit_allows_temporary_limit_excess_removed_by_dirty_page() {
1595 let mut block = Superblock::fresh();
1596 block.db_size = (MAX_ZERO_EXTENTS as u64) * 2 * page_size();
1597 block.zero_extents = (0..MAX_ZERO_EXTENTS)
1598 .map(|index| ZeroExtent {
1599 start_page: (index as u64) * 2,
1600 end_page: (index as u64) * 2 + 1,
1601 })
1602 .collect();
1603
1604 let mut overlay = Overlay::new(block.db_size);
1605 overlay
1606 .write_at(block.db_size, &vec![0xAA; page_len()])
1607 .unwrap();
1608 let final_page_count = page_count_for_size(overlay.size()).unwrap();
1609
1610 let extents = zero_extents_after_commit(&block, &overlay, final_page_count).unwrap();
1611
1612 assert_eq!(extents.len(), MAX_ZERO_EXTENTS);
1613 assert_eq!(extents, block.zero_extents);
1614 }
1615
1616 #[test]
1617 fn zero_extent_commit_rejects_final_limit_excess() {
1618 let mut block = Superblock::fresh();
1619 block.db_size = (MAX_ZERO_EXTENTS as u64) * 2 * page_size();
1620 block.zero_extents = (0..MAX_ZERO_EXTENTS)
1621 .map(|index| ZeroExtent {
1622 start_page: (index as u64) * 2,
1623 end_page: (index as u64) * 2 + 1,
1624 })
1625 .collect();
1626
1627 let mut overlay = Overlay::new(block.db_size);
1628 overlay.truncate(block.db_size + page_size()).unwrap();
1629 let final_page_count = page_count_for_size(overlay.size()).unwrap();
1630
1631 assert!(matches!(
1632 zero_extents_after_commit(&block, &overlay, final_page_count),
1633 Err(StableMemoryError::ZeroExtentLimitExceeded {
1634 limit: MAX_ZERO_EXTENTS
1635 })
1636 ));
1637 }
1638
1639 #[test]
1640 #[serial_test::serial]
1641 fn zero_extent_limit_error_keeps_active_image_and_metadata() {
1642 crate::stable::memory::reset_for_tests();
1643 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1644
1645 let mut block = Superblock::fresh();
1646 let max_zero_extents = u64::try_from(MAX_ZERO_EXTENTS).unwrap();
1647 block.db_size = max_zero_extents * 2 * page_size();
1648 block.page_count = page_count_for_size(block.db_size).unwrap();
1649 block.last_tx_id = 41;
1650 block.flags = FLAG_CHECKSUM_STALE | FLAG_CHECKSUM_REFRESHING;
1651 block.checksum = 0xA5A5;
1652 block.checksum_refresh_offset = 17;
1653 block.checksum_refresh_hash = 0x5A5A;
1654 block.checksum_refresh_tx_id = 40;
1655 block.zero_extents = (0..MAX_ZERO_EXTENTS)
1656 .map(|index| ZeroExtent {
1657 start_page: u64::try_from(index).unwrap() * 2,
1658 end_page: u64::try_from(index).unwrap() * 2 + 1,
1659 })
1660 .collect();
1661
1662 let sample_page_no = 1_u64;
1663 let sample_offset = page_physical_offset(&block, sample_page_no).unwrap();
1664 let sample = b"active-before";
1665 let sample_len = u64::try_from(sample.len()).unwrap();
1666 crate::stable::memory::ensure_capacity(checked_add(sample_offset, sample_len).unwrap())
1667 .unwrap();
1668 crate::stable::memory::write(sample_offset, sample).unwrap();
1669 block.store().unwrap();
1670
1671 let before_block = Superblock::load().unwrap();
1672 let mut before_sample = vec![0_u8; sample.len()];
1673 read_base_at(sample_page_no * page_size(), &mut before_sample).unwrap();
1674 assert_eq!(before_sample, sample);
1675
1676 begin_update().unwrap();
1677 truncate(block.db_size + page_size()).unwrap();
1678 let result = commit_update();
1679
1680 assert!(matches!(
1681 result,
1682 Err(StableMemoryError::ZeroExtentLimitExceeded {
1683 limit: MAX_ZERO_EXTENTS
1684 })
1685 ));
1686 assert_eq!(Superblock::load().unwrap(), before_block);
1687
1688 let mut after_sample = vec![0_u8; sample.len()];
1689 read_base_at(sample_page_no * page_size(), &mut after_sample).unwrap();
1690 assert_eq!(after_sample, before_sample);
1691 }
1692
1693 #[test]
1694 fn zero_extent_temporary_limit_allows_only_dirty_page_slack() {
1695 let extents = (0..=MAX_ZERO_EXTENTS)
1696 .map(|index| ZeroExtent {
1697 start_page: index as u64,
1698 end_page: index as u64 + 1,
1699 })
1700 .collect::<Vec<_>>();
1701
1702 enforce_temporary_zero_extent_limit(&extents, MAX_ZERO_EXTENTS + 1).unwrap();
1703 assert!(matches!(
1704 enforce_temporary_zero_extent_limit(&extents, MAX_ZERO_EXTENTS),
1705 Err(StableMemoryError::ZeroExtentLimitExceeded {
1706 limit: MAX_ZERO_EXTENTS
1707 })
1708 ));
1709 }
1710
1711 #[test]
1712 fn zero_extent_limit_counts_normalized_ranges() {
1713 let mut extents = (0..MAX_ZERO_EXTENTS)
1714 .map(|index| ZeroExtent {
1715 start_page: (index as u64) * 2,
1716 end_page: (index as u64) * 2 + 1,
1717 })
1718 .collect::<Vec<_>>();
1719
1720 add_zero_extent(
1721 &mut extents,
1722 (MAX_ZERO_EXTENTS as u64) * 2 - 2,
1723 (MAX_ZERO_EXTENTS as u64) * 2,
1724 )
1725 .unwrap();
1726
1727 assert_eq!(extents.len(), MAX_ZERO_EXTENTS);
1728 assert_eq!(
1729 extents[MAX_ZERO_EXTENTS - 1],
1730 ZeroExtent {
1731 start_page: (MAX_ZERO_EXTENTS as u64) * 2 - 2,
1732 end_page: (MAX_ZERO_EXTENTS as u64) * 2
1733 }
1734 );
1735 }
1736
1737 #[test]
1738 fn zero_extent_limit_is_rejected_without_fallback() {
1739 let mut extents = (0..MAX_ZERO_EXTENTS)
1740 .map(|index| ZeroExtent {
1741 start_page: (index as u64) * 2,
1742 end_page: (index as u64) * 2 + 1,
1743 })
1744 .collect::<Vec<_>>();
1745 let result = add_zero_extent(
1746 &mut extents,
1747 (MAX_ZERO_EXTENTS as u64) * 2,
1748 (MAX_ZERO_EXTENTS as u64) * 2 + 1,
1749 );
1750 assert!(matches!(
1751 result,
1752 Err(StableMemoryError::ZeroExtentLimitExceeded {
1753 limit: MAX_ZERO_EXTENTS
1754 })
1755 ));
1756 }
1757
1758 #[test]
1759 fn pbt_zero_extent_normalizer_matches_independent_model() {
1760 let mut runner = TestRunner::new(Config {
1761 cases: 512,
1762 ..Config::default()
1763 });
1764
1765 runner
1766 .run(&zero_extent_vec_strategy(), |mut extents| {
1767 let expected = model_normalize_zero_extents(&extents);
1768 let result = normalize_zero_extents(&mut extents);
1769
1770 if expected.len() > MAX_ZERO_EXTENTS {
1771 let rejected = matches!(
1772 result,
1773 Err(StableMemoryError::ZeroExtentLimitExceeded {
1774 limit: MAX_ZERO_EXTENTS
1775 })
1776 );
1777 prop_assert!(rejected);
1778 } else {
1779 result.map_err(|error| TestCaseError::fail(error.to_string()))?;
1780 prop_assert_eq!(extents, expected);
1781 }
1782 Ok(())
1783 })
1784 .unwrap();
1785 }
1786
1787 #[test]
1788 fn pbt_zero_extent_add_subtract_matches_independent_model() {
1789 let mut runner = TestRunner::new(Config {
1790 cases: 256,
1791 ..Config::default()
1792 });
1793
1794 runner
1795 .run(&zero_extent_operation_sequence(), |operations| {
1796 let mut production = Vec::new();
1797 let mut model = Vec::new();
1798
1799 for operation in operations {
1800 let result = match operation {
1801 ZeroExtentOp::Add {
1802 start_page,
1803 end_page,
1804 } => {
1805 model_add_zero_extent(&mut model, start_page, end_page);
1806 add_zero_extent(&mut production, start_page, end_page)
1807 }
1808 ZeroExtentOp::Subtract {
1809 start_page,
1810 end_page,
1811 } => {
1812 model_subtract_zero_extent(&mut model, start_page, end_page);
1813 subtract_zero_extent(&mut production, start_page, end_page)
1814 }
1815 };
1816
1817 let expected = model_normalize_zero_extents(&model);
1818 if expected.len() > MAX_ZERO_EXTENTS {
1819 let rejected = matches!(
1820 result,
1821 Err(StableMemoryError::ZeroExtentLimitExceeded {
1822 limit: MAX_ZERO_EXTENTS
1823 })
1824 );
1825 prop_assert!(rejected);
1826 return Ok(());
1827 }
1828
1829 result.map_err(|error| TestCaseError::fail(error.to_string()))?;
1830 prop_assert_eq!(&production, &expected);
1831 model = expected;
1832 }
1833 Ok(())
1834 })
1835 .unwrap();
1836 }
1837
1838 #[test]
1839 #[serial_test::serial]
1840 fn pbt_noop_compact_preserves_sparse_page_model() {
1841 let mut runner = TestRunner::new(Config {
1842 cases: 32,
1843 ..Config::default()
1844 });
1845
1846 runner
1847 .run(
1848 &proptest::collection::vec(prop::option::of(any::<u8>()), 0..=300),
1849 |pages| {
1850 crate::stable::memory::reset_for_tests();
1851 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
1852
1853 let active_len = pages
1854 .iter()
1855 .rposition(Option::is_some)
1856 .map(|index| index + 1)
1857 .unwrap_or(0);
1858 for (page_no, byte) in pages.iter().take(active_len).enumerate() {
1859 if let Some(byte) = byte {
1860 write_at(
1861 u64::try_from(page_no).unwrap() * page_size(),
1862 &vec![*byte; page_len()],
1863 )
1864 .unwrap();
1865 }
1866 }
1867
1868 compact().unwrap();
1869 let block = Superblock::load().unwrap();
1870 prop_assert_eq!(
1871 block.db_size,
1872 u64::try_from(active_len).unwrap() * page_size()
1873 );
1874 prop_assert_eq!(block.page_count, active_page_count(&block).unwrap());
1875
1876 for (page_no, byte) in pages.iter().take(active_len).enumerate() {
1877 let mut page = vec![0_u8; page_len()];
1878 read_base_at(u64::try_from(page_no).unwrap() * page_size(), &mut page)
1879 .unwrap();
1880
1881 if let Some(byte) = byte {
1882 prop_assert_eq!(page, vec![*byte; page_len()]);
1883 } else {
1884 prop_assert_eq!(page, vec![0_u8; page_len()]);
1885 }
1886 }
1887 Ok(())
1888 },
1889 )
1890 .unwrap();
1891 }
1892
1893 #[derive(Clone, Debug)]
1894 enum BlobOp {
1895 Write { offset: u64, len: usize, byte: u8 },
1896 Truncate { size: u64 },
1897 Compact,
1898 }
1899
1900 #[derive(Clone, Debug)]
1901 enum ZeroExtentOp {
1902 Add { start_page: u64, end_page: u64 },
1903 Subtract { start_page: u64, end_page: u64 },
1904 }
1905
1906 fn zero_extent_vec_strategy() -> impl Strategy<Value = Vec<ZeroExtent>> {
1907 let page_limit = zero_extent_model_page_limit();
1908 proptest::collection::vec(
1909 (0_u64..=page_limit, 0_u64..=page_limit).prop_map(|(left, right)| ZeroExtent {
1910 start_page: left.min(right),
1911 end_page: left.max(right),
1912 }),
1913 0..=MAX_ZERO_EXTENTS + 8,
1914 )
1915 }
1916
1917 fn zero_extent_operation_sequence() -> impl Strategy<Value = Vec<ZeroExtentOp>> {
1918 let page_limit = zero_extent_model_page_limit();
1919 let range = (0_u64..=page_limit, 0_u64..=page_limit)
1920 .prop_map(|(left, right)| (left.min(right), left.max(right)));
1921 proptest::collection::vec(
1922 prop_oneof![
1923 range
1924 .clone()
1925 .prop_map(|(start_page, end_page)| ZeroExtentOp::Add {
1926 start_page,
1927 end_page,
1928 }),
1929 range.prop_map(|(start_page, end_page)| ZeroExtentOp::Subtract {
1930 start_page,
1931 end_page,
1932 }),
1933 ],
1934 0..=96,
1935 )
1936 }
1937
1938 fn zero_extent_model_page_limit() -> u64 {
1939 (u64::try_from(MAX_ZERO_EXTENTS).unwrap() + 8) * 2
1940 }
1941
1942 fn model_add_zero_extent(extents: &mut Vec<ZeroExtent>, start_page: u64, end_page: u64) {
1943 if start_page < end_page {
1944 extents.push(ZeroExtent {
1945 start_page,
1946 end_page,
1947 });
1948 }
1949 }
1950
1951 fn model_subtract_zero_extent(extents: &mut Vec<ZeroExtent>, start_page: u64, end_page: u64) {
1952 if start_page >= end_page {
1953 return;
1954 }
1955
1956 let mut next = Vec::new();
1957 for extent in extents.iter() {
1958 if end_page <= extent.start_page || start_page >= extent.end_page {
1959 next.push(extent.clone());
1960 continue;
1961 }
1962 if extent.start_page < start_page {
1963 next.push(ZeroExtent {
1964 start_page: extent.start_page,
1965 end_page: start_page,
1966 });
1967 }
1968 if end_page < extent.end_page {
1969 next.push(ZeroExtent {
1970 start_page: end_page,
1971 end_page: extent.end_page,
1972 });
1973 }
1974 }
1975 *extents = next;
1976 }
1977
1978 fn model_normalize_zero_extents(extents: &[ZeroExtent]) -> Vec<ZeroExtent> {
1979 let page_limit = usize::try_from(zero_extent_model_page_limit()).unwrap();
1980 let mut mask = vec![false; page_limit + 1];
1981 for extent in extents {
1982 let start = usize::try_from(extent.start_page).unwrap().min(mask.len());
1983 let end = usize::try_from(extent.end_page).unwrap().min(mask.len());
1984 for page in &mut mask[start..end] {
1985 *page = true;
1986 }
1987 }
1988
1989 let mut normalized = Vec::new();
1990 let mut page = 0_usize;
1991 while page < mask.len() {
1992 if !mask[page] {
1993 page += 1;
1994 continue;
1995 }
1996 let start = page;
1997 while page < mask.len() && mask[page] {
1998 page += 1;
1999 }
2000 normalized.push(ZeroExtent {
2001 start_page: u64::try_from(start).unwrap(),
2002 end_page: u64::try_from(page).unwrap(),
2003 });
2004 }
2005 normalized
2006 }
2007
2008 #[test]
2009 #[serial_test::serial]
2010 fn pbt_blob_operations_match_logical_model_across_noop_compact() {
2011 let mut runner = TestRunner::new(Config {
2012 cases: 48,
2013 ..Config::default()
2014 });
2015
2016 runner
2017 .run(&blob_operation_sequence(), |operations| {
2018 crate::stable::memory::reset_for_tests();
2019 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
2020
2021 let mut model = Vec::new();
2022 let mut materialized = BTreeSet::new();
2023 assert_blob_model(&model, &materialized, false)?;
2024
2025 for operation in operations {
2026 let compacted = apply_blob_op(operation, &mut model, &mut materialized)?;
2027 assert_blob_model(&model, &materialized, compacted)?;
2028 }
2029 Ok(())
2030 })
2031 .unwrap();
2032 }
2033
2034 fn blob_operation_sequence() -> impl Strategy<Value = Vec<BlobOp>> {
2035 let write = (blob_offset_strategy(), blob_len_strategy(), any::<u8>())
2036 .prop_map(|(offset, len, byte)| BlobOp::Write { offset, len, byte });
2037 let truncate = blob_offset_strategy().prop_map(|size| BlobOp::Truncate { size });
2038 proptest::collection::vec(prop_oneof![write, truncate, Just(BlobOp::Compact)], 0..=48)
2039 }
2040
2041 fn blob_offset_strategy() -> impl Strategy<Value = u64> {
2042 let limit = blob_model_limit();
2043 let page = page_size();
2044 let far_page = FAR_PAGE_NO * page;
2045 prop_oneof![
2046 0_u64..=limit,
2047 prop::sample::select(boundary_values(&[
2048 0,
2049 1,
2050 page - 1,
2051 page,
2052 page + 1,
2053 far_page - 1,
2054 far_page,
2055 far_page + 1,
2056 limit - 1,
2057 limit,
2058 ]))
2059 .prop_map(move |value| value.min(limit)),
2060 ]
2061 }
2062
2063 fn blob_len_strategy() -> impl Strategy<Value = usize> {
2064 prop_oneof![
2065 0_usize..=(page_len() * 2 + 17),
2066 prop::sample::select(vec![
2067 0,
2068 1,
2069 page_len() - 1,
2070 page_len(),
2071 page_len() + 1,
2072 page_len() * 2 + 1,
2073 ]),
2074 ]
2075 }
2076
2077 fn blob_model_limit() -> u64 {
2078 (FAR_PAGE_NO + 3) * page_size()
2079 }
2080
2081 fn apply_blob_op(
2082 operation: BlobOp,
2083 model: &mut Vec<u8>,
2084 materialized: &mut BTreeSet<u64>,
2085 ) -> Result<bool, TestCaseError> {
2086 match operation {
2087 BlobOp::Write { offset, len, byte } => {
2088 let len = len.min(usize::try_from(blob_model_limit() - offset).unwrap());
2089 let bytes = vec![byte; len];
2090 write_at(offset, &bytes).map_err(|error| TestCaseError::fail(error.to_string()))?;
2091 if len == 0 {
2092 return Ok(false);
2093 }
2094
2095 let start = usize::try_from(offset).unwrap();
2096 let end = start + len;
2097 if model.len() < start {
2098 model.resize(start, 0);
2099 }
2100 if model.len() < end {
2101 model.resize(end, 0);
2102 }
2103 model[start..end].copy_from_slice(&bytes);
2104 mark_materialized_range(offset, len, materialized);
2105 Ok(false)
2106 }
2107 BlobOp::Truncate { size } => {
2108 truncate(size).map_err(|error| TestCaseError::fail(error.to_string()))?;
2109 let new_len = usize::try_from(size).unwrap();
2110 model.resize(new_len, 0);
2111 let active_pages = page_count_for_size(size)
2112 .map_err(|error| TestCaseError::fail(error.to_string()))?;
2113 materialized.retain(|page_no| *page_no < active_pages);
2114 if size > 0 && !size.is_multiple_of(page_size()) {
2115 materialized.insert(size / page_size());
2116 }
2117 Ok(false)
2118 }
2119 BlobOp::Compact => {
2120 compact().map_err(|error| TestCaseError::fail(error.to_string()))?;
2121 Ok(true)
2122 }
2123 }
2124 }
2125
2126 fn mark_materialized_range(offset: u64, len: usize, materialized: &mut BTreeSet<u64>) {
2127 let end = offset + u64::try_from(len).unwrap();
2128 let first_page = offset / page_size();
2129 let last_page = (end - 1) / page_size();
2130 for page_no in first_page..=last_page {
2131 materialized.insert(page_no);
2132 }
2133 }
2134
2135 fn assert_blob_model(
2136 model: &[u8],
2137 _materialized: &BTreeSet<u64>,
2138 _expect_compacted: bool,
2139 ) -> Result<(), TestCaseError> {
2140 let block = Superblock::load().map_err(|error| TestCaseError::fail(error.to_string()))?;
2141 prop_assert_eq!(block.db_size, u64::try_from(model.len()).unwrap());
2142
2143 if !model.is_empty() {
2144 let mut out = vec![0_u8; model.len()];
2145 read_base_at(0, &mut out).map_err(|error| TestCaseError::fail(error.to_string()))?;
2146 prop_assert_eq!(out, model);
2147 }
2148
2149 let mut tail = vec![1_u8; 32];
2150 read_base_at(u64::try_from(model.len()).unwrap(), &mut tail)
2151 .map_err(|error| TestCaseError::fail(error.to_string()))?;
2152 prop_assert_eq!(tail, vec![0_u8; 32]);
2153
2154 let active_pages = page_count_for_size(u64::try_from(model.len()).unwrap())
2155 .map_err(|error| TestCaseError::fail(error.to_string()))?;
2156 prop_assert_eq!(block.page_count, active_pages);
2157 Ok(())
2158 }
2159
2160 #[test]
2161 #[serial_test::serial]
2162 fn read_metrics_track_in_place_data_reads() {
2163 crate::stable::memory::reset_for_tests();
2164 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
2165
2166 let page = vec![7_u8; page_len()];
2167 write_at(0, &page).unwrap();
2168 crate::read_metrics::reset_read_metrics();
2169
2170 let first = read_base_page(0).unwrap();
2171 let second = read_base_page(0).unwrap();
2172 let metrics = crate::read_metrics::read_metrics_snapshot();
2173
2174 assert_eq!(first, page);
2175 assert_eq!(second, page);
2176 assert!(metrics.stable_data_read_calls >= 2);
2177 assert!(metrics.stable_data_read_bytes >= page_size() * 2);
2178 #[cfg(feature = "bench-profile")]
2179 assert!(metrics.superblock_loads <= 1);
2180 #[cfg(not(feature = "bench-profile"))]
2181 assert_eq!(metrics.superblock_loads, 0);
2182 }
2183
2184 #[test]
2185 #[serial_test::serial]
2186 fn page_offset_cache_reuses_page_data_for_small_reads() {
2187 crate::stable::memory::reset_for_tests();
2188 crate::stable::memory::init(crate::stable::memory::memory_for_tests()).unwrap();
2189
2190 let page = vec![9_u8; page_len()];
2191 write_at(0, &page).unwrap();
2192 let block = Superblock::load().unwrap();
2193 let mut cache = PageOffsetCache::new();
2194 let mut first = [0_u8; 16];
2195 let mut second = [0_u8; 16];
2196
2197 crate::read_metrics::reset_read_metrics();
2198 read_base_at_with_page_cache(&block, 0, &mut first, &mut cache).unwrap();
2199 read_base_at_with_page_cache(&block, 8, &mut second, &mut cache).unwrap();
2200 let metrics = crate::read_metrics::read_metrics_snapshot();
2201
2202 assert_eq!(first, [9_u8; 16]);
2203 assert_eq!(second, [9_u8; 16]);
2204 assert_eq!(metrics.stable_data_read_calls, 1);
2205 assert_eq!(metrics.stable_data_read_bytes, page_size());
2206 }
2207}