global: snapshot

This commit is contained in:
nym21
2026-03-04 23:21:56 +01:00
parent 9e23de4ba1
commit ef0b77baa8
51 changed files with 2109 additions and 5730 deletions

View File

@@ -1,10 +1,20 @@
use std::path::Path;
use std::{collections::BTreeMap, path::Path};
use brk_error::Result;
use brk_types::{Age, Cents, CentsSats, CostBasisSnapshot, Height, Sats, SupplyState};
use brk_types::{Age, Cents, CentsCompact, CentsSats, CentsSquaredSats, CostBasisSnapshot, Height, Sats, SupplyState};
use super::super::cost_basis::{CostBasisData, Percentiles, RealizedState, UnrealizedState};
pub struct SendPrecomputed {
pub sats: Sats,
pub prev_price: Cents,
pub age: Age,
pub current_ps: CentsSats,
pub prev_ps: CentsSats,
pub ath_ps: CentsSats,
pub prev_investor_cap: CentsSquaredSats,
}
pub struct CohortState {
pub supply: SupplyState,
pub realized: RealizedState,
@@ -73,10 +83,6 @@ impl CohortState {
self.realized.reset_single_iteration_values();
}
pub(crate) fn increment(&mut self, supply: &SupplyState, price: Cents) {
self.increment_snapshot(&CostBasisSnapshot::from_utxo(price, supply));
}
pub(crate) fn increment_snapshot(&mut self, s: &CostBasisSnapshot) {
self.supply += &s.supply_state;
@@ -92,10 +98,6 @@ impl CohortState {
}
}
pub(crate) fn decrement(&mut self, supply: &SupplyState, price: Cents) {
self.decrement_snapshot(&CostBasisSnapshot::from_utxo(price, supply));
}
pub(crate) fn decrement_snapshot(&mut self, s: &CostBasisSnapshot) {
self.supply -= &s.supply_state;
@@ -112,19 +114,27 @@ impl CohortState {
}
pub(crate) fn receive_utxo(&mut self, supply: &SupplyState, price: Cents) {
self.receive_utxo_snapshot(supply, &CostBasisSnapshot::from_utxo(price, supply));
}
/// Like receive_utxo but takes a pre-computed snapshot to avoid redundant multiplication
/// when the same supply/price is used across multiple cohorts.
pub(crate) fn receive_utxo_snapshot(
&mut self,
supply: &SupplyState,
snapshot: &CostBasisSnapshot,
) {
self.supply += supply;
if supply.value > Sats::ZERO {
let sats = supply.value;
self.realized.receive(snapshot.realized_price, supply.value);
// Compute once using typed values
let price_sats = CentsSats::from_price_sats(price, sats);
let investor_cap = price_sats.to_investor_cap(price);
self.realized.receive(price, sats);
self.cost_basis_data
.increment(price, sats, price_sats, investor_cap);
self.cost_basis_data.increment(
snapshot.realized_price,
supply.value,
snapshot.price_sats,
snapshot.investor_cap,
);
}
}
@@ -160,6 +170,51 @@ impl CohortState {
}
}
/// Pre-computed values for send_utxo when the same supply/prices are shared
/// across multiple cohorts (age_range, epoch, year).
pub(crate) fn precompute_send(
supply: &SupplyState,
current_price: Cents,
prev_price: Cents,
ath: Cents,
age: Age,
) -> Option<SendPrecomputed> {
if supply.utxo_count == 0 || supply.value == Sats::ZERO {
return None;
}
let sats = supply.value;
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = CentsSats::from_price_sats(ath, sats);
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
Some(SendPrecomputed {
sats,
prev_price,
age,
current_ps,
prev_ps,
ath_ps,
prev_investor_cap,
})
}
pub(crate) fn send_utxo_precomputed(
&mut self,
supply: &SupplyState,
pre: &SendPrecomputed,
) {
self.supply -= supply;
self.sent += pre.sats;
self.satblocks_destroyed += pre.age.satblocks_destroyed(pre.sats);
self.satdays_destroyed += pre.age.satdays_destroyed(pre.sats);
self.realized
.send(pre.sats, pre.current_ps, pre.prev_ps, pre.ath_ps, pre.prev_investor_cap);
self.cost_basis_data
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_investor_cap);
}
pub(crate) fn send_utxo(
&mut self,
supply: &SupplyState,
@@ -168,33 +223,10 @@ impl CohortState {
ath: Cents,
age: Age,
) {
if supply.utxo_count == 0 {
return;
}
self.supply -= supply;
if supply.value > Sats::ZERO {
self.sent += supply.value;
self.satblocks_destroyed += age.satblocks_destroyed(supply.value);
self.satdays_destroyed += age.satdays_destroyed(supply.value);
let cp = current_price;
let pp = prev_price;
let ath_price = ath;
let sats = supply.value;
// Compute ONCE using typed values
let current_ps = CentsSats::from_price_sats(cp, sats);
let prev_ps = CentsSats::from_price_sats(pp, sats);
let ath_ps = CentsSats::from_price_sats(ath_price, sats);
let prev_investor_cap = prev_ps.to_investor_cap(pp);
self.realized
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
self.cost_basis_data
.decrement(pp, sats, prev_ps, prev_investor_cap);
if let Some(pre) = Self::precompute_send(supply, current_price, prev_price, ath, age) {
self.send_utxo_precomputed(supply, &pre);
} else if supply.utxo_count > 0 {
self.supply -= supply;
}
}
@@ -263,7 +295,7 @@ impl CohortState {
self.cost_basis_data.write(height, cleanup)
}
pub(crate) fn cost_basis_data_iter(&self) -> impl Iterator<Item = (Cents, &Sats)> {
self.cost_basis_data.iter().map(|(k, v)| (k.into(), v))
pub(crate) fn cost_basis_map(&self) -> &BTreeMap<CentsCompact, Sats> {
self.cost_basis_data.map()
}
}

View File

@@ -34,6 +34,8 @@ pub struct CostBasisData {
percentiles_dirty: bool,
cached_percentiles: Option<Percentiles>,
rounding_digits: Option<i32>,
/// Monotonically increasing counter, bumped on each apply_pending with actual changes.
generation: u64,
}
const STATE_TO_KEEP: usize = 10;
@@ -49,6 +51,7 @@ impl CostBasisData {
percentiles_dirty: true,
cached_percentiles: None,
rounding_digits: None,
generation: 0,
}
}
@@ -93,15 +96,9 @@ impl CostBasisData {
&& self.pending_raw.investor_cap_dec == CentsSquaredSats::ZERO
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (CentsCompact, &Sats)> {
pub(crate) fn map(&self) -> &CostBasisMap {
self.assert_pending_empty();
self.state
.as_ref()
.unwrap()
.base
.map
.iter()
.map(|(&k, v)| (k, v))
&self.state.as_ref().unwrap().base.map
}
pub(crate) fn is_empty(&self) -> bool {
@@ -183,18 +180,14 @@ impl CostBasisData {
}
pub(crate) fn apply_pending(&mut self) {
if !self.pending.is_empty() {
self.percentiles_dirty = true;
if self.pending.is_empty() && self.pending_raw_is_zero() {
return;
}
self.generation = self.generation.wrapping_add(1);
self.percentiles_dirty = true;
let map = &mut self.state.as_mut().unwrap().base.map;
for (cents, (inc, dec)) in self.pending.drain() {
let entry = self
.state
.as_mut()
.unwrap()
.base
.map
.entry(cents)
.or_default();
let entry = map.entry(cents).or_default();
*entry += inc;
if *entry < dec {
panic!(
@@ -211,7 +204,7 @@ impl CostBasisData {
}
*entry -= dec;
if *entry == Sats::ZERO {
self.state.as_mut().unwrap().base.map.remove(&cents);
map.remove(&cents);
}
}
@@ -267,7 +260,8 @@ impl CostBasisData {
if !self.percentiles_dirty {
return self.cached_percentiles;
}
self.cached_percentiles = Percentiles::compute(self.iter().map(|(k, &v)| (k, v)));
self.cached_percentiles =
Percentiles::compute_from_map(&self.state.as_ref().unwrap().base.map);
self.percentiles_dirty = false;
self.cached_percentiles
}

View File

@@ -1,7 +1,9 @@
use brk_types::{Cents, CentsCompact, Sats};
use brk_types::Cents;
use crate::internal::{PERCENTILES, PERCENTILES_LEN};
use super::CostBasisMap;
#[derive(Clone, Copy, Debug)]
pub struct Percentiles {
/// Sat-weighted: percentiles by coin count
@@ -11,19 +13,17 @@ pub struct Percentiles {
}
impl Percentiles {
/// Compute both sat-weighted and USD-weighted percentiles in a single pass.
/// Takes an iterator over (price, sats) pairs, assumed sorted by price ascending.
pub(crate) fn compute(iter: impl Iterator<Item = (CentsCompact, Sats)>) -> Option<Self> {
// Collect to allow two passes: one for totals, one for percentiles
let entries: Vec<_> = iter.collect();
if entries.is_empty() {
/// Compute both sat-weighted and USD-weighted percentiles in two passes over the BTreeMap.
/// Avoids intermediate Vec allocation by iterating the map directly.
pub(crate) fn compute_from_map(map: &CostBasisMap) -> Option<Self> {
if map.is_empty() {
return None;
}
// Compute totals
// First pass: compute totals
let mut total_sats: u64 = 0;
let mut total_usd: u128 = 0;
for &(cents, sats) in &entries {
for (&cents, &sats) in map.iter() {
total_sats += u64::from(sats);
total_usd += cents.as_u128() * sats.as_u128();
}
@@ -32,6 +32,12 @@ impl Percentiles {
return None;
}
// Precompute targets to avoid repeated multiplication in the inner loop
let sat_targets: [u64; PERCENTILES_LEN] =
PERCENTILES.map(|p| total_sats * u64::from(p) / 100);
let usd_targets: [u128; PERCENTILES_LEN] =
PERCENTILES.map(|p| total_usd * u128::from(p) / 100);
let mut sat_weighted = [Cents::ZERO; PERCENTILES_LEN];
let mut usd_weighted = [Cents::ZERO; PERCENTILES_LEN];
let mut cumsum_sats: u64 = 0;
@@ -39,20 +45,17 @@ impl Percentiles {
let mut sat_idx = 0;
let mut usd_idx = 0;
for (cents, sats) in entries {
// Second pass: compute percentiles
for (&cents, &sats) in map.iter() {
cumsum_sats += u64::from(sats);
cumsum_usd += cents.as_u128() * sats.as_u128();
while sat_idx < PERCENTILES_LEN
&& cumsum_sats >= total_sats * u64::from(PERCENTILES[sat_idx]) / 100
{
while sat_idx < PERCENTILES_LEN && cumsum_sats >= sat_targets[sat_idx] {
sat_weighted[sat_idx] = cents.into();
sat_idx += 1;
}
while usd_idx < PERCENTILES_LEN
&& cumsum_usd >= total_usd * u128::from(PERCENTILES[usd_idx]) / 100
{
while usd_idx < PERCENTILES_LEN && cumsum_usd >= usd_targets[usd_idx] {
usd_weighted[usd_idx] = cents.into();
usd_idx += 1;
}