//! Database memory map resizing algorithms. //! //! This modules contains [`ResizeAlgorithm`] which determines how the //! [`ConcreteEnv`](crate::ConcreteEnv) resizes its memory map when needing more space. //! This value is in [`Config`](crate::config::Config) and can be selected at runtime. //! //! Although, it is only used by `ConcreteEnv` if [`Env::MANUAL_RESIZE`](crate::env::Env::MANUAL_RESIZE) is `true`. //! //! The algorithms are available as free functions in this module as well. //! //! # Page size //! All free functions in this module will //! return a multiple of the OS page size ([`PAGE_SIZE`]), //! [LMDB will error](http://www.lmdb.tech/doc/group__mdb.html#gaa2506ec8dab3d969b0e609cd82e619e5) //! if this is not the case. //! //! # Invariants //! All returned [`NonZeroUsize`] values of the free functions in this module //! (including [`ResizeAlgorithm::resize`]) uphold the following invariants: //! 1. It will always be `>=` the input `current_size_bytes` //! 2. It will always be a multiple of [`PAGE_SIZE`] //---------------------------------------------------------------------------------------------------- Import use std::{num::NonZeroUsize, sync::LazyLock}; //---------------------------------------------------------------------------------------------------- ResizeAlgorithm /// The function/algorithm used by the /// database when resizing the memory map. /// // # SOMEDAY // We could test around with different algorithms. // Calling `heed::Env::resize` is surprisingly fast, // around `0.0000082s` on my machine. We could probably // get away with smaller and more frequent resizes. // **With the caveat being we are taking a `WriteGuard` to a `RwLock`.** #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ResizeAlgorithm { /// Uses [`monero`]. Monero, /// Uses [`fixed_bytes`]. FixedBytes(NonZeroUsize), /// Uses [`percent`]. Percent(f32), } impl ResizeAlgorithm { /// Returns [`Self::Monero`]. /// /// ```rust /// # use cuprate_database::resize::*; /// assert!(matches!(ResizeAlgorithm::new(), ResizeAlgorithm::Monero)); /// ``` #[inline] pub const fn new() -> Self { Self::Monero } /// Maps the `self` variant to the free functions in [`crate::resize`]. /// /// This function returns the _new_ memory map size in bytes. #[inline] pub fn resize(&self, current_size_bytes: usize) -> NonZeroUsize { match self { Self::Monero => monero(current_size_bytes), Self::FixedBytes(add_bytes) => fixed_bytes(current_size_bytes, add_bytes.get()), Self::Percent(f) => percent(current_size_bytes, *f), } } } impl Default for ResizeAlgorithm { /// Calls [`Self::new`]. /// /// ```rust /// # use cuprate_database::resize::*; /// assert_eq!(ResizeAlgorithm::new(), ResizeAlgorithm::default()); /// ``` #[inline] fn default() -> Self { Self::new() } } //---------------------------------------------------------------------------------------------------- Free functions /// This retrieves the system’s memory page size. /// /// It is just [`page_size::get`](https://docs.rs/page_size) internally. /// /// # Panics /// Accessing this [`LazyLock`] will panic if the OS returns of page size of `0` (impossible?). pub static PAGE_SIZE: LazyLock = LazyLock::new(|| NonZeroUsize::new(page_size::get()).expect("page_size::get() returned 0")); /// Memory map resize closely matching `monerod`. /// /// # Method /// This function mostly matches `monerod`'s current resize implementation[^1], /// and will increase `current_size_bytes` by `1 << 30`[^2] exactly then /// rounded to the nearest multiple of the OS page size. /// /// [^1]: /// /// [^2]: `1_073_745_920` /// /// ```rust /// # use cuprate_database::resize::*; /// // The value this function will increment by /// // (assuming page multiple of 4096). /// const N: usize = 1_073_741_824; /// /// // 0 returns the minimum value. /// assert_eq!(monero(0).get(), N); /// /// // Rounds up to nearest OS page size. /// assert_eq!(monero(1).get(), N + PAGE_SIZE.get()); /// ``` /// /// # Panics /// This function will panic if adding onto `current_size_bytes` overflows [`usize::MAX`]. /// /// ```rust,should_panic /// # use cuprate_database::resize::*; /// // Ridiculous large numbers panic. /// monero(usize::MAX); /// ``` pub fn monero(current_size_bytes: usize) -> NonZeroUsize { /// The exact expression used by `monerod` /// when calculating how many bytes to add. /// /// The nominal value is `1_073_741_824`. /// Not actually 1 GB but close enough I guess. /// /// const ADD_SIZE: usize = 1_usize << 30; let page_size = PAGE_SIZE.get(); let new_size_bytes = current_size_bytes + ADD_SIZE; // Round up the new size to the // nearest multiple of the OS page size. let remainder = new_size_bytes % page_size; // INVARIANT: minimum is always at least `ADD_SIZE`. NonZeroUsize::new(if remainder == 0 { new_size_bytes } else { (new_size_bytes + page_size) - remainder }) .unwrap() } /// Memory map resize by a fixed amount of bytes. /// /// # Method /// This function will `current_size_bytes + add_bytes` /// and then round up to nearest OS page size. /// /// ```rust /// # use cuprate_database::resize::*; /// let page_size: usize = PAGE_SIZE.get(); /// /// // Anything below the page size will round up to the page size. /// for i in 0..=page_size { /// assert_eq!(fixed_bytes(0, i).get(), page_size); /// } /// /// // (page_size + 1) will round up to (page_size * 2). /// assert_eq!(fixed_bytes(page_size, 1).get(), page_size * 2); /// /// // (page_size + page_size) doesn't require any rounding. /// assert_eq!(fixed_bytes(page_size, page_size).get(), page_size * 2); /// ``` /// /// # Panics /// This function will panic if adding onto `current_size_bytes` overflows [`usize::MAX`]. /// /// ```rust,should_panic /// # use cuprate_database::resize::*; /// // Ridiculous large numbers panic. /// fixed_bytes(1, usize::MAX); /// ``` pub fn fixed_bytes(current_size_bytes: usize, add_bytes: usize) -> NonZeroUsize { let page_size = *PAGE_SIZE; let new_size_bytes = current_size_bytes + add_bytes; // Guard against < page_size. if new_size_bytes <= page_size.get() { return page_size; } // Round up the new size to the // nearest multiple of the OS page size. let remainder = new_size_bytes % page_size; // INVARIANT: we guarded against < page_size above. NonZeroUsize::new(if remainder == 0 { new_size_bytes } else { (new_size_bytes + page_size.get()) - remainder }) .unwrap() } /// Memory map resize by a percentage. /// /// # Method /// This function will multiply `current_size_bytes` by `percent`. /// /// Any input `<= 1.0` or non-normal float ([`f32::NAN`], [`f32::INFINITY`]) /// will make the returning `NonZeroUsize` the same as `current_size_bytes` /// (rounded up to the OS page size). /// /// ```rust /// # use cuprate_database::resize::*; /// let page_size: usize = PAGE_SIZE.get(); /// /// // Anything below the page size will round up to the page size. /// for i in 0..=page_size { /// assert_eq!(percent(i, 1.0).get(), page_size); /// } /// /// // Same for 2 page sizes. /// for i in (page_size + 1)..=(page_size * 2) { /// assert_eq!(percent(i, 1.0).get(), page_size * 2); /// } /// /// // Weird floats do nothing. /// assert_eq!(percent(page_size, f32::NAN).get(), page_size); /// assert_eq!(percent(page_size, f32::INFINITY).get(), page_size); /// assert_eq!(percent(page_size, f32::NEG_INFINITY).get(), page_size); /// assert_eq!(percent(page_size, -1.0).get(), page_size); /// assert_eq!(percent(page_size, 0.999).get(), page_size); /// ``` /// /// # Panics /// This function will panic if `current_size_bytes * percent` /// is closer to [`usize::MAX`] than the OS page size. /// /// ```rust,should_panic /// # use cuprate_database::resize::*; /// // Ridiculous large numbers panic. /// percent(usize::MAX, 1.001); /// ``` pub fn percent(current_size_bytes: usize, percent: f32) -> NonZeroUsize { // Guard against bad floats. use std::num::FpCategory; let percent = match percent.classify() { FpCategory::Normal => { if percent <= 1.0 { 1.0 } else { percent } } _ => 1.0, }; let page_size = *PAGE_SIZE; // INVARIANT: Allow `f32` <-> `usize` casting, we handle all cases. #[allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss )] let new_size_bytes = ((current_size_bytes as f32) * percent) as usize; // Panic if rounding up to the nearest page size would overflow. let new_size_bytes = if new_size_bytes > (usize::MAX - page_size.get()) { panic!("new_size_bytes is percent() near usize::MAX"); } else { new_size_bytes }; // Guard against < page_size. if new_size_bytes <= page_size.get() { return page_size; } // Round up the new size to the // nearest multiple of the OS page size. let remainder = new_size_bytes % page_size; // INVARIANT: we guarded against < page_size above. NonZeroUsize::new(if remainder == 0 { new_size_bytes } else { (new_size_bytes + page_size.get()) - remainder }) .unwrap() } //---------------------------------------------------------------------------------------------------- Tests #[cfg(test)] mod test { // use super::*; }