use super::lazy_list_measure::{LazyListMeasureConfig, DEFAULT_ITEM_SIZE_ESTIMATE};
use super::lazy_list_measured_item::LazyListMeasuredItem;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use web_time::{Duration, Instant};
const DEFAULT_TIME_BUDGET: Duration = Duration::from_millis(50);
const MAX_VISIBLE_ITEMS_SAFETY: usize = 10000;
const MIN_ESTIMATED_ITEM_EXTENT: f32 = 1.0;
static LAZY_MEASURE_TELEMETRY_ENABLED: OnceLock<bool> = OnceLock::new();
static LAZY_MEASURE_PASS_COUNTER: AtomicU64 = AtomicU64::new(1);
fn lazy_measure_telemetry_enabled() -> bool {
*LAZY_MEASURE_TELEMETRY_ENABLED
.get_or_init(|| std::env::var_os("CRANPOSE_LAZY_MEASURE_TELEMETRY").is_some())
}
#[derive(Debug)]
pub struct ItemMeasurePass {
pub items: Vec<LazyListMeasuredItem>,
pub start_index: usize,
pub start_offset: f32,
pub next_index: usize,
pub next_offset: f32,
pub measured_visible_items: usize,
pub hit_time_budget: bool,
pub viewport_filled: bool,
}
pub struct ItemMeasurer<'a, F> {
measure_fn: &'a mut F,
pre_measured: VecDeque<LazyListMeasuredItem>,
config: &'a LazyListMeasureConfig,
items_count: usize,
effective_viewport_size: f32,
average_item_size: f32,
}
impl<'a, F> ItemMeasurer<'a, F>
where
F: FnMut(usize) -> LazyListMeasuredItem,
{
pub fn new(
measure_fn: &'a mut F,
config: &'a LazyListMeasureConfig,
items_count: usize,
effective_viewport_size: f32,
average_item_size: f32,
pre_measured: VecDeque<LazyListMeasuredItem>,
) -> Self {
Self {
measure_fn,
pre_measured,
config,
items_count,
effective_viewport_size,
average_item_size,
}
}
pub fn measure_all(
&mut self,
first_item_index: usize,
first_item_scroll_offset: f32,
) -> ItemMeasurePass {
let start_time = Instant::now();
let start_offset = self.config.before_content_padding - first_item_scroll_offset;
let viewport_end = self.effective_viewport_size - self.config.after_content_padding;
let (mut visible_items, current_index, current_offset, hit_time_budget) =
self.measure_visible(first_item_index, start_offset, viewport_end, start_time);
let measured_visible_items = visible_items.len();
if !hit_time_budget {
self.measure_beyond_after(
current_index,
current_offset,
&mut visible_items,
start_time,
);
if first_item_index > 0 && !visible_items.is_empty() {
let before_items = self.measure_beyond_before(
first_item_index,
visible_items[0].offset,
visible_items.len(),
start_time,
);
if !before_items.is_empty() {
let mut combined = before_items;
combined.append(&mut visible_items);
visible_items = combined;
}
}
}
let viewport_filled = current_offset >= viewport_end || current_index >= self.items_count;
let pass = ItemMeasurePass {
items: visible_items,
start_index: first_item_index,
start_offset,
next_index: current_index,
next_offset: current_offset,
measured_visible_items,
hit_time_budget,
viewport_filled,
};
if lazy_measure_telemetry_enabled() {
let pass_id = LAZY_MEASURE_PASS_COUNTER.fetch_add(1, Ordering::Relaxed);
log::warn!(
"[lazy-measure-telemetry] pass={} start_index={} start_offset={:.2} measured_visible={} next_index={} next_offset={:.2} viewport_end={:.2} viewport_filled={} hit_time_budget={} elapsed_ms={:.2}",
pass_id,
pass.start_index,
pass.start_offset,
pass.measured_visible_items,
pass.next_index,
pass.next_offset,
viewport_end,
pass.viewport_filled,
pass.hit_time_budget,
start_time.elapsed().as_secs_f64() * 1000.0
);
}
pass
}
fn measure_visible(
&mut self,
start_index: usize,
start_offset: f32,
viewport_end: f32,
start_time: Instant,
) -> (Vec<LazyListMeasuredItem>, usize, f32, bool) {
let mut items = Vec::with_capacity(self.estimated_measure_capacity(
start_index,
start_offset,
viewport_end,
));
let mut current_index = start_index;
let mut current_offset = start_offset;
let mut hit_time_budget = false;
while current_index < self.items_count
&& current_offset < viewport_end
&& items.len() < MAX_VISIBLE_ITEMS_SAFETY
{
if start_time.elapsed() > DEFAULT_TIME_BUDGET {
hit_time_budget = true;
log::warn!(
"Lazy list measurement exceeded time budget ({:?}) at index {}. stopping early.",
DEFAULT_TIME_BUDGET,
current_index
);
break;
}
let mut item = self
.take_pre_measured(current_index)
.unwrap_or_else(|| (self.measure_fn)(current_index));
item.offset = current_offset;
current_offset += item.main_axis_size + self.config.spacing;
items.push(item);
current_index += 1;
}
if items.len() >= MAX_VISIBLE_ITEMS_SAFETY && current_offset < viewport_end {
log::warn!(
"MAX_VISIBLE_ITEMS ({}) reached while viewport has remaining space ({:.0}px) - \
viewport may be under-filled. Consider using larger items.",
MAX_VISIBLE_ITEMS_SAFETY,
viewport_end - current_offset
);
}
(items, current_index, current_offset, hit_time_budget)
}
fn measure_beyond_after(
&mut self,
mut current_index: usize,
mut current_offset: f32,
items: &mut Vec<LazyListMeasuredItem>,
start_time: Instant,
) {
let after_count = self
.config
.beyond_bounds_item_count
.min(self.items_count - current_index);
for _ in 0..after_count {
if current_index >= self.items_count {
break;
}
if start_time.elapsed() > DEFAULT_TIME_BUDGET {
break;
}
let mut item = self
.take_pre_measured(current_index)
.unwrap_or_else(|| (self.measure_fn)(current_index));
item.offset = current_offset;
current_offset += item.main_axis_size + self.config.spacing;
items.push(item);
current_index += 1;
}
}
fn measure_beyond_before(
&mut self,
first_index: usize,
first_offset: f32,
following_item_count: usize,
start_time: Instant,
) -> Vec<LazyListMeasuredItem> {
let before_count = self.config.beyond_bounds_item_count.min(first_index);
if before_count == 0 {
return Vec::new();
}
let mut before_items =
Vec::with_capacity(before_count.saturating_add(following_item_count));
let mut before_offset = first_offset;
for i in 0..before_count {
if start_time.elapsed() > DEFAULT_TIME_BUDGET {
break;
}
let idx = first_index - 1 - i;
let mut item = self
.take_pre_measured(idx)
.unwrap_or_else(|| (self.measure_fn)(idx));
before_offset -= item.main_axis_size + self.config.spacing;
item.offset = before_offset;
before_items.push(item);
}
before_items.reverse();
before_items
}
fn estimated_measure_capacity(
&self,
start_index: usize,
start_offset: f32,
viewport_end: f32,
) -> usize {
let remaining_items = self
.items_count
.saturating_sub(start_index)
.min(MAX_VISIBLE_ITEMS_SAFETY);
if remaining_items == 0 || start_offset >= viewport_end {
return 0;
}
let viewport_span = viewport_end - start_offset;
if !viewport_span.is_finite() || viewport_span <= 0.0 {
return 0;
}
let estimated_visible = (viewport_span / self.estimated_item_extent())
.ceil()
.min(MAX_VISIBLE_ITEMS_SAFETY as f32) as usize;
estimated_visible
.saturating_add(self.config.beyond_bounds_item_count)
.min(remaining_items)
}
fn estimated_item_extent(&self) -> f32 {
let item_size = if self.average_item_size.is_finite() && self.average_item_size > 0.0 {
self.average_item_size
} else {
DEFAULT_ITEM_SIZE_ESTIMATE
};
(item_size + self.config.spacing.max(0.0)).max(MIN_ESTIMATED_ITEM_EXTENT)
}
fn take_pre_measured(&mut self, index: usize) -> Option<LazyListMeasuredItem> {
let front_matches = self
.pre_measured
.front()
.is_some_and(|item| item.index == index);
if front_matches {
self.pre_measured.pop_front()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
fn create_test_item(index: usize, size: f32) -> LazyListMeasuredItem {
LazyListMeasuredItem::new(index, index as u64, None, size, 100.0)
}
#[test]
fn test_measure_fills_viewport() {
let config = LazyListMeasureConfig::default();
let mut measure = |i| create_test_item(i, 50.0);
let mut measurer =
ItemMeasurer::new(&mut measure, &config, 100, 200.0, 50.0, VecDeque::new());
let pass = measurer.measure_all(0, 0.0);
let items = pass.items;
assert!(items.len() >= 4);
assert_eq!(items[0].index, 0);
assert_eq!(items[0].offset, 0.0);
}
#[test]
fn test_measure_with_offset() {
let config = LazyListMeasureConfig::default();
let mut measure = |i| create_test_item(i, 50.0);
let mut measurer =
ItemMeasurer::new(&mut measure, &config, 100, 200.0, 50.0, VecDeque::new());
let pass = measurer.measure_all(5, 25.0);
let items = pass.items;
assert!(items.iter().any(|i| i.index == 3));
assert!(items.iter().any(|i| i.index == 5));
}
#[test]
fn test_measure_respects_items_count() {
let config = LazyListMeasureConfig::default();
let mut measure = |i| create_test_item(i, 50.0);
let mut measurer =
ItemMeasurer::new(&mut measure, &config, 3, 1000.0, 50.0, VecDeque::new());
let pass = measurer.measure_all(0, 0.0);
let items = pass.items;
assert_eq!(items.len(), 3);
}
#[test]
fn test_measure_with_spacing() {
let config = LazyListMeasureConfig {
spacing: 10.0,
..Default::default()
};
let mut measure = |i| create_test_item(i, 50.0);
let mut measurer =
ItemMeasurer::new(&mut measure, &config, 100, 200.0, 50.0, VecDeque::new());
let pass = measurer.measure_all(0, 0.0);
let items = pass.items;
assert_eq!(items[0].offset, 0.0);
assert_eq!(items[1].offset, 60.0); }
#[test]
fn measure_capacity_uses_average_item_size_and_beyond_bounds() {
let config = LazyListMeasureConfig {
beyond_bounds_item_count: 2,
..Default::default()
};
let mut measure = |i| create_test_item(i, 50.0);
let measurer = ItemMeasurer::new(&mut measure, &config, 100, 200.0, 50.0, VecDeque::new());
assert_eq!(measurer.estimated_measure_capacity(0, 0.0, 200.0), 6);
}
#[test]
fn measure_capacity_falls_back_for_invalid_average_item_size() {
let config = LazyListMeasureConfig::default();
let mut measure = |i| create_test_item(i, 50.0);
let measurer =
ItemMeasurer::new(&mut measure, &config, 100, 200.0, f32::NAN, VecDeque::new());
assert_eq!(measurer.estimated_measure_capacity(0, 0.0, 96.0), 4);
}
#[test]
fn measure_beyond_before_reserves_capacity_for_following_items() {
let config = LazyListMeasureConfig {
beyond_bounds_item_count: 2,
..Default::default()
};
let mut measure = |i| create_test_item(i, 50.0);
let mut measurer =
ItemMeasurer::new(&mut measure, &config, 100, 200.0, 50.0, VecDeque::new());
let before_items = measurer.measure_beyond_before(5, 0.0, 6, Instant::now());
assert_eq!(
before_items
.iter()
.map(|item| item.index)
.collect::<Vec<_>>(),
vec![3, 4]
);
assert!(before_items.capacity() >= 8);
}
}