From cb9f277d497d4e1f757007c187574664bd7c0084 Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 1 Jun 2026 12:01:24 +0200 Subject: [PATCH] heatmaps: part 14 --- crates/brk_computer/src/prices/compute.rs | 68 ++++++++++--------- crates/brk_query/src/impl/oracle.rs | 82 +++++++---------------- website/src/heatmap/grid.js | 31 ++++----- website/src/heatmap/index.js | 9 ++- website/src/heatmap/lut.js | 2 +- website/src/heatmap/oracle.js | 2 +- website/src/heatmap/types.js | 8 ++- 7 files changed, 92 insertions(+), 110 deletions(-) diff --git a/crates/brk_computer/src/prices/compute.rs b/crates/brk_computer/src/prices/compute.rs index 3f3c25314..8c640d06e 100644 --- a/crates/brk_computer/src/prices/compute.rs +++ b/crates/brk_computer/src/prices/compute.rs @@ -127,39 +127,45 @@ impl Vecs { // Slow cold-start EMA up to START_HEIGHT_FAST, then switch to the fast // mature-market EMA. Steady-state runs start past START_HEIGHT_FAST and skip // the slow segment entirely. - let mut ref_bins = Vec::with_capacity(num_new); - if committed < START_HEIGHT_FAST { - let slow_end = START_HEIGHT_FAST.min(total_heights); - ref_bins.extend(Self::feed_blocks( - &mut oracle, - indexer, - committed..slow_end, - None, - )); - if slow_end == START_HEIGHT_FAST { - oracle.reconfigure(Config::default()); + { + let mut processed = 0usize; + let mut push_ref_bin = |ref_bin| { + self.spot + .cents + .height + .inner + .push(Cents::new(bin_to_cents(ref_bin))); + + processed += 1; + let progress = (processed * 100 / num_new) as u8; + if processed > 1 && progress > (((processed - 1) * 100 / num_new) as u8) { + info!("Oracle price computation: {}%", progress); + } + }; + + if committed < START_HEIGHT_FAST { + let slow_end = START_HEIGHT_FAST.min(total_heights); + Self::feed_blocks_with( + &mut oracle, + indexer, + committed..slow_end, + None, + |_, _, ref_bin| push_ref_bin(ref_bin), + ); + if slow_end == START_HEIGHT_FAST { + oracle.reconfigure(Config::default()); + } } - } - let fast_start = committed.max(START_HEIGHT_FAST); - if fast_start < total_heights { - ref_bins.extend(Self::feed_blocks( - &mut oracle, - indexer, - fast_start..total_heights, - None, - )); - } - for (i, ref_bin) in ref_bins.into_iter().enumerate() { - self.spot - .cents - .height - .inner - .push(Cents::new(bin_to_cents(ref_bin))); - - let progress = ((i + 1) * 100 / num_new) as u8; - if i > 0 && progress > ((i * 100 / num_new) as u8) { - info!("Oracle price computation: {}%", progress); + let fast_start = committed.max(START_HEIGHT_FAST); + if fast_start < total_heights { + Self::feed_blocks_with( + &mut oracle, + indexer, + fast_start..total_heights, + None, + |_, _, ref_bin| push_ref_bin(ref_bin), + ); } } diff --git a/crates/brk_query/src/impl/oracle.rs b/crates/brk_query/src/impl/oracle.rs index 13840333c..44ce1d5ed 100644 --- a/crates/brk_query/src/impl/oracle.rs +++ b/crates/brk_query/src/impl/oracle.rs @@ -4,15 +4,13 @@ use brk_computer::prices::Vecs as PricesVecs; use brk_error::{Error, Result}; use brk_indexer::Lengths; use brk_oracle::{ - Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, cents_to_bin, sats_to_bin, + cents_to_bin, sats_to_bin, Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, }; -use brk_types::{Day1, Dollars, Sats, TxOutIndex}; +use brk_types::{Day1, Dollars, TxOutIndex}; use vecdb::{AnyVec, ReadableVec, VecIndex}; use crate::Query; -const RAW_HISTOGRAM_VALUE_CHUNK: usize = 1_000_000; - impl Query { pub fn live_price(&self) -> Result { Ok(self.live_oracle()?.price_dollars()) @@ -233,70 +231,38 @@ impl Query { /// range instead of one block at a time. fn output_histogram_for_blocks(&self, range: Range, safe: &Lengths) -> HistogramRaw { let indexer = self.indexer(); + let safe_height = safe.height.to_usize(); let total_outputs = safe.txout_index.to_usize(); - let collect_end = (range.end + 1).min(safe.height.to_usize()); - let out_firsts: Vec = indexer + let out_start = indexer .vecs .outputs .first_txout_index - .collect_range_at(range.start, collect_end); - let out_start = out_firsts[0].to_usize(); - let out_end = out_firsts - .get(range.end - range.start) - .copied() - .unwrap_or(TxOutIndex::from(total_outputs)) + .collect_one_at(range.start) + .unwrap() .to_usize(); - - let mut hist = HistogramRaw::zeros(); - let mut values: Vec = Vec::new(); - let mut start = out_start; - while start < out_end { - let end = (start + RAW_HISTOGRAM_VALUE_CHUNK).min(out_end); - values.clear(); + let out_end = if range.end < safe_height { indexer .vecs .outputs - .value - .collect_range_into_at(start, end, &mut values); - add_sats_to_raw_histogram(&mut hist, &values); - start = end; + .first_txout_index + .collect_one_at(range.end) + .unwrap() + } else { + TxOutIndex::from(total_outputs) } + .to_usize(); + + let mut hist = HistogramRaw::zeros(); + indexer + .vecs + .outputs + .value + .for_each_range_at(out_start, out_end, |sats| { + if let Some(bin) = sats_to_bin(sats) { + hist.increment(bin); + } + }); hist } } - -fn add_sats_to_raw_histogram(hist: &mut HistogramRaw, values: &[Sats]) { - for &sats in values { - if let Some(bin) = sats_to_bin(sats) { - hist.increment(bin); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn raw_histogram_accumulation_is_additive() { - let values = [ - Sats::ZERO, - Sats::new(1), - Sats::new(10), - Sats::new(100_000_000), - Sats::new(1_000_000_000_000), - Sats::new(5_000_000_000), - ]; - - let mut one_shot = HistogramRaw::zeros(); - add_sats_to_raw_histogram(&mut one_shot, &values); - - let mut chunked = HistogramRaw::zeros(); - for chunk in values.chunks(2) { - add_sats_to_raw_histogram(&mut chunked, chunk); - } - - assert!(one_shot.iter().eq(chunked.iter())); - } -} diff --git a/website/src/heatmap/grid.js b/website/src/heatmap/grid.js index 05238ee7a..d04c9cac5 100644 --- a/website/src/heatmap/grid.js +++ b/website/src/heatmap/grid.js @@ -37,8 +37,7 @@ export function createAverageGrid({ const sums = new Float64Array(cols * rows); const counts = new Uint32Array(cols * rows); const maxByCol = new Float64Array(cols); - const cumulativeMaxByCol = new Float64Array(cols); - let cumulativeMaxDirty = true; + let maxValue = 0; const ySpan = yMax - yMin; /** @param {number} dateIndex */ @@ -85,7 +84,10 @@ export function createAverageGrid({ if (counts[index]) max = Math.max(max, sums[index] / counts[index]); } maxByCol[col] = max; - cumulativeMaxDirty = true; + maxValue = 0; + for (let c = 0; c < cols; c++) { + maxValue = Math.max(maxValue, maxByCol[c]); + } } /** @type {HeatmapGrid} */ @@ -99,7 +101,13 @@ export function createAverageGrid({ let dirty = false; if (points.kind === "implicit") { for (let i = 0; i < points.values.length; i++) { - if (addValue(col, points.yStart + i * points.yStep, points.values[i])) { + if ( + addValue( + col, + points.yStart + i * points.yStep, + points.values[i], + ) + ) { dirty = true; } } @@ -110,8 +118,9 @@ export function createAverageGrid({ } } if (!dirty) return undefined; + const previousMax = maxValue; updateColumnMax(col); - return col; + return { col, maxChanged: maxValue !== previousMax }; }, getValue(col, row) { if (col < 0 || col >= cols || row < 0 || row >= rows) { @@ -120,16 +129,8 @@ export function createAverageGrid({ const index = row * cols + col; return counts[index] ? sums[index] / counts[index] : Number.NaN; }, - getMaxValue(col = cols - 1) { - if (cumulativeMaxDirty) { - let max = 0; - for (let c = 0; c < cols; c++) { - max = Math.max(max, maxByCol[c]); - cumulativeMaxByCol[c] = max; - } - cumulativeMaxDirty = false; - } - return cumulativeMaxByCol[clamp(col, 0, cols - 1)] ?? 0; + getMaxValue() { + return maxValue; }, getDateIndexRange(col) { if (col < 0 || col >= cols || dates.length === 0) { diff --git a/website/src/heatmap/index.js b/website/src/heatmap/index.js index 69b4f4949..64db186e1 100644 --- a/website/src/heatmap/index.js +++ b/website/src/heatmap/index.js @@ -248,8 +248,13 @@ function rebuildGrid() { */ function addDateToGrid(dateIndex, points) { if (!currentGrid) return; - const dirtyCol = currentGrid.add(dateIndex, points); - if (dirtyCol !== undefined) schedulePaint(dirtyCol); + const result = currentGrid.add(dateIndex, points); + if (!result) return; + if (result.maxChanged) { + paint(); + } else { + schedulePaint(result.col); + } } /** diff --git a/website/src/heatmap/lut.js b/website/src/heatmap/lut.js index 30f9740c2..eb192f5d5 100644 --- a/website/src/heatmap/lut.js +++ b/website/src/heatmap/lut.js @@ -38,7 +38,7 @@ export function intensityColor({ light, dark }) { export function logIntensityColor({ light, dark }) { return (value, context) => { if (!Number.isFinite(value) || value <= 0) return 0x00000000; - const max = context.grid.getMaxValue(context.col); + const max = context.grid.getMaxValue(); if (max <= 0) return 0x00000000; const lut = context.dark ? dark : light; const t = Math.log2(value + 1) / Math.log2(max + 1); diff --git a/website/src/heatmap/oracle.js b/website/src/heatmap/oracle.js index 6aec9c93e..7cc63be6d 100644 --- a/website/src/heatmap/oracle.js +++ b/website/src/heatmap/oracle.js @@ -44,7 +44,7 @@ function createOracleHeatmapOption(mode, name) { kind: "heatmap", name, title: - mode === "outputs" ? "Output Value Histogram" : "Payment Output Histogram", + mode === "outputs" ? "Output Value Histogram" : "Payment Value Histogram", points: { fetch: (date, signal, onPoints) => fetchOraclePoints(mode, date, signal, onPoints), diff --git a/website/src/heatmap/types.js b/website/src/heatmap/types.js index ea1c0fcab..8ebe1718b 100644 --- a/website/src/heatmap/types.js +++ b/website/src/heatmap/types.js @@ -19,13 +19,17 @@ * @property {number} start * @property {number} end * + * @typedef {Object} HeatmapGridAddResult + * @property {number} col + * @property {boolean} maxChanged + * * @typedef {Object} HeatmapGrid * @property {readonly string[]} dates * @property {number} cols * @property {number} rows - * @property {(dateIndex: number, points: HeatmapPoints) => number | undefined} add + * @property {(dateIndex: number, points: HeatmapPoints) => HeatmapGridAddResult | undefined} add * @property {(col: number, row: number) => number} getValue - * @property {(col?: number) => number} getMaxValue + * @property {() => number} getMaxValue * @property {(col: number) => HeatmapRange} getDateIndexRange * @property {(row: number) => HeatmapRange} getYRange *