use super::Engine;
use crate::engine::authority::geom::SYMBOL_SHEET;
use crate::engine::live_edges::{ReadLog, ReadRect, RecordingContext};
use crate::engine::scheduler::{Schedule, ScheduleUnit};
use crate::engine::vertex::VertexId;
use crate::interpreter::Interpreter;
use crate::traits::EvaluationContext;
use formualizer_common::{ExcelError, LiteralValue};
use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::Mutex;
#[derive(Default)]
pub(crate) struct Freshness {
armed: bool,
stale: Mutex<FxHashMap<VertexId, Vec<VertexId>>>,
fresh_reads: Mutex<FxHashMap<VertexId, Vec<ReadRect>>>,
barrier: bool,
skipped: Vec<VertexId>,
committed: crate::engine::idset::DenseIdSet,
unflushed: crate::engine::idset::DenseIdSet,
stale_this_pass: Vec<VertexId>,
group_dropped: FxHashSet<VertexId>,
hints: FxHashMap<VertexId, Vec<VertexId>>,
stale_total: u64,
barrier_stops: u64,
}
impl<R: EvaluationContext> Engine<R> {
pub(super) fn freshness_begin_request(&mut self) {
self.freshness = Freshness::default();
}
pub(super) fn freshness_begin_pass(&mut self, _schedule: &Schedule) {
let f = &mut self.freshness;
f.barrier = false;
f.skipped.clear();
f.committed.clear();
f.stale_this_pass.clear();
f.group_dropped.clear();
f.stale.get_mut().unwrap().clear();
f.fresh_reads.get_mut().unwrap().clear();
f.armed = true;
}
pub(super) fn freshness_extent_hints(
&self,
candidates: &[VertexId],
vdeps: &mut FxHashMap<VertexId, Vec<VertexId>>,
) {
use crate::engine::authority::geom::Rect;
use crate::engine::authority::store::TagFilter;
let anchors: Vec<(VertexId, u16, Rect)> = candidates
.iter()
.filter_map(|&v| {
let cells = self.graph.spill_cells_for_anchor(v)?;
let first = cells.first()?;
let (mut r0, mut c0, mut r1, mut c1) = (u32::MAX, u32::MAX, 0u32, 0u32);
for c in cells {
r0 = r0.min(c.coord.row());
c0 = c0.min(c.coord.col());
r1 = r1.max(c.coord.row());
c1 = c1.max(c.coord.col());
}
Some((v, first.sheet_id, Rect::new(r0, c0, r1, c1)))
})
.collect();
if anchors.is_empty() {
return;
}
let Ok(store) = self.graph.authority_plan_store() else {
return;
};
let wanted: FxHashSet<VertexId> = candidates.iter().copied().collect();
for (anchor, sheet, rect) in anchors {
let readers = store.direct_grid_dependents(sheet, &rect, TagFilter::All);
for (s, row, col) in readers.cells() {
let Some(reader) = self.graph.authority_vertex_of_cell((s, row, col)) else {
continue;
};
if reader != anchor && wanted.contains(&reader) {
let deps = vdeps.entry(reader).or_default();
if !deps.contains(&anchor) {
deps.push(anchor);
}
}
}
}
}
#[cfg(test)]
pub(crate) fn freshness_counters_for_test(&self) -> (u64, u64) {
(self.freshness.stale_total, self.freshness.barrier_stops)
}
pub(super) fn freshness_armed(&self) -> bool {
self.freshness.armed
}
pub(super) fn freshness_stop_after_unit(&mut self, schedule: &Schedule, index: usize) -> bool {
if !self.freshness.armed || !self.freshness.barrier {
return false;
}
for &unit in &schedule.units[index + 1..] {
match unit {
ScheduleUnit::Layer(i) => self
.freshness
.skipped
.extend_from_slice(&schedule.layers[i as usize].vertices),
ScheduleUnit::Cycle(i) => self
.freshness
.skipped
.extend_from_slice(&schedule.cycles[i as usize]),
}
}
self.freshness.barrier_stops += 1;
true
}
pub(super) fn freshness_drop_stale(&mut self, vertex: VertexId) -> bool {
if self.freshness.group_dropped.remove(&vertex) {
self.freshness
.fresh_reads
.get_mut()
.unwrap()
.remove(&vertex);
self.freshness.stale_this_pass.push(vertex);
self.freshness.barrier = true;
return true;
}
let Some(reads) = self.freshness.stale.get_mut().unwrap().remove(&vertex) else {
return false;
};
self.freshness.barrier = true;
self.freshness.stale_total += 1;
self.freshness.stale_this_pass.push(vertex);
let hints = self.freshness.hints.entry(vertex).or_default();
hints.extend(reads);
hints.sort_unstable();
hints.dedup();
true
}
pub(super) fn freshness_gate_group(&mut self, group: &[VertexId]) {
if !self.freshness.armed {
return;
}
let stale = self.freshness.stale.get_mut().unwrap();
if stale.is_empty() || !group.iter().any(|v| stale.contains_key(v)) {
return;
}
for &v in group {
if !stale.contains_key(&v) {
self.freshness.group_dropped.insert(v);
}
}
}
pub(super) fn freshness_group_commit_ok(&mut self) -> bool {
!self.freshness.armed
|| (self.freshness.group_dropped.is_empty()
&& self.freshness.stale.get_mut().unwrap().is_empty())
}
pub(super) fn freshness_mark_committed_group(&mut self, vertices: &[VertexId]) {
if !self.freshness.armed {
return;
}
self.freshness.committed.extend(vertices.iter().copied());
self.graph.clear_dirty_flags(vertices);
}
pub(super) fn freshness_note_unflushed(&mut self, vertices: &[VertexId]) {
self.freshness.unflushed.extend(vertices.iter().copied());
}
pub(super) fn freshness_flushed(&mut self) {
self.freshness.unflushed.clear();
}
pub(super) fn freshness_mark_committed(&mut self, vertex: VertexId) {
self.freshness.committed.insert(vertex);
self.graph.clear_dirty_flags(&[vertex]);
if let Some(reads) = self
.freshness
.fresh_reads
.get_mut()
.unwrap()
.remove(&vertex)
{
self.graph.authority_host_mut().set_observed(vertex, reads);
}
}
pub(super) fn freshness_finish_pass(
&mut self,
to_evaluate: &[VertexId],
changed: &[VertexId],
whole_workbook: bool,
) -> bool {
if !self.freshness.armed {
self.graph.clear_dirty_flags(to_evaluate);
for &v in changed {
self.graph.set_dirty(v, true);
}
return !changed.is_empty();
}
self.freshness.armed = false;
let keep: FxHashSet<VertexId> = self
.freshness
.skipped
.iter()
.copied()
.chain(self.freshness.stale_this_pass.iter().copied())
.collect();
let clear: Vec<VertexId> = to_evaluate
.iter()
.copied()
.filter(|v| !keep.contains(v) && !self.freshness.committed.contains(v))
.collect();
self.graph.clear_dirty_flags(&clear);
self.freshness.committed.clear();
for &v in changed {
self.graph.set_dirty(v, true);
}
!changed.is_empty()
|| if whole_workbook {
self.graph.has_dirty_evaluation_vertices()
} else {
to_evaluate.iter().any(|&v| self.graph.is_dirty(v))
}
}
pub(super) fn freshness_abort_pass(&mut self) {
if !self.freshness.armed {
return;
}
self.freshness.armed = false;
let committed: Vec<VertexId> = self.freshness.committed.iter().collect();
self.freshness.committed.clear();
for v in committed {
if !self.graph.is_dirty(v) && self.graph.is_live_formula_vertex(v) {
self.graph.set_dirty(v, true);
}
}
}
pub(super) fn freshness_has_hints(&self) -> bool {
!self.freshness.hints.is_empty()
}
pub(super) fn freshness_hints(&self, reader: VertexId) -> Option<&[VertexId]> {
self.freshness.hints.get(&reader).map(Vec::as_slice)
}
pub(super) fn freshness_merge_hints(
&self,
candidates: &[VertexId],
vdeps: &mut FxHashMap<VertexId, Vec<VertexId>>,
) {
if self.freshness.hints.is_empty() {
return;
}
for v in candidates {
if let Some(hints) = self.freshness.hints.get(v) {
let deps = vdeps.entry(*v).or_default();
deps.extend_from_slice(hints);
deps.sort_unstable();
deps.dedup();
}
}
}
pub(super) fn freshness_evaluate_recorded(
&self,
vertex: VertexId,
sheet_name: &str,
cell_ref: crate::reference::CellRef,
view: crate::engine::graph::FormulaView,
) -> Option<Result<LiteralValue, ExcelError>> {
if !self.freshness.armed || !self.graph.is_dynamic(vertex) {
return None;
}
let log = ReadLog::default();
let result = {
let ctx = RecordingContext::new(self, &log);
let interpreter = Interpreter::new_with_cell(&ctx, sheet_name, cell_ref);
interpreter
.evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
.map(|cv| {
let format = cv.format_id();
self.record_derived_format(vertex, format);
crate::engine::result_finalization::finalize_formula_result(cv.into_literal())
})
};
let reads = log.take();
let dirty = self.freshness_dirty_reads(vertex, &reads);
if dirty.is_empty() {
self.freshness
.fresh_reads
.lock()
.unwrap()
.insert(vertex, reads);
} else {
self.freshness.stale.lock().unwrap().insert(vertex, dirty);
}
Some(result)
}
fn freshness_dirty_reads(
&self,
reader: VertexId,
reads: &[(u16, u32, u32, u32, u32)],
) -> Vec<VertexId> {
let mut out = Vec::new();
let Ok(store) = self.graph.authority_plan_store() else {
return out;
};
let ids = store.ids();
for &(sheet, r0, c0, r1, c1) in reads {
if sheet == SYMBOL_SHEET {
continue;
}
for col in c0..=c1 {
ids.visit_runs_in(sheet, col, r0, r1, &mut |h| {
let run = ids.run(h);
let a = run.row_start.max(r0);
let b = (run.row_start + run.len - 1).min(r1);
for row in a..=b {
let id = run.first_id + (row - run.row_start);
if let Some(v) = self
.graph
.authority_vertex_of_formula(id, (sheet, row, col))
&& v != reader
&& (self.graph.is_dirty(v) || self.freshness.unflushed.contains(&v))
{
out.push(v);
}
}
});
}
for anchor in self.graph.spill_anchors_in_region(sheet, r0, c0, r1, c1) {
if anchor != reader && self.graph.is_dirty(anchor) {
out.push(anchor);
}
}
}
out.sort_unstable();
out.dedup();
out
}
}