diff --git a/Cargo.lock b/Cargo.lock index b6090729d..d2c67baea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2538,9 +2538,9 @@ dependencies = [ [[package]] name = "rawdb" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1a1534553d4e626de325e000d3250490fb4020d8f177747615f345016c8f0a" +checksum = "dc7e70161aa9dbfcc1f858cae94eda70c9073bab5b22167bc6ab0f85d27054cf" dependencies = [ "libc", "log", @@ -3432,9 +3432,9 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" [[package]] name = "vecdb" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45f0491e73f467ff4dcb360d4341ad6281719362f29b040c2b769d18e161ab1" +checksum = "417cdef9fd0ada1659e1499c7180b3b8edf5256b99eb846c7f960c10a755ea3c" dependencies = [ "itoa", "libc", @@ -3455,9 +3455,9 @@ dependencies = [ [[package]] name = "vecdb_derive" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac60cdf47669f66acd6debfd66705464fb7c2519eed7b3692495d037f6d6399" +checksum = "aba470bc1a709df1efaace5885b25e7685988c64b61ac379758d861d12312735" dependencies = [ "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 30afa2cb1..95275b8f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", " tower-layer = "0.3" tracing = { version = "0.1", default-features = false, features = ["std"] } 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"] } [workspace.metadata.release] diff --git a/crates/brk_computer/src/blocks/import.rs b/crates/brk_computer/src/blocks/import.rs index ce27837ce..004859ffa 100644 --- a/crates/brk_computer/src/blocks/import.rs +++ b/crates/brk_computer/src/blocks/import.rs @@ -21,7 +21,7 @@ impl Vecs { indexer: &Indexer, indexes: &indexes::Vecs, ) -> Result { - 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 lookback = LookbackVecs::forced_import(&db, version)?; diff --git a/crates/brk_computer/src/cointime/import.rs b/crates/brk_computer/src/cointime/import.rs index 39df2e410..c7808d5bd 100644 --- a/crates/brk_computer/src/cointime/import.rs +++ b/crates/brk_computer/src/cointime/import.rs @@ -22,7 +22,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 v1 = version + Version::ONE; let activity = ActivityVecs::forced_import(&db, version, indexes, cached_starts)?; diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs b/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs index 4bc73eaad..0ed2e82cc 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/fenwick.rs @@ -3,7 +3,10 @@ use brk_types::{Cents, CentsCompact, Sats}; use crate::{ distribution::state::PendingDelta, - internal::{PERCENTILES, PERCENTILES_LEN, algo::{FenwickNode, FenwickTree}}, + internal::{ + PERCENTILES, PERCENTILES_LEN, + algo::{FenwickNode, FenwickTree}, + }, }; use super::COST_BASIS_PRICE_DIGITS; @@ -70,7 +73,6 @@ pub(super) struct CostBasisFenwick { // to a flat bucket index across two tiers. // --------------------------------------------------------------------------- -/// Map rounded dollars to a flat bucket index. /// Prices >= $1M are clamped to the last bucket. #[inline] 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] fn bucket_to_cents(bucket: usize) -> Cents { let dollars: u64 = if bucket < TIER1_START { @@ -96,24 +97,18 @@ fn bucket_to_cents(bucket: usize) -> Cents { Cents::from(dollars * 100) } -/// Map a CentsCompact price to a bucket index. #[inline] fn price_to_bucket(price: CentsCompact) -> usize { cents_to_bucket(price.into()) } -/// Map a Cents price to a bucket index. #[inline] fn cents_to_bucket(price: Cents) -> usize { dollars_to_bucket(u64::from(price.round_to_dollar(COST_BASIS_PRICE_DIGITS)) / 100) } -// --------------------------------------------------------------------------- -// CostBasisFenwick implementation -// --------------------------------------------------------------------------- - -impl CostBasisFenwick { - pub(super) fn new() -> Self { +impl Default for CostBasisFenwick { + fn default() -> Self { Self { tree: FenwickTree::new(TREE_SIZE), totals: CostBasisNode::default(), @@ -121,7 +116,9 @@ impl CostBasisFenwick { initialized: false, } } +} +impl CostBasisFenwick { pub(super) fn is_initialized(&self) -> bool { self.initialized } @@ -153,7 +150,8 @@ impl CostBasisFenwick { return; } 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.totals.add_assign(&delta); } @@ -236,8 +234,7 @@ impl CostBasisFenwick { sat_targets[PERCENTILES_LEN + 1] = total_sats - 1; // max let mut sat_buckets = [0usize; PERCENTILES_LEN + 2]; - self.tree - .kth(&sat_targets, &sat_field, &mut sat_buckets); + self.tree.kth(&sat_targets, &sat_field, &mut sat_buckets); result.min_price = bucket_to_cents(sat_buckets[0]); (0..PERCENTILES_LEN).for_each(|i| { @@ -253,8 +250,7 @@ impl CostBasisFenwick { } let mut usd_buckets = [0usize; PERCENTILES_LEN]; - self.tree - .kth(&usd_targets, &usd_field, &mut usd_buckets); + self.tree.kth(&usd_targets, &usd_field, &mut usd_buckets); (0..PERCENTILES_LEN).for_each(|i| { result.usd_prices[i] = bucket_to_cents(usd_buckets[i]); diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs index aeb4bb7c9..a7633e344 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/groups.rs @@ -1,8 +1,8 @@ use std::path::Path; use brk_cohort::{ - AgeRange, AmountRange, Class, ByEpoch, OverAmount, UnderAmount, UnderAge, - OverAge, SpendableType, CohortContext, Filter, Filtered, Term, + AgeRange, AmountRange, ByEpoch, Class, CohortContext, Filter, Filtered, OverAge, OverAmount, + SpendableType, Term, UnderAge, UnderAmount, }; use brk_error::Result; use brk_traversable::Traversable; @@ -17,8 +17,8 @@ use crate::{ distribution::{ DynCohortVecs, metrics::{ - AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, - CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, + AllCohortMetrics, BasicCohortMetrics, CohortMetricsBase, CoreCohortMetrics, + ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig, MinimalCohortMetrics, ProfitabilityMetrics, RealizedFullAccum, SupplyCore, TypeCohortMetrics, }, @@ -52,11 +52,16 @@ pub struct UTXOCohorts { pub profitability: ProfitabilityMetrics, pub matured: AgeRange>, #[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, /// Cached partition_point positions for tick_tock boundary searches. /// Avoids O(log n) binary search per boundary per block; scans forward /// from last known position (typically O(1) per boundary). - #[traversable(skip)] pub(super) tick_tock_cached_positions: [usize; 20], } @@ -258,7 +263,7 @@ impl UTXOCohorts { let prefix = CohortContext::Utxo.prefix(); let matured = AgeRange::try_new(&|_f: Filter, - name: &'static str| + name: &'static str| -> Result { AmountPerBlockCumulativeRolling::forced_import( db, @@ -284,24 +289,30 @@ impl UTXOCohorts { over_amount, profitability, matured, - fenwick: CostBasisFenwick::new(), - tick_tock_cached_positions: [0; 20], + caches: UTXOCohortsTransientState::default(), }) } + /// 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. /// Call after state import when all pending maps have been drained. pub(crate) fn init_fenwick_if_needed(&mut self) { - if self.fenwick.is_initialized() { + if self.caches.fenwick.is_initialized() { return; } let Self { sth, - fenwick, + caches, age_range, .. } = 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 .iter() @@ -312,27 +323,27 @@ impl UTXOCohorts { if map.is_empty() { return None; } - Some((map, fenwick.is_sth_at(i))) + Some((map, caches.fenwick.is_sth_at(i))) }) .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. /// Call after receive/send, before push_cohort_states. pub(crate) fn update_fenwick_from_pending(&mut self) { - if !self.fenwick.is_initialized() { + if !self.caches.fenwick.is_initialized() { return; } - // Destructure to get separate borrows on fenwick and age_range + // Destructure to get separate borrows on caches and age_range let Self { - fenwick, age_range, .. + caches, age_range, .. } = self; for (i, sub) in age_range.iter().enumerate() { 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| { - fenwick.apply_delta(price, delta, is_sth); + caches.fenwick.apply_delta(price, delta, is_sth); }); } } @@ -455,8 +466,7 @@ impl UTXOCohorts { .try_for_each(|vecs| { let sources = filter_minimal_sources_from(amr.iter(), Some(&vecs.metrics.filter)); - vecs.metrics - .compute_from_sources(si, &sources, exit) + vecs.metrics.compute_from_sources(si, &sources, exit) }) }), ]; @@ -483,8 +493,16 @@ impl UTXOCohorts { all.push(&mut self.all); all.push(&mut self.sth); all.push(&mut self.lth); - all.extend(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( + 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( self.over_amount .iter_mut() @@ -542,7 +560,8 @@ impl UTXOCohorts { .metrics .activity .transfer_volume - .block.cents + .block + .cents .read_only_clone(); let under_1h_value_destroyed = self .age_range @@ -567,7 +586,13 @@ impl UTXOCohorts { // 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_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. let Self { @@ -636,9 +661,10 @@ impl UTXOCohorts { }) }), Box::new(|| { - over_amount - .par_iter_mut() - .try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) + over_amount.par_iter_mut().try_for_each(|v| { + v.metrics + .compute_rest_part2(prices, starting_indexes, au, exit) + }) }), Box::new(|| { epoch.par_iter_mut().try_for_each(|v| { @@ -653,19 +679,22 @@ impl UTXOCohorts { }) }), Box::new(|| { - amount_range - .par_iter_mut() - .try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) + amount_range.par_iter_mut().try_for_each(|v| { + v.metrics + .compute_rest_part2(prices, starting_indexes, au, exit) + }) }), Box::new(|| { - under_amount - .par_iter_mut() - .try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) + under_amount.par_iter_mut().try_for_each(|v| { + v.metrics + .compute_rest_part2(prices, starting_indexes, au, exit) + }) }), Box::new(|| { - type_ - .par_iter_mut() - .try_for_each(|v| v.metrics.compute_rest_part2(prices, starting_indexes, au, exit)) + type_.par_iter_mut().try_for_each(|v| { + v.metrics + .compute_rest_part2(prices, starting_indexes, au, exit) + }) }), ]; @@ -829,12 +858,30 @@ impl UTXOCohorts { sth.metrics.realized.push_accum(&sth_acc); lth.metrics.realized.push_accum(<h_acc); - all.metrics.unrealized.investor_cap_in_profit_raw.push(CentsSquaredSats::new(all_icap.0)); - all.metrics.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)); + all.metrics + .unrealized + .investor_cap_in_profit_raw + .push(CentsSquaredSats::new(all_icap.0)); + all.metrics + .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)); } } diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs index d71484a7f..37a9573f7 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs @@ -24,7 +24,7 @@ impl UTXOCohorts { date_opt: Option, states_path: &Path, ) -> Result<()> { - if self.fenwick.is_initialized() { + if self.caches.fenwick.is_initialized() { self.push_fenwick_results(spot_price); } @@ -38,18 +38,18 @@ impl UTXOCohorts { /// Push all Fenwick-derived per-block results: percentiles, density, profitability. 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); - 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); - let lth = self.fenwick.percentiles_lth(); + let lth = self.caches.fenwick.percentiles_lth(); push_cost_basis(<h, 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); } diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs b/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs index a7df31039..d17d9ae3f 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/tick_tock.rs @@ -42,7 +42,7 @@ impl UTXOCohorts { // Cohort 0 covers [0, 1) 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 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 (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() { diff --git a/crates/brk_computer/src/distribution/compute/recover.rs b/crates/brk_computer/src/distribution/compute/recover.rs index e352ae1d7..7db1db946 100644 --- a/crates/brk_computer/src/distribution/compute/recover.rs +++ b/crates/brk_computer/src/distribution/compute/recover.rs @@ -128,6 +128,9 @@ pub(crate) fn reset_state( utxo_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 { starting_height: Height::ZERO, }) diff --git a/crates/brk_computer/src/distribution/state/cost_basis/data.rs b/crates/brk_computer/src/distribution/state/cost_basis/data.rs index 163cf684d..91f238905 100644 --- a/crates/brk_computer/src/distribution/state/cost_basis/data.rs +++ b/crates/brk_computer/src/distribution/state/cost_basis/data.rs @@ -249,7 +249,6 @@ pub struct CostBasisData { pending: FxHashMap, cache: Option>, rounding_digits: Option, - generation: u64, investor_cap_raw: CentsSquaredSats, pending_investor_cap: PendingInvestorCapDelta, } @@ -297,7 +296,6 @@ impl CostBasisData { if self.pending.is_empty() { return; } - self.generation = self.generation.wrapping_add(1); let map = &mut self.map.as_mut().unwrap().map; for (cents, PendingDelta { inc, dec }) in self.pending.drain() { match map.entry(cents) { @@ -353,7 +351,6 @@ impl CostBasisOps for CostBasisData { pending: FxHashMap::default(), cache: None, rounding_digits: None, - generation: 0, investor_cap_raw: CentsSquaredSats::ZERO, pending_investor_cap: PendingInvestorCapDelta::default(), } diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index 55c978b46..22bef272e 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -4,8 +4,8 @@ use brk_error::Result; use brk_indexer::Indexer; use brk_traversable::Traversable; use brk_types::{ - Cents, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, - Indexes, StoredF64, SupplyState, Timestamp, TxIndex, Version, + Cents, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, Indexes, + StoredF64, SupplyState, Timestamp, TxIndex, Version, }; use tracing::{debug, info}; use vecdb::{ @@ -23,15 +23,16 @@ use crate::{ state::BlockState, }, indexes, inputs, - internal::{CachedWindowStarts, PerBlockCumulativeRolling, db_utils::{finalize_db, open_db}}, + internal::{ + CachedWindowStarts, PerBlockCumulativeRolling, + db_utils::{finalize_db, open_db}, + }, outputs, prices, transactions, }; use super::{ AddrCohorts, AddrsDataVecs, AnyAddrIndexesVecs, RangeMap, UTXOCohorts, - addr::{ - AddrCountsVecs, AddrActivityVecs, DeltaVecs, NewAddrCountVecs, TotalAddrCountVecs, - }, + addr::{AddrActivityVecs, AddrCountsVecs, DeltaVecs, NewAddrCountVecs, TotalAddrCountVecs}, }; const VERSION: Version = Version::new(22); @@ -48,8 +49,7 @@ pub struct AddrMetricsVecs { pub funded_index: LazyVecFrom1, #[traversable(wrap = "indexes", rename = "empty")] - pub empty_index: - LazyVecFrom1, + pub empty_index: LazyVecFrom1, } #[derive(Traversable)] @@ -73,23 +73,26 @@ pub struct Vecs { pub coinblocks_destroyed: PerBlockCumulativeRolling, pub addrs: AddrMetricsVecs, - /// In-memory block state for UTXO processing. Persisted via supply_state. - /// Kept across compute() calls to avoid O(n) rebuild on resume. + /// In-memory state that does NOT survive rollback. + /// Grouped so that adding a new field automatically gets it reset. #[traversable(skip)] - chain_state: Vec, - /// In-memory tx_index→height reverse lookup. Kept across compute() calls. - #[traversable(skip)] - tx_index_to_height: RangeMap, + caches: DistributionTransientState, +} - /// Cached height→price mapping. Incrementally extended, O(new_blocks) on resume. - #[traversable(skip)] - cached_prices: Vec, - /// Cached height→timestamp mapping. Incrementally extended, O(new_blocks) on resume. - #[traversable(skip)] - cached_timestamps: Vec, - /// Cached sparse table for O(1) range-max price queries. Incrementally extended. - #[traversable(skip)] - cached_price_range_max: PriceRangeMax, +/// In-memory state that does NOT survive rollback. +/// On rollback, the entire struct is replaced with `Default::default()`. +#[derive(Clone, Default)] +struct DistributionTransientState { + /// Block state for UTXO processing. Persisted via supply_state. + chain_state: Vec, + /// tx_index→height reverse lookup. + tx_index_to_height: RangeMap, + /// Height→price mapping. Incrementally extended. + prices: Vec, + /// Height→timestamp mapping. Incrementally extended. + timestamps: Vec, + /// Sparse table for O(1) range-max price queries. Incrementally extended. + price_range_max: PriceRangeMax, } const SAVED_STAMPED_CHANGES: u16 = 10; @@ -109,9 +112,11 @@ impl Vecs { 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 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)?; // Per-block delta of total (global + per-type) - let new_addr_count = - NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?; + let new_addr_count = NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?; // Growth rate: delta change + rate (global + per-type) let delta = DeltaVecs::new(version, &addr_count, cached_starts, indexes); @@ -186,12 +190,7 @@ impl Vecs { funded: funded_addr_index_to_funded_addr_data, empty: empty_addr_index_to_empty_addr_data, }, - chain_state: Vec::new(), - tx_index_to_height: RangeMap::default(), - - cached_prices: Vec::new(), - cached_timestamps: Vec::new(), - cached_price_range_max: PriceRangeMax::default(), + caches: DistributionTransientState::default(), db, states_path, @@ -201,6 +200,12 @@ impl Vecs { 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. /// /// Processes blocks to compute UTXO and address cohort metrics: @@ -222,32 +227,6 @@ impl Vecs { starting_indexes: &mut Indexes, exit: &Exit, ) -> 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 let current_height = Height::from(self.supply_state.len()); let min_stateful = self.min_stateful_len(); @@ -281,9 +260,6 @@ impl Vecs { &mut self.addr_cohorts, )?; - if recovered.starting_height.is_zero() { - info!("State recovery validation failed, falling back to fresh start"); - } debug!( "recover_state completed, starting_height={}", recovered.starting_height @@ -295,12 +271,14 @@ impl Vecs { debug!("recovered_height={}", recovered_height); - // Take chain_state and tx_index_to_height out of self to avoid borrow conflicts - let mut chain_state = std::mem::take(&mut self.chain_state); - let mut tx_index_to_height = std::mem::take(&mut self.tx_index_to_height); + let needs_fresh_start = recovered_height.is_zero(); + let needs_rollback = recovered_height < current_height; - // Recover or reuse chain_state - let starting_height = if recovered_height.is_zero() { + if needs_fresh_start || needs_rollback { + self.reset_in_memory_caches(); + } + + if needs_fresh_start { self.supply_state.reset()?; self.addrs.funded.reset_height()?; self.addrs.empty.reset_height()?; @@ -311,11 +289,44 @@ impl Vecs { &mut self.utxo_cohorts, &mut self.addr_cohorts, )?; - - chain_state.clear(); - tx_index_to_height.truncate(0); - 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 } else if chain_state.len() == usize::from(recovered_height) { // Normal resume: chain_state already matches, reuse as-is @@ -335,8 +346,8 @@ impl Vecs { .enumerate() .map(|(h, supply)| BlockState { supply, - price: self.cached_prices[h], - timestamp: self.cached_timestamps[h], + price: self.caches.prices[h], + timestamp: self.caches.timestamps[h], }) .collect(); debug!("chain_state rebuilt"); @@ -352,12 +363,11 @@ impl Vecs { starting_indexes.height = starting_height; } - // 2b. Validate computed versions + // 2c. Validate computed versions debug!("validating computed versions"); let base_version = VERSION; self.utxo_cohorts.validate_computed_versions(base_version)?; - self.addr_cohorts - .validate_computed_versions(base_version)?; + self.addr_cohorts.validate_computed_versions(base_version)?; debug!("computed versions validated"); // 3. Get last height from indexer @@ -371,9 +381,9 @@ impl Vecs { if starting_height <= last_height { debug!("calling process_blocks"); - let cached_prices = std::mem::take(&mut self.cached_prices); - let cached_timestamps = std::mem::take(&mut self.cached_timestamps); - let cached_price_range_max = std::mem::take(&mut self.cached_price_range_max); + let prices = std::mem::take(&mut self.caches.prices); + let timestamps = std::mem::take(&mut self.caches.timestamps); + let price_range_max = std::mem::take(&mut self.caches.price_range_max); process_blocks( self, @@ -386,27 +396,33 @@ impl Vecs { last_height, &mut chain_state, &mut tx_index_to_height, - &cached_prices, - &cached_timestamps, - &cached_price_range_max, + &prices, + ×tamps, + &price_range_max, exit, )?; - self.cached_prices = cached_prices; - self.cached_timestamps = cached_timestamps; - self.cached_price_range_max = cached_price_range_max; + self.caches.prices = prices; + self.caches.timestamps = timestamps; + self.caches.price_range_max = price_range_max; } // Put chain_state and tx_index_to_height back - self.chain_state = chain_state; - self.tx_index_to_height = tx_index_to_height; + self.caches.chain_state = chain_state; + self.caches.tx_index_to_height = tx_index_to_height; // 5. Compute aggregates (overlapping cohorts from separate cohorts) info!("Computing overlapping cohorts..."); { 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?; r2?; @@ -420,8 +436,14 @@ impl Vecs { info!("Computing rest part 1..."); { 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?; r2?; @@ -442,11 +464,9 @@ impl Vecs { self.addrs .activity .compute_rest(starting_indexes.height, exit)?; - self.addrs.new.compute( - starting_indexes.height, - &self.addrs.total, - exit, - )?; + self.addrs + .new + .compute(starting_indexes.height, &self.addrs.total, exit)?; // 7. Compute rest part2 (relative metrics) let height_to_market_cap = self @@ -468,7 +488,14 @@ impl Vecs { 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 .compute_rest_part2(prices, starting_indexes, &all_utxo_count, exit)?; diff --git a/crates/brk_computer/src/indexes/mod.rs b/crates/brk_computer/src/indexes/mod.rs index 8e8bca6f3..303be424a 100644 --- a/crates/brk_computer/src/indexes/mod.rs +++ b/crates/brk_computer/src/indexes/mod.rs @@ -93,7 +93,7 @@ impl Vecs { parent_version: Version, indexer: &Indexer, ) -> Result { - let db = open_db(parent, DB_NAME, 10_000_000)?; + let db = open_db(parent, DB_NAME, 1_000_000)?; let version = parent_version; diff --git a/crates/brk_computer/src/inputs/import.rs b/crates/brk_computer/src/inputs/import.rs index 4f14611f3..372604334 100644 --- a/crates/brk_computer/src/inputs/import.rs +++ b/crates/brk_computer/src/inputs/import.rs @@ -17,7 +17,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 spent = SpentVecs::forced_import(&db, version)?; diff --git a/crates/brk_computer/src/market/import.rs b/crates/brk_computer/src/market/import.rs index 2a996bcc4..24091ae21 100644 --- a/crates/brk_computer/src/market/import.rs +++ b/crates/brk_computer/src/market/import.rs @@ -19,7 +19,7 @@ impl Vecs { parent_version: Version, indexes: &indexes::Vecs, ) -> Result { - 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 ath = AthVecs::forced_import(&db, version, indexes)?; diff --git a/crates/brk_computer/src/mining/import.rs b/crates/brk_computer/src/mining/import.rs index 8d6492d7d..09d0ee778 100644 --- a/crates/brk_computer/src/mining/import.rs +++ b/crates/brk_computer/src/mining/import.rs @@ -17,7 +17,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 rewards = RewardsVecs::forced_import(&db, version, indexes, cached_starts)?; diff --git a/crates/brk_computer/src/outputs/import.rs b/crates/brk_computer/src/outputs/import.rs index 86331b3f3..372604334 100644 --- a/crates/brk_computer/src/outputs/import.rs +++ b/crates/brk_computer/src/outputs/import.rs @@ -17,7 +17,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 spent = SpentVecs::forced_import(&db, version)?; diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index 487102b88..7612551c0 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -41,7 +41,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 version = parent_version + Version::new(3) + Version::new(pools.len() as u32); diff --git a/crates/brk_computer/src/prices/mod.rs b/crates/brk_computer/src/prices/mod.rs index adb98a649..7b8dc2de3 100644 --- a/crates/brk_computer/src/prices/mod.rs +++ b/crates/brk_computer/src/prices/mod.rs @@ -38,7 +38,7 @@ impl Vecs { version: Version, indexes: &indexes::Vecs, ) -> brk_error::Result { - 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)?; finalize_db(&this.db, &this)?; Ok(this) diff --git a/crates/brk_computer/src/scripts/import.rs b/crates/brk_computer/src/scripts/import.rs index a17660a24..8243ce8f6 100644 --- a/crates/brk_computer/src/scripts/import.rs +++ b/crates/brk_computer/src/scripts/import.rs @@ -18,7 +18,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 count = CountVecs::forced_import(&db, version, indexes, cached_starts)?; diff --git a/crates/brk_computer/src/supply/import.rs b/crates/brk_computer/src/supply/import.rs index 7bca20832..d6169529f 100644 --- a/crates/brk_computer/src/supply/import.rs +++ b/crates/brk_computer/src/supply/import.rs @@ -26,7 +26,7 @@ impl Vecs { cointime: &cointime::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 supply_metrics = &distribution.utxo_cohorts.all.metrics.supply; diff --git a/crates/brk_computer/src/transactions/import.rs b/crates/brk_computer/src/transactions/import.rs index 1a50a06c4..4be2a16e2 100644 --- a/crates/brk_computer/src/transactions/import.rs +++ b/crates/brk_computer/src/transactions/import.rs @@ -19,7 +19,7 @@ impl Vecs { indexes: &indexes::Vecs, cached_starts: &CachedWindowStarts, ) -> Result { - 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 count = CountVecs::forced_import(&db, version, indexer, indexes, cached_starts)?; diff --git a/crates/brk_indexer/src/lib.rs b/crates/brk_indexer/src/lib.rs index c8a6ed0ae..3fdd6faf3 100644 --- a/crates/brk_indexer/src/lib.rs +++ b/crates/brk_indexer/src/lib.rs @@ -110,6 +110,12 @@ impl Indexer { debug!("Starting indexing..."); 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."); let (starting_indexes, prev_hash) = if let Some(hash) = last_blockhash { diff --git a/crates/brk_indexer/src/vecs/mod.rs b/crates/brk_indexer/src/vecs/mod.rs index e45209d6c..2a95af6fb 100644 --- a/crates/brk_indexer/src/vecs/mod.rs +++ b/crates/brk_indexer/src/vecs/mod.rs @@ -49,7 +49,7 @@ impl Vecs { tracing::debug!("Opening vecs database..."); let db = Database::open(&parent.join("vecs"))?; 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! { blocks = BlocksVecs::forced_import(&db, version), diff --git a/crates/brk_query/src/lib.rs b/crates/brk_query/src/lib.rs index e7d844d21..fbd4a7bae 100644 --- a/crates/brk_query/src/lib.rs +++ b/crates/brk_query/src/lib.rs @@ -63,7 +63,8 @@ impl Query { /// Current computed height (series) 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 diff --git a/crates/brk_server/src/api/openapi/mod.rs b/crates/brk_server/src/api/openapi/mod.rs index 6b3490c5c..c048ee3b3 100644 --- a/crates/brk_server/src/api/openapi/mod.rs +++ b/crates/brk_server/src/api/openapi/mod.rs @@ -107,14 +107,6 @@ All errors return structured JSON with a consistent format: ), ..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 { name: "Blocks".to_string(), description: Some( @@ -165,6 +157,14 @@ All errors return structured JSON with a consistent format: ), ..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 { @@ -172,4 +172,4 @@ All errors return structured JSON with a consistent format: tags, ..OpenApi::default() } -} \ No newline at end of file +} diff --git a/website/assets/fonts/InstrumentSerif-Italic.woff2 b/website/assets/fonts/InstrumentSerif-Italic.woff2 new file mode 100644 index 000000000..77dd4c278 Binary files /dev/null and b/website/assets/fonts/InstrumentSerif-Italic.woff2 differ diff --git a/website/assets/fonts/InstrumentSerif-Regular.woff2 b/website/assets/fonts/InstrumentSerif-Regular.woff2 new file mode 100644 index 000000000..6160792bf Binary files /dev/null and b/website/assets/fonts/InstrumentSerif-Regular.woff2 differ diff --git a/website/assets/fonts/Satoshi-Variable.woff2 b/website/assets/fonts/Satoshi-Variable.woff2 new file mode 100644 index 000000000..b00e833ed Binary files /dev/null and b/website/assets/fonts/Satoshi-Variable.woff2 differ diff --git a/website/scripts/chart/index.js b/website/scripts/chart/index.js index 233f02b66..5b603df3f 100644 --- a/website/scripts/chart/index.js +++ b/website/scripts/chart/index.js @@ -403,7 +403,7 @@ export function createChart({ parent, brk, fitContent }) { if (!pane) return; if (this.isAllHidden(paneIndex)) { const chartHeight = ichart.chartElement().clientHeight; - pane.setStretchFactor(chartHeight > 0 ? 32 / (chartHeight - 32) : 0); + pane.setStretchFactor(chartHeight > 0 ? 48 / (chartHeight - 48) : 0); } else { pane.setStretchFactor(1); } @@ -1445,7 +1445,7 @@ export function createChart({ parent, brk, fitContent }) { const lastTd = ichart .chartElement() - .querySelector("table > tr:last-child > td:nth-child(2)"); + .querySelector("table > tr:last-child > td:last-child"); const chart = { get panes() { @@ -1474,9 +1474,6 @@ export function createChart({ parent, brk, fitContent }) { groups, id: "index", }); - const sep = document.createElement("span"); - sep.textContent = "|"; - indexField.append(sep); if (lastTd) lastTd.append(indexField); }, diff --git a/website/scripts/utils/dom.js b/website/scripts/utils/dom.js index 613273971..3fe785fb3 100644 --- a/website/scripts/utils/dom.js +++ b/website/scripts/utils/dom.js @@ -308,6 +308,12 @@ export function createSelect({ arrow.textContent = "↓"; field.append(arrow); } + + field.addEventListener("click", (e) => { + if (e.target !== select) { + select.showPicker(); + } + }); } return field; diff --git a/website/styles/chart.css b/website/styles/chart.css index 645007273..9c81df9c6 100644 --- a/website/styles/chart.css +++ b/website/styles/chart.css @@ -1,4 +1,5 @@ .chart { + position: relative; display: flex; flex-direction: column; min-height: 0; @@ -138,6 +139,7 @@ display: flex; flex-shrink: 0; gap: 0.375rem; + cursor: pointer; } table > tr { @@ -203,15 +205,18 @@ td:last-child > .field { position: absolute; - top: 0; left: 0; - right: 0; z-index: 50; - display: flex; + display: inline-flex; font-size: var(--font-size-xs); + align-items: center; + } + + tr:not(:last-child) > td:last-child > .field { + top: 0; + right: 0; gap: 0.375rem; background-color: var(--background-color); - align-items: center; text-transform: uppercase; padding-left: 0.625rem; padding-top: 0.35rem; @@ -232,10 +237,14 @@ } } + tr:last-child > td:last-child > .field { + bottom: 2.125rem; + } + button.capture { position: absolute; - top: 0.5rem; - right: 0.5rem; + top: -0.75rem; + right: -0.75rem; z-index: 50; font-size: var(--font-size-xs); line-height: var(--line-height-xs); diff --git a/website/styles/elements.css b/website/styles/elements.css index 34452058d..6fc3ba1ad 100644 --- a/website/styles/elements.css +++ b/website/styles/elements.css @@ -95,7 +95,6 @@ button { } h1 { - text-transform: uppercase; font-size: var(--font-size-xl); line-height: var(--line-height-xl); font-weight: 300; @@ -242,7 +241,6 @@ summary { &::-webkit-details-marker { display: none; } - } :is(a, button, summary) { diff --git a/website/styles/fonts.css b/website/styles/fonts.css index b2f5a39b1..e5a1c7604 100644 --- a/website/styles/fonts.css +++ b/website/styles/fonts.css @@ -14,8 +14,34 @@ 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 { font-family: "Lilex", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } + +h1 { + font-family: + Instrument, Charter, "Bitstream Charter", "Sitka Text", Cambria, serif; +} diff --git a/website/styles/panes/chart.css b/website/styles/panes/chart.css index b8c7fc91f..22e5ca29e 100644 --- a/website/styles/panes/chart.css +++ b/website/styles/panes/chart.css @@ -44,14 +44,14 @@ white-space: nowrap; overflow-x: auto; padding-bottom: 1rem; - margin-bottom: -1rem; + margin-bottom: -0.75rem; padding-left: var(--main-padding); margin-left: var(--negative-main-padding); padding-right: var(--main-padding); margin-right: var(--negative-main-padding); h1 { - font-size: 1.375rem; + font-size: 2rem; letter-spacing: 0.075rem; text-wrap: nowrap; }