Skip to main content

reddb_server/storage/btree/
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//! reddb's MVCC version chains live in the btree itself
55//! (`storage/btree/node.rs:300`), not in row headers, so the
56//! "page" concept here is an *abstract* page — call sites use
57//! the entity ID divided by `entries_per_page` (~256 for 8 KB
58//! pages) as the page index.
59
60use std::sync::RwLock;
61
62/// Per-page visibility tracking bitmap. Pages are indexed by a
63/// dense u32 — sparse table sizes can overshoot the bitmap and
64/// trigger lazy resize via `ensure_capacity`.
65pub struct VisibilityMap {
66    /// Bit-packed visibility bits, indexed by page number.
67    /// `bits[i / 64] & (1 << (i % 64))` set means page `i` is
68    /// all-visible.
69    bits: RwLock<Vec<u64>>,
70}
71
72impl VisibilityMap {
73    /// Create an empty visibility map. Initial capacity is
74    /// minimal; pages get added as `mark_all_visible` extends
75    /// the bitmap.
76    pub fn new() -> Self {
77        Self {
78            bits: RwLock::new(Vec::new()),
79        }
80    }
81
82    /// Pre-allocate room for `pages` pages. Useful when the
83    /// caller knows the table size up-front (e.g. ANALYZE
84    /// freshly imported data).
85    pub fn with_capacity(pages: u32) -> Self {
86        let words = (pages as usize).div_ceil(64);
87        Self {
88            bits: RwLock::new(vec![0u64; words]),
89        }
90    }
91
92    /// Returns true when `page` is marked all-visible. Page
93    /// indexes beyond the current bitmap return false (treated
94    /// as "unknown / not-visible").
95    pub fn is_all_visible(&self, page: u32) -> bool {
96        let bits = self.bits.read().expect("vmap rwlock poisoned");
97        let word_idx = page as usize / 64;
98        if word_idx >= bits.len() {
99            return false;
100        }
101        let bit_idx = page as usize % 64;
102        (bits[word_idx] >> bit_idx) & 1 == 1
103    }
104
105    /// Mark `page` as all-visible. Extends the bitmap on demand.
106    /// Idempotent — calling twice has the same effect as calling
107    /// once.
108    pub fn mark_all_visible(&self, page: u32) {
109        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
110        let word_idx = page as usize / 64;
111        if word_idx >= bits.len() {
112            bits.resize(word_idx + 1, 0);
113        }
114        let bit_idx = page as usize % 64;
115        bits[word_idx] |= 1u64 << bit_idx;
116    }
117
118    /// Clear the all-visible bit for `page`. Called by every
119    /// write path that touches the page (insert / update /
120    /// delete). Cheap no-op when the page wasn't marked or
121    /// doesn't exist in the bitmap yet.
122    pub fn clear_all_visible(&self, page: u32) {
123        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
124        let word_idx = page as usize / 64;
125        if word_idx >= bits.len() {
126            // Page doesn't exist yet — implicit clear, nothing
127            // to do.
128            return;
129        }
130        let bit_idx = page as usize % 64;
131        bits[word_idx] &= !(1u64 << bit_idx);
132    }
133
134    /// Number of all-visible pages currently tracked.
135    pub fn all_visible_count(&self) -> u64 {
136        let bits = self.bits.read().expect("vmap rwlock poisoned");
137        bits.iter().map(|w| w.count_ones() as u64).sum()
138    }
139
140    /// Total number of pages the bitmap can address (capacity,
141    /// not "set count"). Mostly useful for diagnostics and
142    /// memory accounting.
143    pub fn capacity_pages(&self) -> u64 {
144        let bits = self.bits.read().expect("vmap rwlock poisoned");
145        (bits.len() as u64) * 64
146    }
147
148    /// Reset the entire bitmap to "not all-visible". Used by
149    /// crash recovery and DROP TABLE.
150    pub fn clear(&self) {
151        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
152        for w in bits.iter_mut() {
153            *w = 0;
154        }
155    }
156
157    /// Bulk-mark a contiguous range of pages [`start`, `end`)
158    /// as all-visible. Used by VACUUM after a successful sweep
159    /// of a page range.
160    pub fn mark_range_visible(&self, start: u32, end: u32) {
161        if start >= end {
162            return;
163        }
164        let mut bits = self.bits.write().expect("vmap rwlock poisoned");
165        let last_word = (end as usize - 1) / 64;
166        if last_word >= bits.len() {
167            bits.resize(last_word + 1, 0);
168        }
169        for page in start..end {
170            let word_idx = page as usize / 64;
171            let bit_idx = page as usize % 64;
172            bits[word_idx] |= 1u64 << bit_idx;
173        }
174    }
175
176    /// Iterate over (page, all_visible_bool) for the first
177    /// `limit_pages` pages. Diagnostic / debugging helper.
178    pub fn snapshot(&self, limit_pages: u32) -> Vec<(u32, bool)> {
179        let bits = self.bits.read().expect("vmap rwlock poisoned");
180        let mut out = Vec::with_capacity(limit_pages as usize);
181        for page in 0..limit_pages {
182            let word_idx = page as usize / 64;
183            let visible = if word_idx < bits.len() {
184                let bit_idx = page as usize % 64;
185                (bits[word_idx] >> bit_idx) & 1 == 1
186            } else {
187                false
188            };
189            out.push((page, visible));
190        }
191        out
192    }
193}
194
195impl Default for VisibilityMap {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201/// Helper: convert an entity ID to a page index using the given
202/// `rows_per_page` constant. The btree's MVCC version chain
203/// doesn't actually map onto fixed-size pages, so this is an
204/// abstraction layer that lets the planner reason about
205/// "page-shaped" visibility regions without committing to a
206/// specific physical layout.
207pub fn page_of(entity_id: u64, rows_per_page: u32) -> u32 {
208    if rows_per_page == 0 {
209        return 0;
210    }
211    (entity_id / rows_per_page as u64) as u32
212}
213
214/// Phase 3.5 wiring callback. The btree write path calls this
215/// after every insert / update / delete to clear the all-visible
216/// bit for the affected page. Centralised so a single function
217/// can be hooked into multiple write call sites without each
218/// rewriting the page-of math.
219pub fn mark_dirty_after_write(vmap: &VisibilityMap, entity_id: u64, rows_per_page: u32) {
220    let page = page_of(entity_id, rows_per_page);
221    vmap.clear_all_visible(page);
222}
223
224/// Phase 3.5 wiring callback for VACUUM / GC. After confirming
225/// every row in a page range is visible to all active txns, the
226/// GC sweeps this with the (start, end) page bounds.
227pub fn mark_clean_after_gc(vmap: &VisibilityMap, start_page: u32, end_page: u32) {
228    vmap.mark_range_visible(start_page, end_page);
229}