use super::{Memory, Storable};
use crate::substrate::{self, DefaultMemoryImpl};
use ic_memory::{RuntimeGrowError, RuntimeMemory};
use std::{fmt, marker::PhantomData, ops::Range, ops::RangeBounds, rc::Rc};
const PAGE_BYTES: u64 = 65_536;
const MAX_PAGES: u64 = 4_194_304;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RegionGrowError {
LimitExceeded {
minimum: u64,
maximum: u64,
},
Runtime(RuntimeGrowError),
}
impl fmt::Display for RegionGrowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LimitExceeded { minimum, maximum } => {
write!(
f,
"requested region extent {minimum} exceeds {maximum} pages"
)
}
Self::Runtime(error) => error.fmt(f),
}
}
}
impl std::error::Error for RegionGrowError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Runtime(error) => Some(error),
Self::LimitExceeded { .. } => None,
}
}
}
pub struct BTreeMapRegion<K: Storable + Ord + Clone, V: Storable, M: Memory = DefaultMemoryImpl> {
memory: RegionMemory<M>,
record: PhantomData<(K, V)>,
}
impl<K: Storable + Ord + Clone, V: Storable, M: Memory> BTreeMapRegion<K, V, M> {
#[must_use]
pub fn new(memory: Rc<RuntimeMemory<M>>, pages: Range<u64>) -> Self {
assert!(pages.start < pages.end && pages.end <= MAX_PAGES);
Self {
memory: RegionMemory { memory, pages },
record: PhantomData,
}
}
#[must_use]
pub fn allocated_pages(&self) -> u64 {
(&self.memory).size()
}
pub fn read(&self, offset: u64, dst: &mut [u8]) {
(&self.memory).read(offset, dst);
}
pub fn initialize(&mut self) {
assert_eq!(self.allocated_pages(), 0, "region is already nonempty");
substrate::BTreeMap::<K, V, _>::new(&self.memory);
}
pub fn reserve_pages(&mut self, minimum: u64) -> Result<(), RegionGrowError> {
self.memory.reserve_pages(minimum)
}
pub fn get(&self, key: &K) -> Option<V> {
self.current_map().get(key)
}
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
self.current_map().insert(key, value)
}
pub fn remove(&mut self, key: &K) -> Option<V> {
self.current_map().remove(key)
}
#[must_use]
pub fn len(&self) -> u64 {
self.current_map().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.current_map().is_empty()
}
pub fn keys_range(&self, bounds: impl RangeBounds<K>) -> std::vec::IntoIter<K> {
self.current_map()
.keys_range(bounds)
.collect::<Vec<_>>()
.into_iter()
}
fn current_map(&self) -> substrate::BTreeMap<K, V, &RegionMemory<M>> {
substrate::BTreeMap::load(&self.memory)
}
}
struct RegionMemory<M: Memory> {
memory: Rc<RuntimeMemory<M>>,
pages: Range<u64>,
}
impl<M: Memory> RegionMemory<M> {
fn reserve_pages(&self, minimum: u64) -> Result<(), RegionGrowError> {
let maximum = self.pages.end - self.pages.start;
if minimum > maximum {
return Err(RegionGrowError::LimitExceeded { minimum, maximum });
}
if minimum > self.size() {
let additional = self.pages.start + minimum - self.memory.size();
self.memory
.as_ref()
.grow(additional)
.map_err(RegionGrowError::Runtime)?;
}
Ok(())
}
fn check_io(&self, offset: u64, count: usize) {
let extent = self.size() * PAGE_BYTES;
assert!(offset <= extent && count as u64 <= extent - offset);
}
}
impl<M: Memory> Memory for &RegionMemory<M> {
fn size(&self) -> u64 {
self.memory.size().clamp(self.pages.start, self.pages.end) - self.pages.start
}
fn grow(&self, delta: u64) -> i64 {
let previous = self.size();
let Some(minimum) = previous.checked_add(delta) else {
return -1;
};
if self.reserve_pages(minimum).is_ok() {
i64::try_from(previous).unwrap()
} else {
-1
}
}
fn read(&self, offset: u64, dst: &mut [u8]) {
self.check_io(offset, dst.len());
if !dst.is_empty() {
self.memory
.read(self.pages.start * PAGE_BYTES + offset, dst);
}
}
fn write(&self, offset: u64, src: &[u8]) {
self.check_io(offset, src.len());
if !src.is_empty() {
self.memory
.write(self.pages.start * PAGE_BYTES + offset, src);
}
}
}