cuprate_database/resize.rs
1//! Database memory map resizing algorithms.
2//!
3//! This modules contains [`ResizeAlgorithm`] which determines how the
4//! [`ConcreteEnv`](crate::ConcreteEnv) resizes its memory map when needing more space.
5//! This value is in [`Config`](crate::config::Config) and can be selected at runtime.
6//!
7//! Although, it is only used by `ConcreteEnv` if [`Env::MANUAL_RESIZE`](crate::env::Env::MANUAL_RESIZE) is `true`.
8//!
9//! The algorithms are available as free functions in this module as well.
10//!
11//! # Page size
12//! All free functions in this module will
13//! return a multiple of the OS page size ([`PAGE_SIZE`]),
14//! [LMDB will error](http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5)
15//! if this is not the case.
16//!
17//! # Invariants
18//! All returned [`NonZeroUsize`] values of the free functions in this module
19//! (including [`ResizeAlgorithm::resize`]) uphold the following invariants:
20//! 1. It will always be `>=` the input `current_size_bytes`
21//! 2. It will always be a multiple of [`PAGE_SIZE`]
22
23//---------------------------------------------------------------------------------------------------- Import
24use std::{num::NonZeroUsize, sync::LazyLock};
25
26//---------------------------------------------------------------------------------------------------- ResizeAlgorithm
27/// The function/algorithm used by the
28/// database when resizing the memory map.
29///
30// # SOMEDAY
31// We could test around with different algorithms.
32// Calling `heed::Env::resize` is surprisingly fast,
33// around `0.0000082s` on my machine. We could probably
34// get away with smaller and more frequent resizes.
35// **With the caveat being we are taking a `WriteGuard` to a `RwLock`.**
36#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub enum ResizeAlgorithm {
39 /// Uses [`monero`].
40 Monero,
41
42 /// Uses [`fixed_bytes`].
43 FixedBytes(NonZeroUsize),
44
45 /// Uses [`percent`].
46 Percent(f32),
47}
48
49impl ResizeAlgorithm {
50 /// Returns [`Self::Monero`].
51 ///
52 /// ```rust
53 /// # use cuprate_database::resize::*;
54 /// assert!(matches!(ResizeAlgorithm::new(), ResizeAlgorithm::Monero));
55 /// ```
56 #[inline]
57 pub const fn new() -> Self {
58 Self::Monero
59 }
60
61 /// Maps the `self` variant to the free functions in [`crate::resize`].
62 ///
63 /// This function returns the _new_ memory map size in bytes.
64 #[inline]
65 pub fn resize(&self, current_size_bytes: usize) -> NonZeroUsize {
66 match self {
67 Self::Monero => monero(current_size_bytes),
68 Self::FixedBytes(add_bytes) => fixed_bytes(current_size_bytes, add_bytes.get()),
69 Self::Percent(f) => percent(current_size_bytes, *f),
70 }
71 }
72}
73
74impl Default for ResizeAlgorithm {
75 /// Calls [`Self::new`].
76 ///
77 /// ```rust
78 /// # use cuprate_database::resize::*;
79 /// assert_eq!(ResizeAlgorithm::new(), ResizeAlgorithm::default());
80 /// ```
81 #[inline]
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87//---------------------------------------------------------------------------------------------------- Free functions
88/// This retrieves the system’s memory page size.
89///
90/// It is just [`page_size::get`](https://docs.rs/page_size) internally.
91///
92/// # Panics
93/// Accessing this [`LazyLock`] will panic if the OS returns of page size of `0` (impossible?).
94pub static PAGE_SIZE: LazyLock<NonZeroUsize> =
95 LazyLock::new(|| NonZeroUsize::new(page_size::get()).expect("page_size::get() returned 0"));
96
97/// Memory map resize closely matching `monerod`.
98///
99/// # Method
100/// This function mostly matches `monerod`'s current resize implementation[^1],
101/// and will increase `current_size_bytes` by `1 << 30`[^2] exactly then
102/// rounded to the nearest multiple of the OS page size.
103///
104/// [^1]: <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L549>
105///
106/// [^2]: `1_073_745_920`
107///
108/// ```rust
109/// # use cuprate_database::resize::*;
110/// // The value this function will increment by
111/// // (assuming page multiple of 4096).
112/// const N: usize = 1_073_741_824;
113///
114/// // 0 returns the minimum value.
115/// assert_eq!(monero(0).get(), N);
116///
117/// // Rounds up to nearest OS page size.
118/// assert_eq!(monero(1).get(), N + PAGE_SIZE.get());
119/// ```
120///
121/// # Panics
122/// This function will panic if adding onto `current_size_bytes` overflows [`usize::MAX`].
123///
124/// ```rust,should_panic
125/// # use cuprate_database::resize::*;
126/// // Ridiculous large numbers panic.
127/// monero(usize::MAX);
128/// ```
129pub fn monero(current_size_bytes: usize) -> NonZeroUsize {
130 /// The exact expression used by `monerod`
131 /// when calculating how many bytes to add.
132 ///
133 /// The nominal value is `1_073_741_824`.
134 /// Not actually 1 GB but close enough I guess.
135 ///
136 /// <https://github.com/monero-project/monero/blob/059028a30a8ae9752338a7897329fe8012a310d5/src/blockchain_db/lmdb/db_lmdb.cpp#L553>
137 const ADD_SIZE: usize = 1_usize << 30;
138
139 let page_size = PAGE_SIZE.get();
140 let new_size_bytes = current_size_bytes + ADD_SIZE;
141
142 // Round up the new size to the
143 // nearest multiple of the OS page size.
144 let remainder = new_size_bytes % page_size;
145
146 // INVARIANT: minimum is always at least `ADD_SIZE`.
147 NonZeroUsize::new(if remainder == 0 {
148 new_size_bytes
149 } else {
150 (new_size_bytes + page_size) - remainder
151 })
152 .unwrap()
153}
154
155/// Memory map resize by a fixed amount of bytes.
156///
157/// # Method
158/// This function will `current_size_bytes + add_bytes`
159/// and then round up to nearest OS page size.
160///
161/// ```rust
162/// # use cuprate_database::resize::*;
163/// let page_size: usize = PAGE_SIZE.get();
164///
165/// // Anything below the page size will round up to the page size.
166/// for i in 0..=page_size {
167/// assert_eq!(fixed_bytes(0, i).get(), page_size);
168/// }
169///
170/// // (page_size + 1) will round up to (page_size * 2).
171/// assert_eq!(fixed_bytes(page_size, 1).get(), page_size * 2);
172///
173/// // (page_size + page_size) doesn't require any rounding.
174/// assert_eq!(fixed_bytes(page_size, page_size).get(), page_size * 2);
175/// ```
176///
177/// # Panics
178/// This function will panic if adding onto `current_size_bytes` overflows [`usize::MAX`].
179///
180/// ```rust,should_panic
181/// # use cuprate_database::resize::*;
182/// // Ridiculous large numbers panic.
183/// fixed_bytes(1, usize::MAX);
184/// ```
185pub fn fixed_bytes(current_size_bytes: usize, add_bytes: usize) -> NonZeroUsize {
186 let page_size = *PAGE_SIZE;
187 let new_size_bytes = current_size_bytes + add_bytes;
188
189 // Guard against < page_size.
190 if new_size_bytes <= page_size.get() {
191 return page_size;
192 }
193
194 // Round up the new size to the
195 // nearest multiple of the OS page size.
196 let remainder = new_size_bytes % page_size;
197
198 // INVARIANT: we guarded against < page_size above.
199 NonZeroUsize::new(if remainder == 0 {
200 new_size_bytes
201 } else {
202 (new_size_bytes + page_size.get()) - remainder
203 })
204 .unwrap()
205}
206
207/// Memory map resize by a percentage.
208///
209/// # Method
210/// This function will multiply `current_size_bytes` by `percent`.
211///
212/// Any input `<= 1.0` or non-normal float ([`f32::NAN`], [`f32::INFINITY`])
213/// will make the returning `NonZeroUsize` the same as `current_size_bytes`
214/// (rounded up to the OS page size).
215///
216/// ```rust
217/// # use cuprate_database::resize::*;
218/// let page_size: usize = PAGE_SIZE.get();
219///
220/// // Anything below the page size will round up to the page size.
221/// for i in 0..=page_size {
222/// assert_eq!(percent(i, 1.0).get(), page_size);
223/// }
224///
225/// // Same for 2 page sizes.
226/// for i in (page_size + 1)..=(page_size * 2) {
227/// assert_eq!(percent(i, 1.0).get(), page_size * 2);
228/// }
229///
230/// // Weird floats do nothing.
231/// assert_eq!(percent(page_size, f32::NAN).get(), page_size);
232/// assert_eq!(percent(page_size, f32::INFINITY).get(), page_size);
233/// assert_eq!(percent(page_size, f32::NEG_INFINITY).get(), page_size);
234/// assert_eq!(percent(page_size, -1.0).get(), page_size);
235/// assert_eq!(percent(page_size, 0.999).get(), page_size);
236/// ```
237///
238/// # Panics
239/// This function will panic if `current_size_bytes * percent`
240/// is closer to [`usize::MAX`] than the OS page size.
241///
242/// ```rust,should_panic
243/// # use cuprate_database::resize::*;
244/// // Ridiculous large numbers panic.
245/// percent(usize::MAX, 1.001);
246/// ```
247pub fn percent(current_size_bytes: usize, percent: f32) -> NonZeroUsize {
248 // Guard against bad floats.
249 use std::num::FpCategory;
250 let percent = match percent.classify() {
251 FpCategory::Normal => {
252 if percent <= 1.0 {
253 1.0
254 } else {
255 percent
256 }
257 }
258 _ => 1.0,
259 };
260
261 let page_size = *PAGE_SIZE;
262
263 // INVARIANT: Allow `f32` <-> `usize` casting, we handle all cases.
264 #[expect(
265 clippy::cast_possible_truncation,
266 clippy::cast_sign_loss,
267 clippy::cast_precision_loss
268 )]
269 let new_size_bytes = ((current_size_bytes as f32) * percent) as usize;
270
271 // Panic if rounding up to the nearest page size would overflow.
272 let new_size_bytes = if new_size_bytes > (usize::MAX - page_size.get()) {
273 panic!("new_size_bytes is percent() near usize::MAX");
274 } else {
275 new_size_bytes
276 };
277
278 // Guard against < page_size.
279 if new_size_bytes <= page_size.get() {
280 return page_size;
281 }
282
283 // Round up the new size to the
284 // nearest multiple of the OS page size.
285 let remainder = new_size_bytes % page_size;
286
287 // INVARIANT: we guarded against < page_size above.
288 NonZeroUsize::new(if remainder == 0 {
289 new_size_bytes
290 } else {
291 (new_size_bytes + page_size.get()) - remainder
292 })
293 .unwrap()
294}
295
296//---------------------------------------------------------------------------------------------------- Tests
297#[cfg(test)]
298mod test {
299 // use super::*;
300}