Skip to main content

formualizer_eval/engine/
resumable.rs

1#![doc = include_str!("resumable.md")]
2use super::{CancelToken, EvalResult};
3use std::time::Duration;
4
5/// Controls one call. Deadlines are soft: at least one unit may finish.
6#[derive(Clone, Debug, Default)]
7#[non_exhaustive]
8pub struct EvaluationStop {
9    /// Soft time allowance for this call, excluding time between calls.
10    pub soft_deadline: Option<Duration>,
11    /// Cooperative cancellation checked at installation boundaries.
12    pub cancel: Option<CancelToken>,
13}
14impl EvaluationStop {
15    /// Stop at the first eligible boundary after this duration.
16    pub fn after(duration: Duration) -> Self {
17        Self {
18            soft_deadline: Some(duration),
19            cancel: None,
20        }
21    }
22    /// Stop when this token is cancelled.
23    pub fn cancel(token: CancelToken) -> Self {
24        Self::none().with_cancel(token)
25    }
26    /// Run without a soft deadline or cancellation token.
27    pub fn none() -> Self {
28        Self::default()
29    }
30    /// Add cancellation to an existing deadline policy.
31    pub fn with_cancel(mut self, token: CancelToken) -> Self {
32        self.cancel = Some(token);
33        self
34    }
35}
36#[derive(Debug)]
37#[non_exhaustive]
38/// Result of an implicit cooperative recalculation call.
39pub enum EvaluationProgress {
40    /// The logical recalculation finished.
41    Complete(EvalResult),
42    /// Installed work is preserved for another call.
43    Incomplete(IncompleteEvaluation),
44}
45#[derive(Debug)]
46#[non_exhaustive]
47/// Telemetry for an unfinished implicit recalculation.
48pub struct IncompleteEvaluation {
49    /// Cooperative stop that ended this call.
50    pub reason: StopReason,
51    /// Vertices computed during this call.
52    pub computed_vertices: usize,
53}
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55#[non_exhaustive]
56/// Why an implicit call stopped before completion.
57pub enum StopReason {
58    /// The soft duration expired.
59    Deadline,
60    /// The cancellation token was set.
61    Cancelled,
62}
63
64/// A job lifecycle or evaluation failure.
65#[derive(Debug)]
66#[non_exhaustive]
67pub enum RecalcJobError {
68    /// An earlier engine-internal unwind permanently disabled this engine.
69    Poisoned,
70    /// No explicit job exists.
71    NoActiveJob,
72    /// A logical recalculation already exists.
73    AlreadyPending {
74        /// True for an explicit job; false for an implicit continuation.
75        explicit: bool,
76    },
77    /// A step failed. The job has ended; unfinished work remains dirty.
78    Evaluation(formualizer_common::ExcelError),
79}
80impl std::fmt::Display for RecalcJobError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::Poisoned => f.write_str(super::poison::POISON_MESSAGE),
84            Self::NoActiveJob => f.write_str("no active recalculation job"),
85            Self::AlreadyPending { explicit } => {
86                write!(f, "recalculation already pending (explicit: {explicit})")
87            }
88            Self::Evaluation(error) => write!(f, "recalculation failed: {error}"),
89        }
90    }
91}
92impl std::error::Error for RecalcJobError {
93    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
94        match self {
95            Self::Evaluation(error) => Some(error),
96            _ => None,
97        }
98    }
99}
100impl From<formualizer_common::ExcelError> for RecalcJobError {
101    fn from(error: formualizer_common::ExcelError) -> Self {
102        Self::Evaluation(error)
103    }
104}
105
106/// Terminal or suspended outcome of the closure driver.
107#[derive(Debug)]
108#[non_exhaustive]
109pub enum RecalcOutcome {
110    /// All work finished; the job is no longer active.
111    Finished(EvalResult),
112    /// Cancellation ended the job.
113    Cancelled,
114    /// The callback broke; the job remains active for manual stepping.
115    Paused,
116}
117
118/// Options for an engine-owned recalculation job.
119#[derive(Clone, Debug)]
120#[non_exhaustive]
121pub struct RecalcJobOptions {
122    /// Collect changed regions on each step (default true).
123    pub collect_deltas: bool,
124    /// Maximum coalesced records returned per step (default 1,024).
125    /// Overflow returns sheet identities instead; collection itself is per-step.
126    pub max_delta_records: usize,
127    /// Initial viewport targets, materialized lazily after full planning.
128    pub initial_priority: Vec<super::EvaluationTarget>,
129}
130impl Default for RecalcJobOptions {
131    fn default() -> Self {
132        Self {
133            collect_deltas: true,
134            max_delta_records: 1024,
135            initial_priority: Vec::new(),
136        }
137    }
138}
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140#[non_exhaustive]
141/// Phase reached at the end of a step.
142pub enum StepPhase {
143    /// Staged formulas were built into the graph.
144    BuildingGraph,
145    /// The full or priority schedule was prepared.
146    Scheduling,
147    /// One or more atomic evaluation units ran.
148    Evaluating,
149    /// The job finished and carries an EvalResult.
150    Finished,
151    /// Cancellation ended the explicit job.
152    Cancelled,
153}
154#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155#[non_exhaustive]
156/// Formula counts for the current schedule pass and logical recalculation.
157pub struct StepProgress {
158    /// One-based schedule pass; changes invalidate the previous denominator.
159    pub pass: u32,
160    /// Full-schedule formula vertices visited, including clean priority work.
161    pub completed_in_pass: usize,
162    /// Formula vertices scheduled in this pass.
163    pub scheduled_in_pass: usize,
164    /// Formulas computed across all passes, including recomputed formulas.
165    pub cumulative_computed_vertices: usize,
166    /// Conservative proof of a fixed total. False also means unknown:
167    /// proofs are limited to 1,024 scalar arithmetic templates in v1.
168    pub total_exact: bool,
169    /// Formulas still awaiting graph construction.
170    pub staged_formulas_remaining: usize,
171    /// Priority work is queued, unfinished, or deferred by the expansion cap.
172    pub priority_pending: bool,
173}
174#[derive(Clone, Debug, Default, PartialEq, Eq)]
175#[non_exhaustive]
176/// Conservative changed regions for one step.
177pub struct StepDelta {
178    /// Changed regions; scalar bulk commits conservatively include the whole
179    /// installed run, so a region can also contain cells with unchanged values.
180    pub delta: super::TargetEvalDelta,
181    /// Repaint visible cells on these sheets. When populated, delta is empty.
182    pub overflow_sheets: Vec<formualizer_common::SheetId>,
183}
184#[derive(Clone, Copy, Debug, PartialEq, Eq)]
185#[non_exhaustive]
186/// Kind of indivisible work measured by a step.
187pub enum SlowUnitKind {
188    /// Build the graph from staged formulas.
189    GraphBuild,
190    /// Construct or restrict a schedule.
191    ScheduleBuild,
192    /// Evaluate one cell.
193    SingleCell,
194    /// Evaluate one whole physical family run.
195    FamilyRun,
196    /// Evaluate one strongly connected component.
197    Cycle,
198}
199#[derive(Clone, Debug, PartialEq, Eq)]
200#[non_exhaustive]
201/// Longest indivisible unit observed in this step.
202pub struct SlowUnitReport {
203    /// Category of work.
204    pub kind: SlowUnitKind,
205    /// One zero-based cell in the unit, when available.
206    pub representative_cell: Option<formualizer_common::PackedSheetCell>,
207    /// Number of formula vertices in the unit.
208    pub vertex_count: usize,
209    /// Time spent in this unit.
210    pub elapsed: Duration,
211}
212#[derive(Debug)]
213#[non_exhaustive]
214/// Per-step locations and telemetry. Reads between steps observe installed
215/// new values alongside old values for still-pending cells. Only Finished
216/// carries a result; Cancelled is terminal for explicit jobs.
217pub struct StepReport {
218    /// Phase reached at return.
219    pub phase: StepPhase,
220    /// Formula progress, with a pass-specific denominator.
221    pub progress: StepProgress,
222    /// Formulas computed during this step.
223    pub step_computed_vertices: usize,
224    /// Active elapsed time in this step.
225    pub step_elapsed: Duration,
226    /// Regions changed by installed work in this step.
227    pub changed: StepDelta,
228    /// Longest atomic unit, if any ran.
229    pub longest_unit: Option<SlowUnitReport>,
230    /// Present only when phase is Finished.
231    pub result: Option<EvalResult>,
232}
233
234/// Held pass, clock, hints, collector and pending iterative re-dirties.
235#[derive(Default)]
236pub(super) struct LogicalRecalculation {
237    pub held: Option<super::eval::HeldRecalcSchedule>,
238    pub pass: u32,
239    pub pass_completed: usize,
240    pub pass_scheduled: usize,
241    pub pass_exact: bool,
242    pub priority_targets: Vec<super::EvaluationTarget>,
243    pub priority_deferred: bool,
244    pub work_units: u64,
245    pub active_elapsed: Duration,
246    pub seed_retained_bytes: u64,
247    pub topology_epoch: u64,
248    pub authority_revision: (u64, u64),
249    pub computed_vertices: usize,
250    pub cycle_errors: usize,
251    pub elapsed: Duration,
252    pub replans: usize,
253    // Keep independent admission revisions: ordinary edit APIs can consume
254    // the engine's planning semantic-epoch guard between resumable calls.
255    pub function_epoch: u64,
256    pub provider_revision: Option<u64>,
257    pub mutation_revision: u64,
258    pub source_mutation_generation: u64,
259    // FR5 must also propagate from readers installed in an earlier slice.
260    pub installed: super::idset::DenseIdSet,
261    // Retrying an SCC after an edit/cancellation must not count another
262    // recalculation's iteration against its already advanced visible values.
263    pub cycle_seeds: rustc_hash::FxHashMap<
264        Vec<super::VertexId>,
265        rustc_hash::FxHashMap<super::VertexId, formualizer_common::LiteralValue>,
266    >,
267    pub original_cycles: rustc_hash::FxHashSet<Vec<super::VertexId>>,
268    pub original_cycles_recorded: bool,
269    // Fast rejection for ordinary layer units; no scan of SCC seed maps on
270    // the usual acyclic path. Conservatively retains bits after dissolution.
271    pub new_cycle_members: super::idset::DenseIdSet,
272}
273
274impl LogicalRecalculation {
275    pub fn refresh_seed_retained_bytes(&mut self) {
276        use formualizer_common::LiteralValue;
277        use std::mem::size_of;
278        fn payload(value: &LiteralValue) -> usize {
279            match value {
280                LiteralValue::Text(s) => s.capacity(),
281                LiteralValue::Array(rows) => {
282                    rows.capacity() * size_of::<Vec<LiteralValue>>()
283                        + rows
284                            .iter()
285                            .map(|r| {
286                                r.capacity() * size_of::<LiteralValue>()
287                                    + r.iter().map(payload).sum::<usize>()
288                            })
289                            .sum::<usize>()
290                }
291                _ => 0,
292            }
293        }
294        self.seed_retained_bytes = (self.cycle_seeds.capacity()
295            * size_of::<(
296                Vec<super::VertexId>,
297                rustc_hash::FxHashMap<super::VertexId, LiteralValue>,
298            )>()
299            + self
300                .cycle_seeds
301                .iter()
302                .map(|(key, values)| {
303                    key.capacity() * size_of::<super::VertexId>()
304                        + values.capacity() * size_of::<(super::VertexId, LiteralValue)>()
305                        + values.values().map(payload).sum::<usize>()
306                })
307                .sum::<usize>()
308            + self.original_cycles.capacity() * size_of::<Vec<super::VertexId>>()
309            + self
310                .original_cycles
311                .iter()
312                .map(|k| k.capacity() * size_of::<super::VertexId>())
313                .sum::<usize>()) as u64;
314    }
315    pub fn forget_dissolved_new_cycles(&mut self, vertices: &[super::VertexId]) {
316        if self.new_cycle_members.is_empty()
317            || !vertices.iter().any(|v| self.new_cycle_members.contains(v))
318        {
319            return;
320        }
321        // A new cycle that dissolved and ran acyclically must start from its
322        // then-current values if it forms again. Original SCC seeds survive.
323        let before = self.cycle_seeds.len();
324        self.cycle_seeds.retain(|key, _| {
325            self.original_cycles.contains(key)
326                || !vertices.iter().any(|v| key.binary_search(v).is_ok())
327        });
328        if before != self.cycle_seeds.len() {
329            self.refresh_seed_retained_bytes();
330        }
331    }
332}