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