use std::{marker::PhantomData, sync::Arc};
use brk_traversable::{Traversable, TreeNode, make_leaf};
use brk_types::Height;
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{
AnyExportableVec, AnyVec, BinaryTransform, CachedBoxedVec, CheckedSub, Formattable,
PrintableIndex, ReadableBoxedVec, ReadableVec, TypedVec, VecIndex, VecValue, Version,
short_type_name,
};
pub struct LazyRollingRatioVec<S, C, T, F>
where
S: VecValue,
C: VecValue,
T: VecValue,
{
name: Arc<str>,
base_version: Version,
source: ReadableBoxedVec<Height, S>,
cached: CachedBoxedVec<Height, C>,
cached_transform: fn(Height, C) -> C,
window_starts: CachedBoxedVec<Height, Height>,
_marker: PhantomData<(T, F)>,
}
impl<S, C, T, F> LazyRollingRatioVec<S, C, T, F>
where
S: VecValue + CheckedSub + Default,
C: VecValue + CheckedSub + Default,
T: VecValue,
F: BinaryTransform<S, C, T>,
{
#[inline(always)]
fn identity_cached(_: Height, value: C) -> C {
value
}
pub fn new(
name: &str,
version: Version,
source: ReadableBoxedVec<Height, S>,
cached: CachedBoxedVec<Height, C>,
window_starts: CachedBoxedVec<Height, Height>,
) -> Self {
Self::with_cached_transform(
name,
version,
source,
cached,
window_starts,
Self::identity_cached,
)
}
pub fn with_cached_transform(
name: &str,
version: Version,
source: ReadableBoxedVec<Height, S>,
cached: CachedBoxedVec<Height, C>,
window_starts: CachedBoxedVec<Height, Height>,
cached_transform: fn(Height, C) -> C,
) -> Self {
Self {
name: Arc::from(name),
base_version: version,
source,
cached,
cached_transform,
window_starts,
_marker: PhantomData,
}
}
#[inline(always)]
fn previous_index(start: Height) -> Option<usize> {
start.to_usize().checked_sub(1)
}
#[inline(always)]
fn compute(
&self,
index: usize,
previous: Option<usize>,
source_current: S,
source_previous: S,
cached_current: C,
cached_previous: C,
) -> T {
F::apply(
source_current
.checked_sub(source_previous)
.unwrap_or_default(),
(self.cached_transform)(Height::from(index), cached_current)
.checked_sub(
previous
.map(|index| (self.cached_transform)(Height::from(index), cached_previous))
.unwrap_or_default(),
)
.unwrap_or_default(),
)
}
fn for_each_value(&self, from: usize, to: usize, mut each: impl FnMut(T)) {
let cached = self.cached.cached();
let window_starts = self.window_starts.cached();
let to = to
.min(self.source.len())
.min(cached.len())
.min(window_starts.len());
if from >= to {
return;
}
let starts = &window_starts[from..to];
let first_previous = starts.iter().find_map(|start| Self::previous_index(*start));
let last_previous = starts
.iter()
.rev()
.find_map(|start| Self::previous_index(*start));
if let Some((first_previous, last_previous)) = first_previous.zip(last_previous)
&& last_previous + 1 >= from
{
let read_from = first_previous.min(from);
let source = self.source.collect_range_dyn(read_from, to);
for (offset, start) in starts.iter().enumerate() {
let index = from + offset;
let source_current = source[index - read_from].clone();
let previous = Self::previous_index(*start);
let source_previous = previous
.map(|previous| source[previous - read_from].clone())
.unwrap_or_default();
let cached_current = cached[index].clone();
let cached_previous = previous
.map(|previous| cached[previous].clone())
.unwrap_or_default();
each(self.compute(
index,
previous,
source_current,
source_previous,
cached_current,
cached_previous,
));
}
return;
}
let current = self.source.collect_range_dyn(from, to);
let previous = first_previous
.zip(last_previous)
.map(|(first, last)| (first, self.source.collect_range_dyn(first, last + 1)));
for (offset, (source_current, start)) in current.into_iter().zip(starts).enumerate() {
let index = from + offset;
let previous_index = Self::previous_index(*start);
let source_previous = previous_index
.and_then(|previous_index| {
previous
.as_ref()
.map(|(first, values)| values[previous_index - first].clone())
})
.unwrap_or_default();
let cached_current = cached[index].clone();
let cached_previous = previous_index
.map(|previous| cached[previous].clone())
.unwrap_or_default();
each(self.compute(
index,
previous_index,
source_current,
source_previous,
cached_current,
cached_previous,
));
}
}
}
impl<S, C, T, F> Clone for LazyRollingRatioVec<S, C, T, F>
where
S: VecValue,
C: VecValue,
T: VecValue,
{
fn clone(&self) -> Self {
Self {
name: Arc::clone(&self.name),
base_version: self.base_version,
source: self.source.clone(),
cached: self.cached.clone(),
cached_transform: self.cached_transform,
window_starts: self.window_starts.clone(),
_marker: PhantomData,
}
}
}
impl<S, C, T, F> AnyVec for LazyRollingRatioVec<S, C, T, F>
where
S: VecValue + CheckedSub + Default,
C: VecValue + CheckedSub + Default,
T: VecValue,
F: BinaryTransform<S, C, T> + Send + Sync,
{
fn version(&self) -> Version {
self.base_version
+ self.source.version()
+ self.cached.version()
+ self.window_starts.version()
}
fn name(&self) -> &str {
&self.name
}
fn len(&self) -> usize {
self.source
.len()
.min(self.cached.len())
.min(self.window_starts.len())
}
fn index_type_to_string(&self) -> &'static str {
<Height as PrintableIndex>::to_string()
}
fn region_names(&self) -> Vec<String> {
Vec::new()
}
fn value_type_to_size_of(&self) -> usize {
size_of::<T>()
}
fn value_type_to_string(&self) -> &'static str {
short_type_name::<T>()
}
}
impl<S, C, T, F> TypedVec for LazyRollingRatioVec<S, C, T, F>
where
S: VecValue + CheckedSub + Default,
C: VecValue + CheckedSub + Default,
T: VecValue,
F: BinaryTransform<S, C, T> + Send + Sync,
{
type I = Height;
type T = T;
}
impl<S, C, T, F> ReadableVec<Height, T> for LazyRollingRatioVec<S, C, T, F>
where
S: VecValue + CheckedSub + Default,
C: VecValue + CheckedSub + Default,
T: VecValue,
F: BinaryTransform<S, C, T> + Send + Sync,
{
fn read_into_at(&self, from: usize, to: usize, buf: &mut Vec<T>) {
buf.reserve(to.saturating_sub(from));
self.for_each_value(from, to, |value| buf.push(value));
}
fn for_each_range_dyn_at(&self, from: usize, to: usize, each: &mut dyn FnMut(T)) {
self.for_each_value(from, to, each);
}
fn fold_range_at<B, G: FnMut(B, T) -> B>(&self, from: usize, to: usize, init: B, fold: G) -> B {
let mut values = Vec::with_capacity(to.saturating_sub(from));
self.read_into_at(from, to, &mut values);
values.into_iter().fold(init, fold)
}
fn try_fold_range_at<B, E, G: FnMut(B, T) -> Result<B, E>>(
&self,
from: usize,
to: usize,
init: B,
fold: G,
) -> Result<B, E> {
let mut values = Vec::with_capacity(to.saturating_sub(from));
self.read_into_at(from, to, &mut values);
values.into_iter().try_fold(init, fold)
}
fn collect_one_at(&self, index: usize) -> Option<T> {
if index >= self.len() {
return None;
}
let cached = self.cached.cached();
let window_starts = self.window_starts.cached();
let previous = Self::previous_index(window_starts[index]);
Some(
self.compute(
index,
previous,
self.source.collect_one_at(index)?,
previous
.and_then(|index| self.source.collect_one_at(index))
.unwrap_or_default(),
cached[index].clone(),
previous
.map(|index| cached[index].clone())
.unwrap_or_default(),
),
)
}
fn read_sorted_into_at(&self, indices: &[usize], out: &mut Vec<T>) {
out.reserve(indices.len());
indices
.iter()
.filter_map(|&index| self.collect_one_at(index))
.for_each(|value| out.push(value));
}
}
impl<S, C, T, F> Traversable for LazyRollingRatioVec<S, C, T, F>
where
S: VecValue + CheckedSub + Default,
C: VecValue + CheckedSub + Default,
T: VecValue + Formattable + Serialize + JsonSchema,
F: BinaryTransform<S, C, T> + Send + Sync,
{
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
std::iter::once(self as &dyn AnyExportableVec)
}
fn to_tree_node(&self) -> TreeNode {
make_leaf::<Height, T, _>(self)
}
}
#[cfg(test)]
mod tests {
use brk_types::{PartsPerMillion32, Sats};
use vecdb::{
AnyStoredVec, CachedVec, Database, EagerVec, ImportableVec, PcoVec, ReadableCloneableVec,
ReadableVec, WritableVec,
};
use super::*;
use crate::internal::RatioSats;
fn double(_: Height, value: Sats) -> Sats {
value + value
}
#[test]
fn derives_rolling_ratios_from_one_source_and_cached_denominator() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"brk-lazy-rolling-ratio-{}-{suffix}",
std::process::id()
));
let db = Database::open(&path).unwrap();
let mut source: EagerVec<PcoVec<Height, Sats>> =
EagerVec::forced_import(&db, "source", Version::ONE).unwrap();
let mut denominator: EagerVec<PcoVec<Height, Sats>> =
EagerVec::forced_import(&db, "denominator", Version::ONE).unwrap();
let mut starts: EagerVec<PcoVec<Height, Height>> =
EagerVec::forced_import(&db, "starts", Version::ONE).unwrap();
for value in [10, 30, 60, 100] {
source.push(Sats::new(value));
}
for value in [20, 50, 90, 140] {
denominator.push(Sats::new(value));
}
for value in [0, 0, 1, 2] {
starts.push(Height::new(value));
}
source.write().unwrap();
denominator.write().unwrap();
starts.write().unwrap();
let denominator = CachedVec::wrap(denominator);
let starts = CachedVec::wrap(starts);
let ratio = LazyRollingRatioVec::<
Sats,
Sats,
PartsPerMillion32,
RatioSats<PartsPerMillion32>,
>::new(
"ratio",
Version::ONE,
source.read_only_boxed_clone(),
denominator.read_only_cached_boxed_clone(),
starts.read_only_cached_boxed_clone(),
);
assert_eq!(
ratio.collect_range(Height::ZERO, Height::new(4)),
vec![
PartsPerMillion32::from(0.5),
PartsPerMillion32::from(0.6),
PartsPerMillion32::from(50.0 / 70.0),
PartsPerMillion32::from(70.0 / 90.0),
],
);
assert_eq!(
ratio.collect_range(Height::new(3), Height::new(4)),
[PartsPerMillion32::from(70.0 / 90.0)],
);
let transformed = LazyRollingRatioVec::<
Sats,
Sats,
PartsPerMillion32,
RatioSats<PartsPerMillion32>,
>::with_cached_transform(
"transformed",
Version::ONE,
source.read_only_boxed_clone(),
denominator.read_only_cached_boxed_clone(),
starts.read_only_cached_boxed_clone(),
double,
);
assert_eq!(
transformed.collect_range(Height::ZERO, Height::new(4)),
vec![
PartsPerMillion32::from(0.25),
PartsPerMillion32::from(0.3),
PartsPerMillion32::from(25.0 / 70.0),
PartsPerMillion32::from(35.0 / 90.0),
],
);
drop(transformed);
drop(ratio);
drop(starts);
drop(denominator);
drop(source);
drop(db);
std::fs::remove_dir_all(path).unwrap();
}
}