Skip to main content

reddb_engine/storage/
visibility_map.rs

1//! Visibility map — Fase 5 P2 building block.
2//!
3//! Provides a `VisibilityMap` data structure that tracks per-page
4//! "all-visible" status: a single bit per heap page recording
5//! whether every row on that page is visible to every concurrent
6//! transaction. When the bit is set, the planner can answer
7//! certain queries without ever fetching the heap row — index-
8//! only scans return the indexed columns directly.
9//!
10//! Mirrors PG's `visibilitymap.c` modulo features we don't have:
11//!
12//! - **Crash recovery**: PG's vmap is durable via WAL. Ours is
13//!   memory-only for now; on restart every page resets to
14//!   "not all-visible" and gets re-marked lazily as queries
15//!   verify rows.
16//! - **All-frozen bit**: PG tracks a second bit per page
17//!   ("all-frozen") used by the freeze map for vacuum
18//!   optimisation. We omit it — reddb doesn't have freeze
19//!   semantics yet.
20//! - **Concurrent updates**: PG uses LWLocks per buffer slot
21//!   for fine-grained concurrency. Ours uses a single RwLock
22//!   over the bitmap. Acceptable for Week 5; later weeks
23//!   shard by page range when contention shows up.
24//!
25//! ## Why it matters
26//!
27//! Index-only scans are the killer perf win for "narrow"
28//! queries that select only indexed columns:
29//!
30//! ```text
31//! SELECT user_id FROM users WHERE email = ?
32//! ```
33//!
34//! With an index on `email` covering `user_id`, the planner can
35//! skip the heap fetch entirely. But that's only safe when the
36//! tuple's MVCC visibility hasn't changed since the last vacuum
37//! — i.e. the page is "all-visible".
38//!
39//! Without a vmap, every index-only scan candidate must double-
40//! check by fetching the heap row anyway, defeating the point.
41//! With a vmap, the per-page bit is a single memory access.
42//!
43//! ## Marking strategy
44//!
45//! - **Set** the all-visible bit when:
46//!   - VACUUM determines every row on the page is visible to
47//!     every active xmin (no in-flight inserts / updates).
48//!   - A bulk-load operation atomically writes a new page where
49//!     every tuple is committed.
50//! - **Clear** the all-visible bit when:
51//!   - Any tuple on the page is updated, deleted, or inserted.
52//!   - Even a no-op WAL replay: clearing is conservative.
53//!
54//! The unified store's MVCC version chains are not tied to fixed-size
55//! heap pages, so the "page" concept here is an *abstract* page: call sites use
56//! the entity ID divided by `entries_per_page` (~256 for 8 KB
57//! pages) as the page index.
58
59use std::sync::RwLock;
60
61/// Per-page visibility tracking bitmap. Pages are indexed by a
62/// dense u32 — sparse table sizes can overshoot the bitmap and
63/// trigger lazy resize via `ensure_capacity`.
64pub struct VisibilityMap {
65    /// Bit-packed visibility bits, indexed by page number.
66    /// `bits[i / 64] & (1 << (i % 64))` set means page `i` is
67    /// all-visible.
68    bits: RwLock<Vec<u64>>,
69}
70
71impl VisibilityMap {
72    /// Create an empty visibility map. Initial capacity is
73    /// minimal; pages get added as `mark_all_visible` extends
74    /// the bitmap.
75    pub fn new() -> Self {
76        Self {
77            bits: RwLock::new(Vec::new()),
78        }
79    }
80
81    /// Pre-allocate room for `pages` pages. Useful when the
82    /// caller knows the table size up-front (e.g. ANALYZE
83    /// freshly imported data).
84    pub fn with_capacity(pages: u32) -> Self {
85        let words = (pages as usize).div_ceil(64);
86        Self {
87            bits: RwLock::new(vec![0u64; words]),
88        }
89    }
90
91    /// Returns true when `page` is marked all-visible. Page
92    /// indexes beyond the current bitmap return false (treated
93    /// as "unknown / not-visible").
94    pub fn is_all_visible(&self, page: u32) -> bool {
95        let bits = self.bits.read().expect("vmap rwlock poisoned");
96        let word_idx = page as usize / 64;
97        if word_idx >= bits.len() {
98            return false;
99        }
100        let bit_idx = page as usize % 64;
101        (bits[word_idx] >> bit_idx) & 1 == 1
102    }
103
104    /// Mark `page` as all-visible. Extends the bitmap on demand.
105    /// Idempotent — calling twice has the same effect as calling
106    /// once.
107    pub fn mark_all_visible(&self, page: u32) {
108        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
109        let word_idx = page as usize / 64;
110        if word_idx >= bits.len() {
111            bits.resize(word_idx + 1, 0);
112        }
113        let bit_idx = page as usize % 64;
114        bits[word_idx] |= 1u64 << bit_idx;
115    }
116
117    /// Clear the all-visible bit for `page`. Called by every
118    /// write path that touches the page (insert / update /
119    /// delete). Cheap no-op when the page wasn't marked or
120    /// doesn't exist in the bitmap yet.
121    pub fn clear_all_visible(&self, page: u32) {
122        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
123        let word_idx = page as usize / 64;
124        if word_idx >= bits.len() {
125            // Page doesn't exist yet — implicit clear, nothing
126            // to do.
127            return;
128        }
129        let bit_idx = page as usize % 64;
130        bits[word_idx] &= !(1u64 << bit_idx);
131    }
132
133    /// Number of all-visible pages currently tracked.
134    pub fn all_visible_count(&self) -> u64 {
135        let bits = self.bits.read().expect("vmap rwlock poisoned");
136        bits.iter().map(|w| w.count_ones() as u64).sum()
137    }
138
139    /// Total number of pages the bitmap can address (capacity,
140    /// not "set count"). Mostly useful for diagnostics and
141    /// memory accounting.
142    pub fn capacity_pages(&self) -> u64 {
143        let bits = self.bits.read().expect("vmap rwlock poisoned");
144        (bits.len() as u64) * 64
145    }
146
147    /// Reset the entire bitmap to "not all-visible". Used by
148    /// crash recovery and DROP TABLE.
149    pub fn clear(&self) {
150        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
151        for w in bits.iter_mut() {
152            *w = 0;
153        }
154    }
155
156    /// Bulk-mark a contiguous range of pages [`start`, `end`)
157    /// as all-visible. Used by VACUUM after a successful sweep
158    /// of a page range.
159    pub fn mark_range_visible(&self, start: u32, end: u32) {
160        if start >= end {
161            return;
162        }
163        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
164        let last_word = (end as usize - 1) / 64;
165        if last_word >= bits.len() {
166            bits.resize(last_word + 1, 0);
167        }
168        for page in start..end {
169            let word_idx = page as usize / 64;
170            let bit_idx = page as usize % 64;
171            bits[word_idx] |= 1u64 << bit_idx;
172        }
173    }
174
175    /// Iterate over (page, all_visible_bool) for the first
176    /// `limit_pages` pages. Diagnostic / debugging helper.
177    pub fn snapshot(&self, limit_pages: u32) -> Vec<(u32, bool)> {
178        let bits = self.bits.read().expect("vmap rwlock poisoned");
179        let mut out = Vec::with_capacity(limit_pages as usize);
180        for page in 0..limit_pages {
181            let word_idx = page as usize / 64;
182            let visible = if word_idx < bits.len() {
183                let bit_idx = page as usize % 64;
184                (bits[word_idx] >> bit_idx) & 1 == 1
185            } else {
186                false
187            };
188            out.push((page, visible));
189        }
190        out
191    }
192}
193
194impl Default for VisibilityMap {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200/// Helper: convert an entity ID to a page index using the given
201/// `rows_per_page` constant. The unified store's MVCC version chain
202/// does not map onto fixed-size pages, so this is an
203/// abstraction layer that lets the planner reason about
204/// "page-shaped" visibility regions without committing to a
205/// specific physical layout.
206pub fn page_of(entity_id: u64, rows_per_page: u32) -> u32 {
207    if rows_per_page == 0 {
208        return 0;
209    }
210    (entity_id / rows_per_page as u64) as u32
211}
212
213/// Phase 3.5 wiring callback. The unified write path calls this
214/// after every insert / update / delete to clear the all-visible
215/// bit for the affected page. Centralised so a single function
216/// can be hooked into multiple write call sites without each
217/// rewriting the page-of math.
218pub fn mark_dirty_after_write(vmap: &VisibilityMap, entity_id: u64, rows_per_page: u32) {
219    let page = page_of(entity_id, rows_per_page);
220    vmap.clear_all_visible(page);
221}
222
223/// Phase 3.5 wiring callback for VACUUM / GC. After confirming
224/// every row in a page range is visible to all active txns, the
225/// GC sweeps this with the (start, end) page bounds.
226pub fn mark_clean_after_gc(vmap: &VisibilityMap, start_page: u32, end_page: u32) {
227    vmap.mark_range_visible(start_page, end_page);
228}