global: final snapshot and fixes before release

This commit is contained in:
nym21
2026-03-22 23:16:52 +01:00
parent 514fdc40ee
commit 514b0513de
34 changed files with 323 additions and 210 deletions

12
Cargo.lock generated
View File

@@ -2538,9 +2538,9 @@ dependencies = [
[[package]] [[package]]
name = "rawdb" name = "rawdb"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f1a1534553d4e626de325e000d3250490fb4020d8f177747615f345016c8f0a" checksum = "dc7e70161aa9dbfcc1f858cae94eda70c9073bab5b22167bc6ab0f85d27054cf"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -3432,9 +3432,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]] [[package]]
name = "vecdb" name = "vecdb"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a45f0491e73f467ff4dcb360d4341ad6281719362f29b040c2b769d18e161ab1" checksum = "417cdef9fd0ada1659e1499c7180b3b8edf5256b99eb846c7f960c10a755ea3c"
dependencies = [ dependencies = [
"itoa", "itoa",
"libc", "libc",
@@ -3455,9 +3455,9 @@ dependencies = [
[[package]] [[package]]
name = "vecdb_derive" name = "vecdb_derive"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac60cdf47669f66acd6debfd66705464fb7c2519eed7b3692495d037f6d6399" checksum = "aba470bc1a709df1efaace5885b25e7685988c64b61ac379758d861d12312735"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn",

View File

@@ -87,7 +87,7 @@ tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "
tower-layer = "0.3" tower-layer = "0.3"
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
ureq = { version = "3.3.0", features = ["json"] } ureq = { version = "3.3.0", features = ["json"] }
vecdb = { version = "0.7.0", features = ["derive", "serde_json", "pco", "schemars"] } vecdb = { version = "0.7.1", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } # vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release] [workspace.metadata.release]

View File

@@ -21,7 +21,7 @@ impl Vecs {
indexer: &Indexer, indexer: &Indexer,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 50_000_000)?; let db = open_db(parent_path, super::DB_NAME, 1_000_000)?;
let version = parent_version; let version = parent_version;
let lookback = LookbackVecs::forced_import(&db, version)?; let lookback = LookbackVecs::forced_import(&db, version)?;

View File

@@ -22,7 +22,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, DB_NAME, 1_000_000)?; let db = open_db(parent_path, DB_NAME, 250_000)?;
let version = parent_version; let version = parent_version;
let v1 = version + Version::ONE; let v1 = version + Version::ONE;
let activity = ActivityVecs::forced_import(&db, version, indexes, cached_starts)?; let activity = ActivityVecs::forced_import(&db, version, indexes, cached_starts)?;

View File

@@ -3,7 +3,10 @@ use brk_types::{Cents, CentsCompact, Sats};
use crate::{ use crate::{
distribution::state::PendingDelta, distribution::state::PendingDelta,
internal::{PERCENTILES, PERCENTILES_LEN, algo::{FenwickNode, FenwickTree}}, internal::{
PERCENTILES, PERCENTILES_LEN,
algo::{FenwickNode, FenwickTree},
},
}; };
use super::COST_BASIS_PRICE_DIGITS; use super::COST_BASIS_PRICE_DIGITS;
@@ -70,7 +73,6 @@ pub(super) struct CostBasisFenwick {
// to a flat bucket index across two tiers. // to a flat bucket index across two tiers.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Map rounded dollars to a flat bucket index.
/// Prices >= $1M are clamped to the last bucket. /// Prices >= $1M are clamped to the last bucket.
#[inline] #[inline]
fn dollars_to_bucket(dollars: u64) -> usize { fn dollars_to_bucket(dollars: u64) -> usize {
@@ -83,7 +85,6 @@ fn dollars_to_bucket(dollars: u64) -> usize {
} }
} }
/// Convert a bucket index back to a price in Cents.
#[inline] #[inline]
fn bucket_to_cents(bucket: usize) -> Cents { fn bucket_to_cents(bucket: usize) -> Cents {
let dollars: u64 = if bucket < TIER1_START { let dollars: u64 = if bucket < TIER1_START {
@@ -96,24 +97,18 @@ fn bucket_to_cents(bucket: usize) -> Cents {
Cents::from(dollars * 100) Cents::from(dollars * 100)
} }
/// Map a CentsCompact price to a bucket index.
#[inline] #[inline]
fn price_to_bucket(price: CentsCompact) -> usize { fn price_to_bucket(price: CentsCompact) -> usize {
cents_to_bucket(price.into()) cents_to_bucket(price.into())
} }
/// Map a Cents price to a bucket index.
#[inline] #[inline]
fn cents_to_bucket(price: Cents) -> usize { fn cents_to_bucket(price: Cents) -> usize {
dollars_to_bucket(u64::from(price.round_to_dollar(COST_BASIS_PRICE_DIGITS)) / 100) dollars_to_bucket(u64::from(price.round_to_dollar(COST_BASIS_PRICE_DIGITS)) / 100)
} }
// --------------------------------------------------------------------------- impl Default for CostBasisFenwick {
// CostBasisFenwick implementation fn default() -> Self {
// ---------------------------------------------------------------------------
impl CostBasisFenwick {
pub(super) fn new() -> Self {
Self { Self {
tree: FenwickTree::new(TREE_SIZE), tree: FenwickTree::new(TREE_SIZE),
totals: CostBasisNode::default(), totals: CostBasisNode::default(),
@@ -121,7 +116,9 @@ impl CostBasisFenwick {
initialized: false, initialized: false,
} }
} }
}
impl CostBasisFenwick {
pub(super) fn is_initialized(&self) -> bool { pub(super) fn is_initialized(&self) -> bool {
self.initialized self.initialized
} }
@@ -153,7 +150,8 @@ impl CostBasisFenwick {
return; return;
} }
let bucket = price_to_bucket(price); let bucket = price_to_bucket(price);
let delta = CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth); let delta =
CostBasisNode::new(net_sats, price.as_u128() as i128 * net_sats as i128, is_sth);
self.tree.add(bucket, &delta); self.tree.add(bucket, &delta);
self.totals.add_assign(&delta); self.totals.add_assign(&delta);
} }
@@ -236,8 +234,7 @@ impl CostBasisFenwick {
sat_targets[PERCENTILES_LEN + 1] = total_sats - 1; // max sat_targets[PERCENTILES_LEN + 1] = total_sats - 1; // max
let mut sat_buckets = [0usize; PERCENTILES_LEN + 2]; let mut sat_buckets = [0usize; PERCENTILES_LEN + 2];
self.tree self.tree.kth(&sat_targets, &sat_field, &mut sat_buckets);
.kth(&sat_targets, &sat_field, &mut sat_buckets);
result.min_price = bucket_to_cents(sat_buckets[0]); result.min_price = bucket_to_cents(sat_buckets[0]);
(0..PERCENTILES_LEN).for_each(|i| { (0..PERCENTILES_LEN).for_each(|i| {
@@ -253,8 +250,7 @@ impl CostBasisFenwick {
} }
let mut usd_buckets = [0usize; PERCENTILES_LEN]; let mut usd_buckets = [0usize; PERCENTILES_LEN];
self.tree self.tree.kth(&usd_targets, &usd_field, &mut usd_buckets);
.kth(&usd_targets, &usd_field, &mut usd_buckets);
(0..PERCENTILES_LEN).for_each(|i| { (0..PERCENTILES_LEN).for_each(|i| {
result.usd_prices[i] = bucket_to_cents(usd_buckets[i]); result.usd_prices[i] = bucket_to_cents(usd_buckets[i]);

View File

@@ -1,8 +1,8 @@
use std::path::Path; use std::path::Path;
use brk_cohort::{ use brk_cohort::{
AgeRange, AmountRange, Class, ByEpoch, OverAmount, UnderAmount, UnderAge, AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount,
OverAge, SpendableType, CohortContext, Filter, Filtered, Term, SpendableType, Term, UnderAge, UnderAmount,
}; };
use brk_error::Result; use brk_error::Result;
use brk_traversable::Traversable; use brk_traversable::Traversable;
@@ -17,8 +17,8 @@ use crate::{
distribution::{ distribution::{
DynCohortVecs, DynCohortVecs,
metrics::{ metrics::{
AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics,
CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
MinimalCohortMetrics, ProfitabilityMetrics, RealizedFullAccum, SupplyCore, MinimalCohortMetrics, ProfitabilityMetrics, RealizedFullAccum, SupplyCore,
TypeCohortMetrics, TypeCohortMetrics,
}, },
@@ -52,11 +52,16 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
pub profitability: ProfitabilityMetrics<M>, pub profitability: ProfitabilityMetrics<M>,
pub matured: AgeRange<AmountPerBlockCumulativeRolling<M>>, pub matured: AgeRange<AmountPerBlockCumulativeRolling<M>>,
#[traversable(skip)] #[traversable(skip)]
pub(super) caches: UTXOCohortsTransientState,
}
/// In-memory state that does NOT survive rollback.
#[derive(Clone, Default)]
pub(crate) struct UTXOCohortsTransientState {
pub(super) fenwick: CostBasisFenwick, pub(super) fenwick: CostBasisFenwick,
/// Cached partition_point positions for tick_tock boundary searches. /// Cached partition_point positions for tick_tock boundary searches.
/// Avoids O(log n) binary search per boundary per block; scans forward /// Avoids O(log n) binary search per boundary per block; scans forward
/// from last known position (typically O(1) per boundary). /// from last known position (typically O(1) per boundary).
#[traversable(skip)]
pub(super) tick_tock_cached_positions: [usize; 20], pub(super) tick_tock_cached_positions: [usize; 20],
} }
@@ -284,24 +289,30 @@ impl UTXOCohorts<Rw> {
over_amount, over_amount,
profitability, profitability,
matured, matured,
fenwick: CostBasisFenwick::new(), caches: UTXOCohortsTransientState::default(),
tick_tock_cached_positions: [0; 20],
}) })
} }
/// Reset in-memory caches that become stale after rollback.
pub(crate) fn reset_caches(&mut self) {
self.caches = UTXOCohortsTransientState::default();
}
/// Initialize the Fenwick tree from all age-range BTreeMaps. /// Initialize the Fenwick tree from all age-range BTreeMaps.
/// Call after state import when all pending maps have been drained. /// Call after state import when all pending maps have been drained.
pub(crate) fn init_fenwick_if_needed(&mut self) { pub(crate) fn init_fenwick_if_needed(&mut self) {
if self.fenwick.is_initialized() { if self.caches.fenwick.is_initialized() {
return; return;
} }
let Self { let Self {
sth, sth,
fenwick, caches,
age_range, age_range,
.. ..
} = self; } = self;
fenwick.compute_is_sth(&sth.metrics.filter, age_range.iter().map(|v| v.filter())); caches
.fenwick
.compute_is_sth(&sth.metrics.filter, age_range.iter().map(|v| v.filter()));
let maps: Vec<_> = age_range let maps: Vec<_> = age_range
.iter() .iter()
@@ -312,27 +323,27 @@ impl UTXOCohorts<Rw> {
if map.is_empty() { if map.is_empty() {
return None; return None;
} }
Some((map, fenwick.is_sth_at(i))) Some((map, caches.fenwick.is_sth_at(i)))
}) })
.collect(); .collect();
fenwick.bulk_init(maps.into_iter()); caches.fenwick.bulk_init(maps.into_iter());
} }
/// Apply pending deltas from all age-range cohorts to the Fenwick tree. /// Apply pending deltas from all age-range cohorts to the Fenwick tree.
/// Call after receive/send, before push_cohort_states. /// Call after receive/send, before push_cohort_states.
pub(crate) fn update_fenwick_from_pending(&mut self) { pub(crate) fn update_fenwick_from_pending(&mut self) {
if !self.fenwick.is_initialized() { if !self.caches.fenwick.is_initialized() {
return; return;
} }
// Destructure to get separate borrows on fenwick and age_range // Destructure to get separate borrows on caches and age_range
let Self { let Self {
fenwick, age_range, .. caches, age_range, ..
} = self; } = self;
for (i, sub) in age_range.iter().enumerate() { for (i, sub) in age_range.iter().enumerate() {
if let Some(state) = sub.state.as_ref() { if let Some(state) = sub.state.as_ref() {
let is_sth = fenwick.is_sth_at(i); let is_sth = caches.fenwick.is_sth_at(i);
state.for_each_cost_basis_pending(|&price, delta| { state.for_each_cost_basis_pending(|&price, delta| {
fenwick.apply_delta(price, delta, is_sth); caches.fenwick.apply_delta(price, delta, is_sth);
}); });
} }
} }
@@ -455,8 +466,7 @@ impl UTXOCohorts<Rw> {
.try_for_each(|vecs| { .try_for_each(|vecs| {
let sources = let sources =
filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter)); filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter));
vecs.metrics vecs.metrics.compute_from_sources(si, &sources, exit)
.compute_from_sources(si, &sources, exit)
}) })
}), }),
]; ];
@@ -483,8 +493,16 @@ impl UTXOCohorts<Rw> {
all.push(&mut self.all); all.push(&mut self.all);
all.push(&mut self.sth); all.push(&mut self.sth);
all.push(&mut self.lth); all.push(&mut self.lth);
all.extend(self.under_age.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); all.extend(
all.extend(self.over_age.iter_mut().map(|x| x as &mut dyn DynCohortVecs)); self.under_age
.iter_mut()
.map(|x| x as &mut dyn DynCohortVecs),
);
all.extend(
self.over_age
.iter_mut()
.map(|x| x as &mut dyn DynCohortVecs),
);
all.extend( all.extend(
self.over_amount self.over_amount
.iter_mut() .iter_mut()
@@ -542,7 +560,8 @@ impl UTXOCohorts<Rw> {
.metrics .metrics
.activity .activity
.transfer_volume .transfer_volume
.block.cents .block
.cents
.read_only_clone(); .read_only_clone();
let under_1h_value_destroyed = self let under_1h_value_destroyed = self
.age_range .age_range
@@ -567,7 +586,13 @@ impl UTXOCohorts<Rw> {
// Clone all_supply_sats and all_utxo_count for non-all cohorts. // Clone all_supply_sats and all_utxo_count for non-all cohorts.
let all_supply_sats = self.all.metrics.supply.total.sats.height.read_only_clone(); let all_supply_sats = self.all.metrics.supply.total.sats.height.read_only_clone();
let all_utxo_count = self.all.metrics.outputs.unspent_count.height.read_only_clone(); let all_utxo_count = self
.all
.metrics
.outputs
.unspent_count
.height
.read_only_clone();
// Destructure to allow parallel mutable access to independent fields. // Destructure to allow parallel mutable access to independent fields.
let Self { let Self {
@@ -636,9 +661,10 @@ impl UTXOCohorts<Rw> {
}) })
}), }),
Box::new(|| { Box::new(|| {
over_amount over_amount.par_iter_mut().try_for_each(|v| {
.par_iter_mut() v.metrics
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) .compute_rest_part2(prices, starting_indexes, au, exit)
})
}), }),
Box::new(|| { Box::new(|| {
epoch.par_iter_mut().try_for_each(|v| { epoch.par_iter_mut().try_for_each(|v| {
@@ -653,19 +679,22 @@ impl UTXOCohorts<Rw> {
}) })
}), }),
Box::new(|| { Box::new(|| {
amount_range amount_range.par_iter_mut().try_for_each(|v| {
.par_iter_mut() v.metrics
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) .compute_rest_part2(prices, starting_indexes, au, exit)
})
}), }),
Box::new(|| { Box::new(|| {
under_amount under_amount.par_iter_mut().try_for_each(|v| {
.par_iter_mut() v.metrics
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) .compute_rest_part2(prices, starting_indexes, au, exit)
})
}), }),
Box::new(|| { Box::new(|| {
type_ type_.par_iter_mut().try_for_each(|v| {
.par_iter_mut() v.metrics
.try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) .compute_rest_part2(prices, starting_indexes, au, exit)
})
}), }),
]; ];
@@ -829,12 +858,30 @@ impl UTXOCohorts<Rw> {
sth.metrics.realized.push_accum(&sth_acc); sth.metrics.realized.push_accum(&sth_acc);
lth.metrics.realized.push_accum(&lth_acc); lth.metrics.realized.push_accum(&lth_acc);
all.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(all_icap.0)); all.metrics
all.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(all_icap.1)); .unrealized
sth.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(sth_icap.0)); .investor_cap_in_profit_raw
sth.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(sth_icap.1)); .push(CentsSquaredSats::new(all_icap.0));
lth.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(lth_icap.0)); all.metrics
lth.metrics.unrealized.investor_cap_in_loss_raw.push(CentsSquaredSats::new(lth_icap.1)); .unrealized
.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(all_icap.1));
sth.metrics
.unrealized
.investor_cap_in_profit_raw
.push(CentsSquaredSats::new(sth_icap.0));
sth.metrics
.unrealized
.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(sth_icap.1));
lth.metrics
.unrealized
.investor_cap_in_profit_raw
.push(CentsSquaredSats::new(lth_icap.0));
lth.metrics
.unrealized
.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(lth_icap.1));
} }
} }

View File

@@ -24,7 +24,7 @@ impl UTXOCohorts {
date_opt: Option<Date>, date_opt: Option<Date>,
states_path: &Path, states_path: &Path,
) -> Result<()> { ) -> Result<()> {
if self.fenwick.is_initialized() { if self.caches.fenwick.is_initialized() {
self.push_fenwick_results(spot_price); self.push_fenwick_results(spot_price);
} }
@@ -38,18 +38,18 @@ impl UTXOCohorts {
/// Push all Fenwick-derived per-block results: percentiles, density, profitability. /// Push all Fenwick-derived per-block results: percentiles, density, profitability.
fn push_fenwick_results(&mut self, spot_price: Cents) { fn push_fenwick_results(&mut self, spot_price: Cents) {
let (all_d, sth_d, lth_d) = self.fenwick.density(spot_price); let (all_d, sth_d, lth_d) = self.caches.fenwick.density(spot_price);
let all = self.fenwick.percentiles_all(); let all = self.caches.fenwick.percentiles_all();
push_cost_basis(&all, all_d, &mut self.all.metrics.cost_basis); push_cost_basis(&all, all_d, &mut self.all.metrics.cost_basis);
let sth = self.fenwick.percentiles_sth(); let sth = self.caches.fenwick.percentiles_sth();
push_cost_basis(&sth, sth_d, &mut self.sth.metrics.cost_basis); push_cost_basis(&sth, sth_d, &mut self.sth.metrics.cost_basis);
let lth = self.fenwick.percentiles_lth(); let lth = self.caches.fenwick.percentiles_lth();
push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis); push_cost_basis(&lth, lth_d, &mut self.lth.metrics.cost_basis);
let prof = self.fenwick.profitability(spot_price); let prof = self.caches.fenwick.profitability(spot_price);
push_profitability(&prof, &mut self.profitability); push_profitability(&prof, &mut self.profitability);
} }

View File

@@ -42,7 +42,7 @@ impl UTXOCohorts<Rw> {
// Cohort 0 covers [0, 1) hours // Cohort 0 covers [0, 1) hours
// Cohort 20 covers [15*365*24, infinity) hours // Cohort 20 covers [15*365*24, infinity) hours
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect(); let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
let cached = &mut self.tick_tock_cached_positions; let cached = &mut self.caches.tick_tock_cached_positions;
// For each boundary (in hours), find blocks that just crossed it // For each boundary (in hours), find blocks that just crossed it
for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() { for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() {

View File

@@ -128,6 +128,9 @@ pub(crate) fn reset_state(
utxo_cohorts.reset_separate_cost_basis_data()?; utxo_cohorts.reset_separate_cost_basis_data()?;
addr_cohorts.reset_separate_cost_basis_data()?; addr_cohorts.reset_separate_cost_basis_data()?;
// Reset in-memory caches (fenwick, tick_tock positions)
utxo_cohorts.reset_caches();
Ok(RecoveredState { Ok(RecoveredState {
starting_height: Height::ZERO, starting_height: Height::ZERO,
}) })

View File

@@ -249,7 +249,6 @@ pub struct CostBasisData<S: Accumulate> {
pending: FxHashMap<CentsCompact, PendingDelta>, pending: FxHashMap<CentsCompact, PendingDelta>,
cache: Option<CachedUnrealizedState<S>>, cache: Option<CachedUnrealizedState<S>>,
rounding_digits: Option<i32>, rounding_digits: Option<i32>,
generation: u64,
investor_cap_raw: CentsSquaredSats, investor_cap_raw: CentsSquaredSats,
pending_investor_cap: PendingInvestorCapDelta, pending_investor_cap: PendingInvestorCapDelta,
} }
@@ -297,7 +296,6 @@ impl<S: Accumulate> CostBasisData<S> {
if self.pending.is_empty() { if self.pending.is_empty() {
return; return;
} }
self.generation = self.generation.wrapping_add(1);
let map = &mut self.map.as_mut().unwrap().map; let map = &mut self.map.as_mut().unwrap().map;
for (cents, PendingDelta { inc, dec }) in self.pending.drain() { for (cents, PendingDelta { inc, dec }) in self.pending.drain() {
match map.entry(cents) { match map.entry(cents) {
@@ -353,7 +351,6 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
pending: FxHashMap::default(), pending: FxHashMap::default(),
cache: None, cache: None,
rounding_digits: None, rounding_digits: None,
generation: 0,
investor_cap_raw: CentsSquaredSats::ZERO, investor_cap_raw: CentsSquaredSats::ZERO,
pending_investor_cap: PendingInvestorCapDelta::default(), pending_investor_cap: PendingInvestorCapDelta::default(),
} }

View File

@@ -4,8 +4,8 @@ use brk_error::Result;
use brk_indexer::Indexer; use brk_indexer::Indexer;
use brk_traversable::Traversable; use brk_traversable::Traversable;
use brk_types::{ use brk_types::{
Cents, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, Cents, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, Indexes,
Indexes, StoredF64, SupplyState, Timestamp, TxIndex, Version, StoredF64, SupplyState, Timestamp, TxIndex, Version,
}; };
use tracing::{debug, info}; use tracing::{debug, info};
use vecdb::{ use vecdb::{
@@ -23,15 +23,16 @@ use crate::{
state::BlockState, state::BlockState,
}, },
indexes, inputs, indexes, inputs,
internal::{CachedWindowStarts, PerBlockCumulativeRolling, db_utils::{finalize_db, open_db}}, internal::{
CachedWindowStarts, PerBlockCumulativeRolling,
db_utils::{finalize_db, open_db},
},
outputs, prices, transactions, outputs, prices, transactions,
}; };
use super::{ use super::{
AddrCohorts, AddrsDataVecs, AnyAddrIndexesVecs, RangeMap, UTXOCohorts, AddrCohorts, AddrsDataVecs, AnyAddrIndexesVecs, RangeMap, UTXOCohorts,
addr::{ addr::{AddrActivityVecs, AddrCountsVecs, DeltaVecs, NewAddrCountVecs, TotalAddrCountVecs},
AddrCountsVecs, AddrActivityVecs, DeltaVecs, NewAddrCountVecs, TotalAddrCountVecs,
},
}; };
const VERSION: Version = Version::new(22); const VERSION: Version = Version::new(22);
@@ -48,8 +49,7 @@ pub struct AddrMetricsVecs<M: StorageMode = Rw> {
pub funded_index: pub funded_index:
LazyVecFrom1<FundedAddrIndex, FundedAddrIndex, FundedAddrIndex, FundedAddrData>, LazyVecFrom1<FundedAddrIndex, FundedAddrIndex, FundedAddrIndex, FundedAddrData>,
#[traversable(wrap = "indexes", rename = "empty")] #[traversable(wrap = "indexes", rename = "empty")]
pub empty_index: pub empty_index: LazyVecFrom1<EmptyAddrIndex, EmptyAddrIndex, EmptyAddrIndex, EmptyAddrData>,
LazyVecFrom1<EmptyAddrIndex, EmptyAddrIndex, EmptyAddrIndex, EmptyAddrData>,
} }
#[derive(Traversable)] #[derive(Traversable)]
@@ -73,23 +73,26 @@ pub struct Vecs<M: StorageMode = Rw> {
pub coinblocks_destroyed: PerBlockCumulativeRolling<StoredF64, StoredF64, M>, pub coinblocks_destroyed: PerBlockCumulativeRolling<StoredF64, StoredF64, M>,
pub addrs: AddrMetricsVecs<M>, pub addrs: AddrMetricsVecs<M>,
/// In-memory block state for UTXO processing. Persisted via supply_state. /// In-memory state that does NOT survive rollback.
/// Kept across compute() calls to avoid O(n) rebuild on resume. /// Grouped so that adding a new field automatically gets it reset.
#[traversable(skip)] #[traversable(skip)]
chain_state: Vec<BlockState>, caches: DistributionTransientState,
/// In-memory tx_index→height reverse lookup. Kept across compute() calls. }
#[traversable(skip)]
tx_index_to_height: RangeMap<TxIndex, Height>,
/// Cached height→price mapping. Incrementally extended, O(new_blocks) on resume. /// In-memory state that does NOT survive rollback.
#[traversable(skip)] /// On rollback, the entire struct is replaced with `Default::default()`.
cached_prices: Vec<Cents>, #[derive(Clone, Default)]
/// Cached height→timestamp mapping. Incrementally extended, O(new_blocks) on resume. struct DistributionTransientState {
#[traversable(skip)] /// Block state for UTXO processing. Persisted via supply_state.
cached_timestamps: Vec<Timestamp>, chain_state: Vec<BlockState>,
/// Cached sparse table for O(1) range-max price queries. Incrementally extended. /// tx_index→height reverse lookup.
#[traversable(skip)] tx_index_to_height: RangeMap<TxIndex, Height>,
cached_price_range_max: PriceRangeMax, /// Height→price mapping. Incrementally extended.
prices: Vec<Cents>,
/// Height→timestamp mapping. Incrementally extended.
timestamps: Vec<Timestamp>,
/// Sparse table for O(1) range-max price queries. Incrementally extended.
price_range_max: PriceRangeMax,
} }
const SAVED_STAMPED_CHANGES: u16 = 10; const SAVED_STAMPED_CHANGES: u16 = 10;
@@ -109,9 +112,11 @@ impl Vecs {
let version = parent_version + VERSION; let version = parent_version + VERSION;
let utxo_cohorts = UTXOCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?; let utxo_cohorts =
UTXOCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?;
let addr_cohorts = AddrCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?; let addr_cohorts =
AddrCohorts::forced_import(&db, version, indexes, &states_path, cached_starts)?;
// Create address data BytesVecs first so we can also use them for identity mappings // Create address data BytesVecs first so we can also use them for identity mappings
let funded_addr_index_to_funded_addr_data = BytesVec::forced_import_with( let funded_addr_index_to_funded_addr_data = BytesVec::forced_import_with(
@@ -147,8 +152,7 @@ impl Vecs {
let total_addr_count = TotalAddrCountVecs::forced_import(&db, version, indexes)?; let total_addr_count = TotalAddrCountVecs::forced_import(&db, version, indexes)?;
// Per-block delta of total (global + per-type) // Per-block delta of total (global + per-type)
let new_addr_count = let new_addr_count = NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?;
NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?;
// Growth rate: delta change + rate (global + per-type) // Growth rate: delta change + rate (global + per-type)
let delta = DeltaVecs::new(version, &addr_count, cached_starts, indexes); let delta = DeltaVecs::new(version, &addr_count, cached_starts, indexes);
@@ -186,12 +190,7 @@ impl Vecs {
funded: funded_addr_index_to_funded_addr_data, funded: funded_addr_index_to_funded_addr_data,
empty: empty_addr_index_to_empty_addr_data, empty: empty_addr_index_to_empty_addr_data,
}, },
chain_state: Vec::new(), caches: DistributionTransientState::default(),
tx_index_to_height: RangeMap::default(),
cached_prices: Vec::new(),
cached_timestamps: Vec::new(),
cached_price_range_max: PriceRangeMax::default(),
db, db,
states_path, states_path,
@@ -201,6 +200,12 @@ impl Vecs {
Ok(this) Ok(this)
} }
/// Reset in-memory caches that become stale after rollback.
fn reset_in_memory_caches(&mut self) {
self.utxo_cohorts.reset_caches();
self.caches = DistributionTransientState::default();
}
/// Main computation loop. /// Main computation loop.
/// ///
/// Processes blocks to compute UTXO and address cohort metrics: /// Processes blocks to compute UTXO and address cohort metrics:
@@ -222,32 +227,6 @@ impl Vecs {
starting_indexes: &mut Indexes, starting_indexes: &mut Indexes,
exit: &Exit, exit: &Exit,
) -> Result<()> { ) -> Result<()> {
let cache_target_len = prices
.spot
.cents
.height
.len()
.min(blocks.time.timestamp_monotonic.len());
let cache_current_len = self.cached_prices.len();
if cache_target_len < cache_current_len {
self.cached_prices.truncate(cache_target_len);
self.cached_timestamps.truncate(cache_target_len);
self.cached_price_range_max.truncate(cache_target_len);
} else if cache_target_len > cache_current_len {
let new_prices = prices
.spot
.cents
.height
.collect_range_at(cache_current_len, cache_target_len);
let new_timestamps = blocks
.time
.timestamp_monotonic
.collect_range_at(cache_current_len, cache_target_len);
self.cached_prices.extend(new_prices);
self.cached_timestamps.extend(new_timestamps);
}
self.cached_price_range_max.extend(&self.cached_prices);
// 1. Find minimum height we have data for across stateful vecs // 1. Find minimum height we have data for across stateful vecs
let current_height = Height::from(self.supply_state.len()); let current_height = Height::from(self.supply_state.len());
let min_stateful = self.min_stateful_len(); let min_stateful = self.min_stateful_len();
@@ -281,9 +260,6 @@ impl Vecs {
&mut self.addr_cohorts, &mut self.addr_cohorts,
)?; )?;
if recovered.starting_height.is_zero() {
info!("State recovery validation failed, falling back to fresh start");
}
debug!( debug!(
"recover_state completed, starting_height={}", "recover_state completed, starting_height={}",
recovered.starting_height recovered.starting_height
@@ -295,12 +271,14 @@ impl Vecs {
debug!("recovered_height={}", recovered_height); debug!("recovered_height={}", recovered_height);
// Take chain_state and tx_index_to_height out of self to avoid borrow conflicts let needs_fresh_start = recovered_height.is_zero();
let mut chain_state = std::mem::take(&mut self.chain_state); let needs_rollback = recovered_height < current_height;
let mut tx_index_to_height = std::mem::take(&mut self.tx_index_to_height);
// Recover or reuse chain_state if needs_fresh_start || needs_rollback {
let starting_height = if recovered_height.is_zero() { self.reset_in_memory_caches();
}
if needs_fresh_start {
self.supply_state.reset()?; self.supply_state.reset()?;
self.addrs.funded.reset_height()?; self.addrs.funded.reset_height()?;
self.addrs.empty.reset_height()?; self.addrs.empty.reset_height()?;
@@ -311,11 +289,44 @@ impl Vecs {
&mut self.utxo_cohorts, &mut self.utxo_cohorts,
&mut self.addr_cohorts, &mut self.addr_cohorts,
)?; )?;
chain_state.clear();
tx_index_to_height.truncate(0);
info!("State recovery: fresh start"); info!("State recovery: fresh start");
}
// Populate price/timestamp caches from the prices module.
// Must happen AFTER rollback/reset (which clears caches) but BEFORE
// chain_state rebuild (which reads from them).
let cache_target_len = prices
.spot
.cents
.height
.len()
.min(blocks.time.timestamp_monotonic.len());
let cache_current_len = self.caches.prices.len();
if cache_target_len < cache_current_len {
self.caches.prices.truncate(cache_target_len);
self.caches.timestamps.truncate(cache_target_len);
self.caches.price_range_max.truncate(cache_target_len);
} else if cache_target_len > cache_current_len {
let new_prices = prices
.spot
.cents
.height
.collect_range_at(cache_current_len, cache_target_len);
let new_timestamps = blocks
.time
.timestamp_monotonic
.collect_range_at(cache_current_len, cache_target_len);
self.caches.prices.extend(new_prices);
self.caches.timestamps.extend(new_timestamps);
}
self.caches.price_range_max.extend(&self.caches.prices);
// Take chain_state and tx_index_to_height out of self to avoid borrow conflicts
let mut chain_state = std::mem::take(&mut self.caches.chain_state);
let mut tx_index_to_height = std::mem::take(&mut self.caches.tx_index_to_height);
// Recover or reuse chain_state
let starting_height = if recovered_height.is_zero() {
Height::ZERO Height::ZERO
} else if chain_state.len() == usize::from(recovered_height) { } else if chain_state.len() == usize::from(recovered_height) {
// Normal resume: chain_state already matches, reuse as-is // Normal resume: chain_state already matches, reuse as-is
@@ -335,8 +346,8 @@ impl Vecs {
.enumerate() .enumerate()
.map(|(h, supply)| BlockState { .map(|(h, supply)| BlockState {
supply, supply,
price: self.cached_prices[h], price: self.caches.prices[h],
timestamp: self.cached_timestamps[h], timestamp: self.caches.timestamps[h],
}) })
.collect(); .collect();
debug!("chain_state rebuilt"); debug!("chain_state rebuilt");
@@ -352,12 +363,11 @@ impl Vecs {
starting_indexes.height = starting_height; starting_indexes.height = starting_height;
} }
// 2b. Validate computed versions // 2c. Validate computed versions
debug!("validating computed versions"); debug!("validating computed versions");
let base_version = VERSION; let base_version = VERSION;
self.utxo_cohorts.validate_computed_versions(base_version)?; self.utxo_cohorts.validate_computed_versions(base_version)?;
self.addr_cohorts self.addr_cohorts.validate_computed_versions(base_version)?;
.validate_computed_versions(base_version)?;
debug!("computed versions validated"); debug!("computed versions validated");
// 3. Get last height from indexer // 3. Get last height from indexer
@@ -371,9 +381,9 @@ impl Vecs {
if starting_height <= last_height { if starting_height <= last_height {
debug!("calling process_blocks"); debug!("calling process_blocks");
let cached_prices = std::mem::take(&mut self.cached_prices); let prices = std::mem::take(&mut self.caches.prices);
let cached_timestamps = std::mem::take(&mut self.cached_timestamps); let timestamps = std::mem::take(&mut self.caches.timestamps);
let cached_price_range_max = std::mem::take(&mut self.cached_price_range_max); let price_range_max = std::mem::take(&mut self.caches.price_range_max);
process_blocks( process_blocks(
self, self,
@@ -386,27 +396,33 @@ impl Vecs {
last_height, last_height,
&mut chain_state, &mut chain_state,
&mut tx_index_to_height, &mut tx_index_to_height,
&cached_prices, &prices,
&cached_timestamps, &timestamps,
&cached_price_range_max, &price_range_max,
exit, exit,
)?; )?;
self.cached_prices = cached_prices; self.caches.prices = prices;
self.cached_timestamps = cached_timestamps; self.caches.timestamps = timestamps;
self.cached_price_range_max = cached_price_range_max; self.caches.price_range_max = price_range_max;
} }
// Put chain_state and tx_index_to_height back // Put chain_state and tx_index_to_height back
self.chain_state = chain_state; self.caches.chain_state = chain_state;
self.tx_index_to_height = tx_index_to_height; self.caches.tx_index_to_height = tx_index_to_height;
// 5. Compute aggregates (overlapping cohorts from separate cohorts) // 5. Compute aggregates (overlapping cohorts from separate cohorts)
info!("Computing overlapping cohorts..."); info!("Computing overlapping cohorts...");
{ {
let (r1, r2) = rayon::join( let (r1, r2) = rayon::join(
|| self.utxo_cohorts.compute_overlapping_vecs(starting_indexes, exit), || {
|| self.addr_cohorts.compute_overlapping_vecs(starting_indexes, exit), self.utxo_cohorts
.compute_overlapping_vecs(starting_indexes, exit)
},
|| {
self.addr_cohorts
.compute_overlapping_vecs(starting_indexes, exit)
},
); );
r1?; r1?;
r2?; r2?;
@@ -420,8 +436,14 @@ impl Vecs {
info!("Computing rest part 1..."); info!("Computing rest part 1...");
{ {
let (r1, r2) = rayon::join( let (r1, r2) = rayon::join(
|| self.utxo_cohorts.compute_rest_part1(prices, starting_indexes, exit), || {
|| self.addr_cohorts.compute_rest_part1(prices, starting_indexes, exit), self.utxo_cohorts
.compute_rest_part1(prices, starting_indexes, exit)
},
|| {
self.addr_cohorts
.compute_rest_part1(prices, starting_indexes, exit)
},
); );
r1?; r1?;
r2?; r2?;
@@ -442,11 +464,9 @@ impl Vecs {
self.addrs self.addrs
.activity .activity
.compute_rest(starting_indexes.height, exit)?; .compute_rest(starting_indexes.height, exit)?;
self.addrs.new.compute( self.addrs
starting_indexes.height, .new
&self.addrs.total, .compute(starting_indexes.height, &self.addrs.total, exit)?;
exit,
)?;
// 7. Compute rest part2 (relative metrics) // 7. Compute rest part2 (relative metrics)
let height_to_market_cap = self let height_to_market_cap = self
@@ -468,7 +488,14 @@ impl Vecs {
exit, exit,
)?; )?;
let all_utxo_count = self.utxo_cohorts.all.metrics.outputs.unspent_count.height.read_only_clone(); let all_utxo_count = self
.utxo_cohorts
.all
.metrics
.outputs
.unspent_count
.height
.read_only_clone();
self.addr_cohorts self.addr_cohorts
.compute_rest_part2(prices, starting_indexes, &all_utxo_count, exit)?; .compute_rest_part2(prices, starting_indexes, &all_utxo_count, exit)?;

View File

@@ -93,7 +93,7 @@ impl Vecs {
parent_version: Version, parent_version: Version,
indexer: &Indexer, indexer: &Indexer,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent, DB_NAME, 10_000_000)?; let db = open_db(parent, DB_NAME, 1_000_000)?;
let version = parent_version; let version = parent_version;

View File

@@ -17,7 +17,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 50_000_000)?; let db = open_db(parent_path, super::DB_NAME, 20_000_000)?;
let version = parent_version; let version = parent_version;
let spent = SpentVecs::forced_import(&db, version)?; let spent = SpentVecs::forced_import(&db, version)?;

View File

@@ -19,7 +19,7 @@ impl Vecs {
parent_version: Version, parent_version: Version,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 1_000_000)?; let db = open_db(parent_path, super::DB_NAME, 250_000)?;
let version = parent_version; let version = parent_version;
let ath = AthVecs::forced_import(&db, version, indexes)?; let ath = AthVecs::forced_import(&db, version, indexes)?;

View File

@@ -17,7 +17,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 50_000_000)?; let db = open_db(parent_path, super::DB_NAME, 1_000_000)?;
let version = parent_version; let version = parent_version;
let rewards = RewardsVecs::forced_import(&db, version, indexes, cached_starts)?; let rewards = RewardsVecs::forced_import(&db, version, indexes, cached_starts)?;

View File

@@ -17,7 +17,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 10_000_000)?; let db = open_db(parent_path, super::DB_NAME, 20_000_000)?;
let version = parent_version; let version = parent_version;
let spent = SpentVecs::forced_import(&db, version)?; let spent = SpentVecs::forced_import(&db, version)?;

View File

@@ -41,7 +41,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, DB_NAME, 1_000_000)?; let db = open_db(parent_path, DB_NAME, 100_000)?;
let pools = pools(); let pools = pools();
let version = parent_version + Version::new(3) + Version::new(pools.len() as u32); let version = parent_version + Version::new(3) + Version::new(pools.len() as u32);

View File

@@ -38,7 +38,7 @@ impl Vecs {
version: Version, version: Version,
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
) -> brk_error::Result<Self> { ) -> brk_error::Result<Self> {
let db = open_db(parent, DB_NAME, 1_000_000)?; let db = open_db(parent, DB_NAME, 100_000)?;
let this = Self::forced_import_inner(&db, version, indexes)?; let this = Self::forced_import_inner(&db, version, indexes)?;
finalize_db(&this.db, &this)?; finalize_db(&this.db, &this)?;
Ok(this) Ok(this)

View File

@@ -18,7 +18,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 50_000_000)?; let db = open_db(parent_path, super::DB_NAME, 1_000_000)?;
let version = parent_version; let version = parent_version;
let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?; let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?;

View File

@@ -26,7 +26,7 @@ impl Vecs {
cointime: &cointime::Vecs, cointime: &cointime::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent, super::DB_NAME, 10_000_000)?; let db = open_db(parent, super::DB_NAME, 1_000_000)?;
let version = parent_version + VERSION; let version = parent_version + VERSION;
let supply_metrics = &distribution.utxo_cohorts.all.metrics.supply; let supply_metrics = &distribution.utxo_cohorts.all.metrics.supply;

View File

@@ -19,7 +19,7 @@ impl Vecs {
indexes: &indexes::Vecs, indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts, cached_starts: &CachedWindowStarts,
) -> Result<Self> { ) -> Result<Self> {
let db = open_db(parent_path, super::DB_NAME, 50_000_000)?; let db = open_db(parent_path, super::DB_NAME, 10_000_000)?;
let version = parent_version; let version = parent_version;
let count = CountVecs::forced_import(&db, version, indexer, indexes, cached_starts)?; let count = CountVecs::forced_import(&db, version, indexer, indexes, cached_starts)?;

View File

@@ -110,6 +110,12 @@ impl Indexer {
debug!("Starting indexing..."); debug!("Starting indexing...");
let last_blockhash = self.vecs.blocks.blockhash.collect_last(); let last_blockhash = self.vecs.blocks.blockhash.collect_last();
// Rollback sim
// let last_blockhash = self
// .vecs
// .blocks
// .blockhash
// .collect_one_at(self.vecs.blocks.blockhash.len() - 2);
debug!("Last block hash found."); debug!("Last block hash found.");
let (starting_indexes, prev_hash) = if let Some(hash) = last_blockhash { let (starting_indexes, prev_hash) = if let Some(hash) = last_blockhash {

View File

@@ -49,7 +49,7 @@ impl Vecs {
tracing::debug!("Opening vecs database..."); tracing::debug!("Opening vecs database...");
let db = Database::open(&parent.join("vecs"))?; let db = Database::open(&parent.join("vecs"))?;
tracing::debug!("Setting min len..."); tracing::debug!("Setting min len...");
db.set_min_len(PAGE_SIZE * 50_000_000)?; db.set_min_len(PAGE_SIZE * 60_000_000)?;
let (blocks, transactions, inputs, outputs, addrs, scripts) = parallel_import! { let (blocks, transactions, inputs, outputs, addrs, scripts) = parallel_import! {
blocks = BlocksVecs::forced_import(&db, version), blocks = BlocksVecs::forced_import(&db, version),

View File

@@ -63,7 +63,8 @@ impl Query {
/// Current computed height (series) /// Current computed height (series)
pub fn computed_height(&self) -> Height { pub fn computed_height(&self) -> Height {
Height::from(self.computer().distribution.supply_state.len()) let len = self.computer().distribution.supply_state.len();
Height::from(len.saturating_sub(1))
} }
/// Minimum of indexed and computed heights /// Minimum of indexed and computed heights

View File

@@ -107,14 +107,6 @@ All errors return structured JSON with a consistent format:
), ),
..Default::default() ..Default::default()
}, },
Tag {
name: "Metrics".to_string(),
description: Some(
"Deprecated — use Series".to_string(),
),
extensions: [("x-deprecated".to_string(), serde_json::Value::Bool(true))].into(),
..Default::default()
},
Tag { Tag {
name: "Blocks".to_string(), name: "Blocks".to_string(),
description: Some( description: Some(
@@ -165,6 +157,14 @@ All errors return structured JSON with a consistent format:
), ),
..Default::default() ..Default::default()
}, },
Tag {
name: "Metrics".to_string(),
description: Some(
"Deprecated — use Series".to_string(),
),
extensions: [("deprecated".to_string(), serde_json::Value::Bool(true))].into(),
..Default::default()
},
]; ];
OpenApi { OpenApi {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -403,7 +403,7 @@ export function createChart({ parent, brk, fitContent }) {
if (!pane) return; if (!pane) return;
if (this.isAllHidden(paneIndex)) { if (this.isAllHidden(paneIndex)) {
const chartHeight = ichart.chartElement().clientHeight; const chartHeight = ichart.chartElement().clientHeight;
pane.setStretchFactor(chartHeight > 0 ? 32 / (chartHeight - 32) : 0); pane.setStretchFactor(chartHeight > 0 ? 48 / (chartHeight - 48) : 0);
} else { } else {
pane.setStretchFactor(1); pane.setStretchFactor(1);
} }
@@ -1445,7 +1445,7 @@ export function createChart({ parent, brk, fitContent }) {
const lastTd = ichart const lastTd = ichart
.chartElement() .chartElement()
.querySelector("table > tr:last-child > td:nth-child(2)"); .querySelector("table > tr:last-child > td:last-child");
const chart = { const chart = {
get panes() { get panes() {
@@ -1474,9 +1474,6 @@ export function createChart({ parent, brk, fitContent }) {
groups, groups,
id: "index", id: "index",
}); });
const sep = document.createElement("span");
sep.textContent = "|";
indexField.append(sep);
if (lastTd) lastTd.append(indexField); if (lastTd) lastTd.append(indexField);
}, },

View File

@@ -308,6 +308,12 @@ export function createSelect({
arrow.textContent = "↓"; arrow.textContent = "↓";
field.append(arrow); field.append(arrow);
} }
field.addEventListener("click", (e) => {
if (e.target !== select) {
select.showPicker();
}
});
} }
return field; return field;

View File

@@ -1,4 +1,5 @@
.chart { .chart {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
@@ -138,6 +139,7 @@
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
gap: 0.375rem; gap: 0.375rem;
cursor: pointer;
} }
table > tr { table > tr {
@@ -203,15 +205,18 @@
td:last-child > .field { td:last-child > .field {
position: absolute; position: absolute;
top: 0;
left: 0; left: 0;
right: 0;
z-index: 50; z-index: 50;
display: flex; display: inline-flex;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
align-items: center;
}
tr:not(:last-child) > td:last-child > .field {
top: 0;
right: 0;
gap: 0.375rem; gap: 0.375rem;
background-color: var(--background-color); background-color: var(--background-color);
align-items: center;
text-transform: uppercase; text-transform: uppercase;
padding-left: 0.625rem; padding-left: 0.625rem;
padding-top: 0.35rem; padding-top: 0.35rem;
@@ -232,10 +237,14 @@
} }
} }
tr:last-child > td:last-child > .field {
bottom: 2.125rem;
}
button.capture { button.capture {
position: absolute; position: absolute;
top: 0.5rem; top: -0.75rem;
right: 0.5rem; right: -0.75rem;
z-index: 50; z-index: 50;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
line-height: var(--line-height-xs); line-height: var(--line-height-xs);

View File

@@ -95,7 +95,6 @@ button {
} }
h1 { h1 {
text-transform: uppercase;
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
line-height: var(--line-height-xl); line-height: var(--line-height-xl);
font-weight: 300; font-weight: 300;
@@ -242,7 +241,6 @@ summary {
&::-webkit-details-marker { &::-webkit-details-marker {
display: none; display: none;
} }
} }
:is(a, button, summary) { :is(a, button, summary) {

View File

@@ -14,8 +14,34 @@
font-display: block; font-display: block;
} }
@font-face {
font-family: Instrument;
src: url("/assets/fonts/InstrumentSerif-Regular.woff2") format("woff2");
font-style: normal;
font-display: block;
}
@font-face {
font-family: Instrument;
src: url("/assets/fonts/InstrumentSerif-Italic.woff2") format("woff2");
font-style: italic;
font-display: block;
}
@font-face {
font-family: Satoshi;
src: url("/assets/fonts/Satoshi-Variable.woff2") format("woff2");
font-weight: 100 900;
font-display: block;
}
html { html {
font-family: font-family:
"Lilex", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Lilex", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace; "Liberation Mono", "Courier New", monospace;
} }
h1 {
font-family:
Instrument, Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
}

View File

@@ -44,14 +44,14 @@
white-space: nowrap; white-space: nowrap;
overflow-x: auto; overflow-x: auto;
padding-bottom: 1rem; padding-bottom: 1rem;
margin-bottom: -1rem; margin-bottom: -0.75rem;
padding-left: var(--main-padding); padding-left: var(--main-padding);
margin-left: var(--negative-main-padding); margin-left: var(--negative-main-padding);
padding-right: var(--main-padding); padding-right: var(--main-padding);
margin-right: var(--negative-main-padding); margin-right: var(--negative-main-padding);
h1 { h1 {
font-size: 1.375rem; font-size: 2rem;
letter-spacing: 0.075rem; letter-spacing: 0.075rem;
text-wrap: nowrap; text-wrap: nowrap;
} }