global: snapshot

This commit is contained in:
nym21
2026-03-02 15:28:13 +01:00
parent 4d97cec869
commit 4e7cd9ab6f
21 changed files with 595 additions and 373 deletions
@@ -2,150 +2,123 @@ use std::time::Instant;
use brk_types::{Cents, Height, Timestamp};
use tracing::debug;
use vecdb::{ReadableVec, VecIndex};
use crate::{blocks, prices};
use vecdb::VecIndex;
/// Sparse table for O(1) range maximum queries on prices.
/// Uses O(n log n) space (~140MB for 880k blocks).
/// Vec<Vec> per level for incremental O(new_blocks * log n) extension.
#[derive(Debug, Clone, Default)]
pub struct PriceRangeMax {
/// Flattened table: table[k * n + i] = max of 2^k elements starting at index i
/// Using flat layout for better cache locality.
table: Vec<Cents>,
/// Number of elements
levels: Vec<Vec<Cents>>,
n: usize,
}
impl PriceRangeMax {
/// Build sparse table from high prices. O(n log n) time and space.
pub(crate) fn build(prices: &[Cents]) -> Self {
let start = Instant::now();
let n = prices.len();
if n == 0 {
return Self {
table: vec![],
n: 0,
};
pub(crate) fn extend(&mut self, prices: &[Cents]) {
let new_n = prices.len();
if new_n <= self.n || new_n == 0 {
return;
}
// levels = floor(log2(n)) + 1
let levels = (usize::BITS - n.leading_zeros()) as usize;
let start = Instant::now();
let old_n = self.n;
let new_levels_count = (usize::BITS - new_n.leading_zeros()) as usize;
// Allocate flat table: levels * n elements
let mut table = vec![Cents::ZERO; levels * n];
while self.levels.len() < new_levels_count {
self.levels.push(Vec::new());
}
// Base case: level 0 = original prices
table[..n].copy_from_slice(prices);
self.levels[0].extend_from_slice(&prices[old_n..new_n]);
// Build each level from the previous
// table[k][i] = max(table[k-1][i], table[k-1][i + 2^(k-1)])
for k in 1..levels {
let prev_offset = (k - 1) * n;
let curr_offset = k * n;
for k in 1..new_levels_count {
let half = 1 << (k - 1);
let end = n.saturating_sub(1 << k) + 1;
let new_end = if new_n >= (1 << k) {
new_n + 1 - (1 << k)
} else {
0
};
// Use split_at_mut to avoid bounds checks in the loop
let (prev_level, rest) = table.split_at_mut(curr_offset);
let prev = &prev_level[prev_offset..prev_offset + n];
let curr = &mut rest[..n];
for i in 0..end {
curr[i] = prev[i].max(prev[i + half]);
let old_end = self.levels[k].len();
if new_end > old_end {
let (prev_levels, curr_levels) = self.levels.split_at_mut(k);
let prev = &prev_levels[k - 1];
let curr = &mut curr_levels[0];
curr.reserve(new_end - old_end);
for i in old_end..new_end {
curr.push(prev[i].max(prev[i + half]));
}
}
}
self.n = new_n;
let elapsed = start.elapsed();
let total_entries: usize = self.levels.iter().map(|l| l.len()).sum();
debug!(
"PriceRangeMax built: {} heights, {} levels, {:.2}MB, {:.2}ms",
n,
levels,
(levels * n * std::mem::size_of::<Cents>()) as f64 / 1_000_000.0,
"PriceRangeMax extended: {} -> {} heights ({} new), {} levels, {:.2}MB, {:.2}ms",
old_n,
new_n,
new_n - old_n,
new_levels_count,
(total_entries * std::mem::size_of::<Cents>()) as f64 / 1_000_000.0,
elapsed.as_secs_f64() * 1000.0
);
Self { table, n }
}
/// Query maximum value in range [l, r] (inclusive). O(1) time.
pub(crate) fn truncate(&mut self, new_n: usize) {
if new_n >= self.n {
return;
}
if new_n == 0 {
self.levels.clear();
self.n = 0;
return;
}
let new_levels_count = (usize::BITS - new_n.leading_zeros()) as usize;
self.levels.truncate(new_levels_count);
for k in 0..new_levels_count {
let valid = if new_n >= (1 << k) {
new_n + 1 - (1 << k)
} else {
0
};
self.levels[k].truncate(valid);
}
self.n = new_n;
}
#[inline]
pub(crate) fn range_max(&self, l: usize, r: usize) -> Cents {
debug_assert!(l <= r && r < self.n);
let len = r - l + 1;
// k = floor(log2(len))
let k = (usize::BITS - len.leading_zeros() - 1) as usize;
let half = 1 << k;
// max of [l, l + 2^k) and [r - 2^k + 1, r + 1)
let offset = k * self.n;
let level = &self.levels[k];
unsafe {
let a = *self.table.get_unchecked(offset + l);
let b = *self.table.get_unchecked(offset + r + 1 - half);
let a = *level.get_unchecked(l);
let b = *level.get_unchecked(r + 1 - half);
a.max(b)
}
}
/// Query maximum value in height range. O(1) time.
#[inline]
pub(crate) fn max_between(&self, from: Height, to: Height) -> Cents {
self.range_max(from.to_usize(), to.to_usize())
}
}
/// Context shared across block processing.
pub struct ComputeContext {
/// Starting height for this computation run
pub struct ComputeContext<'a> {
pub starting_height: Height,
/// Last height to process
pub last_height: Height,
/// Pre-computed height -> timestamp mapping
pub height_to_timestamp: Vec<Timestamp>,
/// Pre-computed height -> price mapping
pub height_to_price: Vec<Cents>,
/// Sparse table for O(1) range max queries on high prices.
/// Used for computing max price during UTXO holding periods (peak regret).
pub price_range_max: PriceRangeMax,
pub height_to_timestamp: &'a [Timestamp],
pub height_to_price: &'a [Cents],
pub price_range_max: &'a PriceRangeMax,
}
impl ComputeContext {
/// Create a new computation context.
pub(crate) fn new(
starting_height: Height,
last_height: Height,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
) -> Self {
let height_to_timestamp: Vec<Timestamp> =
blocks.time.timestamp_monotonic.collect();
let height_to_price: Vec<Cents> =
prices.price.cents.height.collect();
// Build sparse table for O(1) range max queries on prices
// Used for computing peak price during UTXO holding periods (peak regret)
let price_range_max = PriceRangeMax::build(&height_to_price);
Self {
starting_height,
last_height,
height_to_timestamp,
height_to_price,
price_range_max,
}
}
/// Get price at height.
impl<'a> ComputeContext<'a> {
pub(crate) fn price_at(&self, height: Height) -> Cents {
self.height_to_price[height.to_usize()]
}
/// Get timestamp at height.
pub(crate) fn timestamp_at(&self, height: Height) -> Timestamp {
self.height_to_timestamp[height.to_usize()]
}