Skip to main content

turbo_tasks_backend/backend/
mod.rs

1mod cell_data;
2mod counter_map;
3mod eviction;
4mod gc;
5mod operation;
6mod snapshot_coordinator;
7mod storage;
8pub mod storage_schema;
9
10// Only the `verify_aggregation_graph` feature still uses atomics here; `stopping` is an
11// `RwLock<bool>` so that checking it and acting on it cannot be split (see the field's docs).
12#[cfg(feature = "verify_aggregation_graph")]
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::{
15    borrow::Cow,
16    fmt::Write,
17    future::Future,
18    hash::BuildHasherDefault,
19    mem::take,
20    pin::Pin,
21    sync::{Arc, LazyLock},
22    time::SystemTime,
23};
24
25use anyhow::{Result, bail};
26use auto_hash_map::{AutoMap, AutoSet};
27use gc::DEFAULT_GC_ROOT_TTL;
28pub use gc::{GcPassResult, GcStats, TtlCounter};
29use hashbrown::hash_table::Entry;
30use parking_lot::{Mutex, RwLock};
31use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
32use smallvec::SmallVec;
33use tokio::time::{Duration, Instant};
34use tracing::{Span, field::display, trace_span};
35use turbo_bincode::TurboBincodeBuffer;
36use turbo_tasks::{
37    CellId, DynTaskInputsStorage, RawVc, RawVcUnpacked, ReadCellOptions, ReadCellTracking,
38    ReadConsistency, ReadOutcome, ReadOutputOptions, ReadTracking, SharedReference,
39    TRANSIENT_TASK_BIT, TaskExecutionReason, TaskId, TaskPersistence, TaskPriority, TraitTypeId,
40    TurboTasks, TurboTasksCallApi, TurboTasksPanic, ValueTypeId,
41    backend::{
42        Backend, CachedTaskType, CachedTaskTypeArc, CellContent, CellHash, TaskCollectiblesMap,
43        TaskExecutionSpec, TransientTaskType, TurboTaskContextError, TurboTaskLocalContextError,
44        TurboTasksError, TurboTasksExecutionError, TurboTasksExecutionErrorMessage,
45        TypedCellContent, VerificationMode,
46    },
47    event::{Event, EventDescription, EventListener},
48    macro_helpers::NativeFunction,
49    message_queue::{DiagnosticEvent, Severity, TimingEvent, TraceEvent},
50    parallel,
51    registry::get_value_type,
52    scope_bounded::scope_bounded,
53    task_statistics::TaskStatisticsApi,
54    util::{IdFactoryWithReuse, good_chunk_size, into_chunks},
55};
56#[cfg(feature = "task_dirty_cause")]
57use turbo_tasks::{FunctionId, TaskDirtyCause};
58use turbo_tasks_malloc::TurboMalloc;
59
60use self::eviction::EvictionControl;
61#[cfg(test)]
62pub(crate) use self::storage::TrackOutcome;
63pub use self::{
64    eviction::EvictionMode,
65    storage::{EvictionCounts, SpecificTaskDataCategory, TaskDataCategory},
66};
67use crate::{
68    backend::{
69        operation::{
70            AggregationUpdateJob, AggregationUpdateQueue, ExecuteContext, LeafDistanceUpdateQueue,
71            OutdatedEdge, TaskGuard, TaskType, TaskTypeRef, capture_all_edges, cleanup_old_edges,
72            connect_child, connect_children, get_aggregation_number, get_uppers, invalidate,
73            make_task_dirty_internal, prepare_new_children, update_cell,
74        },
75        snapshot_coordinator::{OperationGuard, SlowSettle, SnapshotCoordinator},
76        storage::{Storage, StorageOptions, encode_snapshot_item},
77        storage_schema::{TaskStorage, TaskStorageAccessors},
78    },
79    backing_storage::SnapshotItem,
80    data::{
81        ActivenessState, CellRef, CollectibleRef, CollectiblesRef, Dirtyness, InProgressCellState,
82        InProgressState, InProgressStateInner, OutputValue, TransientTask,
83    },
84    error::{TaskError, TaskErrorItem},
85    kv_backing_storage::TurboBackingStorage,
86    utils::{
87        dash_map_entry::{get_in_shard, get_shard, with_entry_in_shard},
88        shard_amount::compute_shard_amount,
89        stopwatch::Stopwatch,
90    },
91};
92
93/// Threshold for parallelizing making dependent tasks dirty.
94/// If the number of dependent tasks exceeds this threshold,
95/// the operation will be parallelized.
96const DEPENDENT_TASKS_DIRTY_PARALLELIZATION_THRESHOLD: usize = 10000;
97
98/// The minimum useful quantum of GC work: how much a pass always does before it will honour an
99/// interrupt.  GC holds an operation lock and so we can block concurrent operations.  Interruption
100/// ensures we are responsive and this ensures a minimum amount of progress.
101const GC_MIN_PROGRESS: Duration = Duration::from_millis(100);
102
103/// Priority used to re-schedule a task that became stale during execution.
104///
105/// Stale tasks must run again, but at a priority that reflects why they're being re-run rather
106/// than the (likely higher) priority of the original schedule. We use invalidation priority
107/// based on the task's leaf distance, parented under either the task's current dirty priority
108/// or `leaf()` if it is no longer dirty.
109fn compute_stale_priority(task: &TaskGuard<'_>) -> TaskPriority {
110    TaskPriority::invalidation(
111        task.get_leaf_distance()
112            .copied()
113            .unwrap_or_default()
114            .distance,
115    )
116    .in_parent(task.is_dirty().unwrap_or(TaskPriority::leaf()))
117}
118
119#[derive(PartialEq, Eq)]
120pub enum StorageMode {
121    /// Queries the storage for cache entries that don't exist locally.
122    ReadOnly,
123    /// Queries the storage for cache entries that don't exist locally.
124    /// Regularly pushes changes to the backing storage.
125    ReadWrite,
126    /// Queries the storage for cache entries that don't exist locally.
127    /// On shutdown, pushes all changes to the backing storage.
128    ReadWriteOnShutdown,
129}
130
131pub struct BackendOptions {
132    /// Enables dependency tracking.
133    ///
134    /// When disabled: No state changes are allowed. Tasks will never reexecute and stay cached
135    /// forever.
136    pub dependency_tracking: bool,
137
138    /// Enables active tracking.
139    ///
140    /// Automatically disabled when `dependency_tracking` is disabled.
141    ///
142    /// When disabled: All tasks are considered as active.
143    pub active_tracking: bool,
144
145    /// Enables the backing storage.
146    pub storage_mode: Option<StorageMode>,
147
148    /// Number of tokio worker threads. It will be used to compute the shard amount of parallel
149    /// datastructures. If `None`, it will use the available parallelism.
150    pub num_workers: Option<usize>,
151
152    /// Avoid big preallocations for faster startup. Should only be used for testing purposes.
153    pub small_preallocation: bool,
154
155    /// Strategy for evicting evictable tasks from in-memory storage after a snapshot.
156    /// This reclaims memory by clearing persisted data that can be re-loaded from disk on demand.
157    pub eviction_mode: EvictionMode,
158
159    /// Whether the reference-counting GC runs for this backend. `None` (default) leaves it off.
160    ///
161    /// In Next.js this is driven by the `experimental.turbopackGc` config option.
162    pub gc: Option<bool>,
163
164    /// How long a GC root may go un-anchored before it ages out. `None` (default) uses
165    /// `DEFAULT_GC_ROOT_TTL`.
166    pub gc_root_ttl: Option<Duration>,
167
168    /// How long a GC pass runs before it will honour an interrupt. `None` (default) uses
169    /// `GC_MIN_PROGRESS`.
170    pub gc_min_progress: Option<Duration>,
171}
172
173impl Default for BackendOptions {
174    fn default() -> Self {
175        Self {
176            dependency_tracking: true,
177            active_tracking: true,
178            storage_mode: Some(StorageMode::ReadWrite),
179            num_workers: None,
180            small_preallocation: false,
181            eviction_mode: EvictionMode::Off,
182            gc: None,
183            gc_root_ttl: None,
184            gc_min_progress: None,
185        }
186    }
187}
188
189pub enum TurboTasksBackendJob {
190    Snapshot,
191}
192
193/// Why a snapshot/persist is being performed.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195enum SnapshotReason {
196    Test,
197    Stop,
198    InitialSnapshotTimeout,
199    RegularSnapshotInterval,
200    IdleTimeout,
201}
202
203impl SnapshotReason {
204    fn as_str(self) -> &'static str {
205        match self {
206            SnapshotReason::Test => "test",
207            SnapshotReason::Stop => "stop",
208            SnapshotReason::InitialSnapshotTimeout => "initial snapshot timeout",
209            SnapshotReason::RegularSnapshotInterval => "regular snapshot interval",
210            SnapshotReason::IdleTimeout => "idle timeout",
211        }
212    }
213
214    /// Whether this snapshot waits for in-flight operations to settle. Otherwise it is skipped
215    /// if any operation is active when it starts.
216    fn waits_for_operations(self) -> bool {
217        // Idle snapshots are opportunistic: an active operation means we are no longer idle, so
218        // skip and let the background loop try again rather than pausing the work that woke us.
219        !matches!(self, SnapshotReason::IdleTimeout)
220    }
221
222    /// Whether a GC pass run for this reason may wind down early when an operation is waiting.
223    fn gc_is_interruptible(self) -> bool {
224        matches!(self, SnapshotReason::IdleTimeout | SnapshotReason::Test)
225    }
226
227    /// True only for `Stop`: at shutdown the whole map is dropped right after, so each task
228    /// entry can be drained from the map and freed as it is serialized instead of after the
229    /// whole batch is written. This reduces peak memory during `next build` shutdown.
230    fn drain_entries(self) -> bool {
231        matches!(self, SnapshotReason::Stop)
232    }
233}
234
235/// The outcome of the last persistence cycle. The backend starts at `Ok` in its constructor.
236///
237/// `Failed` is sticky: a failed persist (an error or a panic) leaves the task graph and the
238/// database out of sync (e.g. `save_snapshot` already consumed `persisted_task_cache_log`
239/// entries), so persisting stays disabled for the rest of the session.
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241enum LastPersistenceStatus {
242    Ok,
243    Failed,
244}
245
246pub struct TurboTasksBackend {
247    options: BackendOptions,
248
249    start_time: Instant,
250
251    persisted_task_id_factory: IdFactoryWithReuse<TaskId>,
252    transient_task_id_factory: IdFactoryWithReuse<TaskId>,
253
254    storage: Storage,
255
256    /// Coordinates the operation/snapshot/GC interleaving protocol. See
257    /// [`SnapshotCoordinator`] for details.
258    snapshot_coord: SnapshotCoordinator,
259    /// Serializes calls to `snapshot_and_persist` (and `gc_for_testing`). The coordinator's
260    /// `begin_exclusion` asserts that exclusive phases don't overlap; this mutex
261    /// enforces that contract for our callers (background loop and
262    /// `stop_and_wait`).
263    ///
264    /// Also holds the [`LastPersistenceStatus`], which disables persisting after a failure.
265    snapshot_in_progress: Mutex<LastPersistenceStatus>,
266
267    /// Experimental feature to enable dead tasks to be deleted from storage and ram.
268    gc_enabled: bool,
269
270    stopping: RwLock<bool>,
271    stopping_event: Event,
272    idle_start_event: Event,
273    idle_end_event: Event,
274    #[cfg(feature = "verify_aggregation_graph")]
275    is_idle: AtomicBool,
276
277    task_statistics: TaskStatisticsApi,
278
279    backing_storage: TurboBackingStorage,
280    /// How long a GC root may go un-anchored before it ages out.
281    gc_root_ttl: Duration,
282
283    /// How long a GC pass runs before it will honour an interrupt
284    gc_min_progress: Duration,
285
286    #[cfg(feature = "verify_aggregation_graph")]
287    root_tasks: Mutex<FxHashSet<TaskId>>,
288}
289
290/// What [`TurboTasksBackend::snapshot_and_evict_for_testing`] observed.
291#[doc(hidden)]
292#[derive(Default)]
293pub struct TestSnapshotOutcome {
294    /// Whether the snapshot found modifications to persist.
295    pub had_new_data: bool,
296    /// Tasks evicted from memory at each level.
297    pub eviction_counts: EvictionCounts,
298    /// The [`GcStats`] and [`GcPassResult`] of the GC pass this snapshot ran, or `None` when GC
299    /// is disabled for the backend.
300    pub gc: Option<(GcStats, GcPassResult)>,
301}
302
303impl TestSnapshotOutcome {
304    /// The [`GcStats`] for the GC pass
305    pub fn gc_stats(&self) -> &GcStats {
306        &self
307            .gc
308            .as_ref()
309            .expect("no GC pass ran: the backend needs `BackendOptions::gc = Some(true)`")
310            .0
311    }
312
313    /// Whether the GC pass wound down early. `false` when GC is disabled.
314    pub fn gc_interrupted(&self) -> bool {
315        self.gc
316            .as_ref()
317            .is_some_and(|(_, result)| result.interrupted)
318    }
319}
320
321impl TurboTasksBackend {
322    /// Invalidates the persistent storage so that it will be deleted the next time a turbopack
323    /// instance is created with the filesystem cache enabled.
324    ///
325    /// `reason_code` should be one of the codes in
326    /// [`crate::db_invalidation::invalidation_reasons`].
327    pub fn invalidate_storage(&self, reason_code: &str) -> Result<()> {
328        self.backing_storage.invalidate(reason_code)
329    }
330
331    pub fn new(mut options: BackendOptions, backing_storage: TurboBackingStorage) -> Self {
332        let shard_amount = compute_shard_amount(options.num_workers, options.small_preallocation);
333        if !options.dependency_tracking {
334            options.active_tracking = false;
335        }
336        let small_preallocation = options.small_preallocation;
337        let gc_root_ttl = options.gc_root_ttl.unwrap_or(DEFAULT_GC_ROOT_TTL);
338        let gc_min_progress = options.gc_min_progress.unwrap_or(GC_MIN_PROGRESS);
339        let next_task_id = backing_storage
340            .next_free_task_id()
341            .expect("Failed to get task id");
342
343        let mut gc_enabled = options.gc.unwrap_or(false);
344        if gc_enabled
345            && options.storage_mode == Some(StorageMode::ReadWrite)
346            && options.eviction_mode == EvictionMode::Off
347        {
348            eprintln!(
349                "warning: GC is enabled but eviction is disabled; GC would leave collected tasks \
350                 resident forever. Forcing GC off. Enable eviction ('auto'/'full') to use GC in \
351                 this mode."
352            );
353            gc_enabled = false;
354        }
355
356        Self {
357            options,
358            gc_enabled,
359            start_time: Instant::now(),
360            persisted_task_id_factory: IdFactoryWithReuse::new(
361                next_task_id,
362                TaskId::try_from(TRANSIENT_TASK_BIT - 1).unwrap(),
363            ),
364            transient_task_id_factory: IdFactoryWithReuse::new(
365                TaskId::try_from(TRANSIENT_TASK_BIT).unwrap(),
366                TaskId::MAX,
367            ),
368            storage: Storage::new(StorageOptions {
369                shard_amount,
370                small_preallocation,
371            }),
372            snapshot_coord: SnapshotCoordinator::new(),
373            snapshot_in_progress: Mutex::new(LastPersistenceStatus::Ok),
374            stopping: RwLock::new(false),
375            stopping_event: Event::new(|| || "TurboTasksBackend::stopping_event".to_string()),
376            idle_start_event: Event::new(|| || "TurboTasksBackend::idle_start_event".to_string()),
377            idle_end_event: Event::new(|| || "TurboTasksBackend::idle_end_event".to_string()),
378            #[cfg(feature = "verify_aggregation_graph")]
379            is_idle: AtomicBool::new(false),
380            task_statistics: TaskStatisticsApi::default(),
381            backing_storage,
382            gc_root_ttl,
383            gc_min_progress,
384            #[cfg(feature = "verify_aggregation_graph")]
385            root_tasks: Default::default(),
386        }
387    }
388
389    fn execute_context<'a>(
390        &'a self,
391        turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
392    ) -> ExecuteContext<'a> {
393        ExecuteContext::new(self, turbo_tasks)
394    }
395
396    /// Like [`TurboTasksBackend::execute_context`], but with an explicit current task priority
397    /// instead of the one from the task-local task state. For work that runs on another thread
398    /// than the calling task.
399    fn execute_context_with_priority<'a>(
400        &'a self,
401        turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
402        current_task_priority: TaskPriority,
403    ) -> ExecuteContext<'a> {
404        ExecuteContext::new(self, turbo_tasks).with_current_task_priority(current_task_priority)
405    }
406
407    /// Like [`TurboTasksBackend::execute_context`], but refuses to hand out a context once
408    /// shutdown has begun, and blocks shutdown for as long as the returned context is alive.
409    ///
410    /// Use this for entry points reachable from threads that `stop_and_wait` does **not** drain.
411    ///
412    /// Returns `None` once [`TurboTasksBackend::stopping`] has run, in which case the caller must
413    /// do nothing: storage teardown is imminent or already underway.
414    fn try_execute_context<'a>(
415        &'a self,
416        turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
417    ) -> Option<ExecuteContext<'a>> {
418        let stopping = self.stopping.read();
419        if *stopping {
420            return None;
421        }
422        Some(ExecuteContext::new_with_shutdown_guard(
423            self,
424            turbo_tasks,
425            stopping,
426        ))
427    }
428
429    pub(crate) fn start_operation(&self) -> Option<OperationGuard<'_>> {
430        if !self.should_persist() {
431            return None;
432        }
433        Some(self.snapshot_coord.begin_operation())
434    }
435
436    fn should_persist(&self) -> bool {
437        matches!(
438            self.options.storage_mode,
439            Some(StorageMode::ReadWrite) | Some(StorageMode::ReadWriteOnShutdown)
440        )
441    }
442
443    /// Perform a snapshot and then evict all evictable tasks from memory.
444    ///
445    /// This is exposed for integration tests that need to verify the snapshot → evict → restore
446    /// cycle works correctly.
447    #[doc(hidden)]
448    pub fn snapshot_and_evict_for_testing(
449        &self,
450        turbo_tasks: &TurboTasks<TurboTasksBackend>,
451    ) -> TestSnapshotOutcome {
452        assert!(
453            self.should_persist(),
454            "snapshot_and_evict requires persistence"
455        );
456        let snapshot_result = self.snapshot_and_persist(None, SnapshotReason::Test, turbo_tasks);
457        let (had_new_data, gc_outcome) = match snapshot_result {
458            Ok(Some((_, new_data, gc_outcome))) => (new_data, gc_outcome),
459            // Test snapshots wait for operations to settle, so `None` means persisting is
460            // disabled after an earlier failure.
461            Ok(None) | Err(_) => {
462                // Snapshot/persist failed — skip eviction since the data may not
463                // be on disk yet. Evicting now could lose in-memory state that
464                // can't be restored.
465                return TestSnapshotOutcome::default();
466            }
467        };
468        let eviction_counts = self.storage.evict_after_snapshot(None);
469        TestSnapshotOutcome {
470            had_new_data,
471            eviction_counts,
472            gc: gc_outcome,
473        }
474    }
475
476    /// The number oftasks resident in the map.
477    #[doc(hidden)]
478    pub fn resident_task_count_for_testing(&self) -> usize {
479        self.storage.resident_task_count_for_testing()
480    }
481
482    /// The parent priority a dirty task was made dirty with, or `None` if it doesn't exist or is
483    /// not dirty. Test-only hook for verifying invalidation priorities.
484    #[doc(hidden)]
485    pub fn dirty_parent_priority_for_testing(
486        &self,
487        task: TaskId,
488        turbo_tasks: &TurboTasks<TurboTasksBackend>,
489    ) -> Option<TaskPriority> {
490        let mut ctx = self.execute_context(turbo_tasks);
491        let task = ctx.try_task(task, TaskDataCategory::All)?;
492        match task.get_dirty() {
493            Some(Dirtyness::Dirty {
494                parent_priority, ..
495            }) => Some(*parent_priority),
496            _ => None,
497        }
498    }
499
500    /// The persistent `parent_count` of a resident task (0 if absent or not resident). Test-only
501    /// hook for verifying incremental refcount maintenance.
502    #[doc(hidden)]
503    pub fn parent_count_for_testing(&self, task: TaskId) -> u32 {
504        self.storage
505            .with_task(task, |t| t.gc_parent_count())
506            .unwrap_or(0)
507    }
508
509    /// The transient `transient_ref_count` of a resident task (0 if absent or not resident).
510    #[doc(hidden)]
511    pub fn transient_ref_count_for_testing(&self, task: TaskId) -> u32 {
512        self.storage
513            .with_task(task, |t| t.gc_transient_ref_count())
514            .unwrap_or(0)
515    }
516
517    /// The GC roots set as currently persisted on disk (task id -> [`TtlCounter`]).
518    #[doc(hidden)]
519    pub fn persisted_gc_roots_for_testing(&self) -> Vec<(TaskId, TtlCounter)> {
520        self.backing_storage.roots().unwrap_or_default()
521    }
522    /// Opens `task` with the must-exist [`ExecuteContext::task`] and drops the guard. Test-only
523    /// hook to exercise the non-fabricating existence guarantee: this panics if `task` exists in
524    /// neither memory nor persistent storage (rather than fabricating a blank).
525    #[doc(hidden)]
526    pub fn assert_task_exists_for_testing(
527        &self,
528        task: TaskId,
529        turbo_tasks: &TurboTasks<TurboTasksBackend>,
530    ) {
531        let mut ctx = self.execute_context(turbo_tasks);
532        let _ = ctx.task(task, TaskDataCategory::All);
533    }
534
535    fn should_restore(&self) -> bool {
536        self.options.storage_mode.is_some()
537    }
538
539    fn should_track_dependencies(&self) -> bool {
540        self.options.dependency_tracking
541    }
542
543    fn should_track_activeness(&self) -> bool {
544        self.options.active_tracking
545    }
546
547    fn track_cache_hit_by_fn(&self, native_fn: &'static NativeFunction) {
548        self.task_statistics
549            .map(|stats| stats.increment_cache_hit(native_fn));
550    }
551
552    fn track_cache_miss_by_fn(&self, native_fn: &'static NativeFunction) {
553        self.task_statistics
554            .map(|stats| stats.increment_cache_miss(native_fn));
555    }
556
557    /// Reconstructs a full [`TurboTasksExecutionError`] from the compact [`TaskError`] storage
558    /// representation. For [`TaskError::TaskChain`], this looks up the source error from the last
559    /// task's output and rebuilds the nested `TaskContext` wrappers with `TurboTasksCallApi`
560    /// references for lazy name resolution.
561    fn task_error_to_turbo_tasks_execution_error(
562        &self,
563        error: &TaskError,
564        ctx: &mut ExecuteContext<'_>,
565    ) -> TurboTasksExecutionError {
566        match error {
567            TaskError::Panic(panic) => TurboTasksExecutionError::Panic(panic.clone()),
568            TaskError::Error(item) => TurboTasksExecutionError::Error(Arc::new(TurboTasksError {
569                message: item.message.clone(),
570                source: item
571                    .source
572                    .as_ref()
573                    .map(|e| self.task_error_to_turbo_tasks_execution_error(e, ctx)),
574            })),
575            TaskError::LocalTaskContext(local_task_context) => {
576                TurboTasksExecutionError::LocalTaskContext(Arc::new(TurboTaskLocalContextError {
577                    name: local_task_context.name.clone(),
578                    source: local_task_context
579                        .source
580                        .as_ref()
581                        .map(|e| self.task_error_to_turbo_tasks_execution_error(e, ctx)),
582                }))
583            }
584            TaskError::TaskChain(chain) => {
585                let task_id = chain.last().unwrap();
586                let error = {
587                    let task = ctx.task(*task_id, TaskDataCategory::Meta);
588                    if let Some(OutputValue::Error(error)) = task.get_output() {
589                        Some(error.clone())
590                    } else {
591                        None
592                    }
593                };
594                let error = error.map_or_else(
595                    || {
596                        // Eventual consistency will cause errors to no longer be available
597                        TurboTasksExecutionError::Panic(Arc::new(TurboTasksPanic {
598                            message: TurboTasksExecutionErrorMessage::PIISafe(Cow::Borrowed(
599                                "Error no longer available",
600                            )),
601                            location: None,
602                        }))
603                    },
604                    |e| self.task_error_to_turbo_tasks_execution_error(&e, ctx),
605                );
606                let mut current_error = error;
607                for &task_id in chain.iter().rev() {
608                    current_error =
609                        TurboTasksExecutionError::TaskContext(Arc::new(TurboTaskContextError {
610                            task_id,
611                            source: Some(current_error),
612                            turbo_tasks: ctx.turbo_tasks(),
613                        }));
614                }
615                current_error
616            }
617        }
618    }
619}
620
621/// Intermediate result of step 1 of task execution completion.
622struct TaskExecutionCompletePrepareResult {
623    pub new_children: FxHashSet<TaskId>,
624    pub is_now_immutable: bool,
625    #[cfg(feature = "verify_determinism")]
626    pub no_output_set: bool,
627    #[cfg(feature = "task_dirty_cause")]
628    pub function_id: Option<FunctionId>,
629    pub new_output: Option<OutputValue>,
630    pub output_dependent_tasks: SmallVec<[TaskId; 4]>,
631    pub is_recomputation: bool,
632    pub is_session_dependent: bool,
633}
634
635/// Locks the task being read, and the reader too when a dependency edge may need to be added.
636///
637/// Returns `None` if the task being read is gone (collected by GC, or missing from storage).
638fn lock_task_and_optional_reader<'e>(
639    ctx: &mut ExecuteContext<'e>,
640    task_id: TaskId,
641    reader_id: Option<TaskId>,
642) -> Option<(TaskGuard<'e>, Option<TaskGuard<'e>>)> {
643    let task = ctx.try_task(task_id, TaskDataCategory::All)?;
644    let Some(reader_id) = reader_id else {
645        return Some((task, None));
646    };
647
648    // Immutable tasks never need dependency edges and can never be invalidated. Avoid locking the
649    // reader too in that common case. When the task is still mutable, drop the speculative lock
650    // and reacquire both locks together to preserve the invalidation race guarantee.
651    if task.immutable() && !cfg!(feature = "verify_immutable") {
652        Some((task, None))
653    } else {
654        drop(task);
655
656        // Having a task_pair here is not optimal, but locking the reader separately could lose an
657        // invalidation that races with dependency edge insertion.
658        // TODO(sokra): solve this more performantly for mutable tasks.
659        let (task, reader) = ctx.task_pair(task_id, reader_id, TaskDataCategory::All);
660        // The task can become immutable between the speculative task lock and the pair lock.
661        // Recheck under the final lock so callers can rely on a reader guard only being returned
662        // when dependency edges may still be added.
663        if task.immutable() && !cfg!(feature = "verify_immutable") {
664            drop(reader);
665            Some((task, None))
666        } else {
667            Some((task, Some(reader)))
668        }
669    }
670}
671
672/// The error a read of a missing task (see [`lock_task_and_optional_reader`]) fails with.
673fn collected_task_read_error(task_id: TaskId, operation: &str) -> anyhow::Error {
674    anyhow::anyhow!(
675        "{operation}: task {task_id} no longer exists (it was garbage collected). The reading \
676         task holds a stale reference to it and is expected to be dropped by its parent."
677    )
678}
679
680// Operations
681impl TurboTasksBackend {
682    fn try_read_task_output(
683        &self,
684        task_id: TaskId,
685        reader: Option<TaskId>,
686        options: ReadOutputOptions,
687        turbo_tasks: &TurboTasks<TurboTasksBackend>,
688    ) -> Result<ReadOutcome<RawVc>> {
689        self.assert_not_persistent_calling_transient(reader, task_id);
690
691        let mut ctx = self.execute_context(turbo_tasks);
692        let need_reader_task = reader.and_then(|reader_id| {
693            (self.should_track_dependencies()
694                && !matches!(options.tracking, ReadTracking::Untracked)
695                && reader_id != task_id)
696                .then_some(reader_id)
697        });
698        let Some((mut task, mut reader_task)) =
699            lock_task_and_optional_reader(&mut ctx, task_id, need_reader_task)
700        else {
701            return Err(collected_task_read_error(task_id, "read_task_output"));
702        };
703
704        fn listen_to_done_event(
705            reader_description: Option<EventDescription>,
706            tracking: ReadTracking,
707            done_event: &Event,
708        ) -> EventListener {
709            done_event.listen_with_note(move || {
710                move || {
711                    if let Some(reader_description) = reader_description.as_ref() {
712                        format!(
713                            "try_read_task_output from {} ({})",
714                            reader_description, tracking
715                        )
716                    } else {
717                        format!("try_read_task_output ({})", tracking)
718                    }
719                }
720            })
721        }
722
723        /// Reports whether the task is already being computed, and — crucially for the reader —
724        /// whether a worker has actually started it. A task that is only `Scheduled` can be taken
725        /// over and executed by the reader; one that is `InProgress` can only be waited for.
726        fn check_in_progress<T>(
727            task: &TaskGuard<'_>,
728            reader_description: Option<EventDescription>,
729            tracking: ReadTracking,
730        ) -> Option<Result<ReadOutcome<T>>> {
731            match task.get_in_progress() {
732                Some(InProgressState::Scheduled { done_event, .. }) => {
733                    Some(Ok(ReadOutcome::Scheduled(listen_to_done_event(
734                        reader_description,
735                        tracking,
736                        done_event,
737                    ))))
738                }
739                Some(InProgressState::InProgress(InProgressStateInner { done_event, .. })) => {
740                    Some(Ok(ReadOutcome::InProgress(listen_to_done_event(
741                        reader_description,
742                        tracking,
743                        done_event,
744                    ))))
745                }
746                Some(InProgressState::Canceled) => Some(Err(anyhow::anyhow!(
747                    "{} was canceled",
748                    task.get_task_description()
749                ))),
750                None => None,
751            }
752        }
753
754        if matches!(options.consistency, ReadConsistency::Strong) {
755            if task
756                .get_persistent_task_type()
757                .is_some_and(|t| !t.native_fn.is_root)
758            {
759                drop(task);
760                drop(reader_task);
761                panic!(
762                    "Strongly consistent read of non-root task {} (reader: {}). The `root` \
763                     attribute is missing on the task.",
764                    self.debug_get_task_description(task_id),
765                    reader.map_or_else(
766                        || "unknown".to_string(),
767                        |r| self.debug_get_task_description(r)
768                    )
769                );
770            }
771
772            let is_dirty = task.is_dirty();
773
774            // Check the dirty count of the root node
775            let has_dirty_containers = task.has_dirty_containers();
776            if has_dirty_containers || is_dirty.is_some() {
777                let activeness = task.get_activeness_mut();
778                let mut task_ids_to_schedule: Vec<_> = Vec::new();
779                // When there are dirty task, subscribe to the all_clean_event
780                let activeness = if let Some(activeness) = activeness {
781                    // This makes sure all tasks stay active and this task won't stale.
782                    // active_until_clean is automatically removed when this
783                    // task is clean.
784                    activeness.set_active_until_clean();
785                    activeness
786                } else {
787                    // If we don't have a root state, add one. This also makes sure all tasks stay
788                    // active and this task won't stale. active_until_clean
789                    // is automatically removed when this task is clean.
790                    if ctx.should_track_activeness() {
791                        // A newly added Activeness need to make sure to schedule the tasks
792                        task_ids_to_schedule = task.dirty_containers().collect();
793                        task_ids_to_schedule.push(task_id);
794                    }
795                    let activeness =
796                        task.get_activeness_mut_or_insert_with(|| ActivenessState::new(task_id));
797                    activeness.set_active_until_clean();
798                    activeness
799                };
800                let listener = activeness.all_clean_event.listen_with_note(move || {
801                    // Reach the backend through the pinned `turbo_tasks` handle rather than
802                    // cloning `self`: pinning keeps the backend alive for the closure's lifetime.
803                    let tt = turbo_tasks.pin();
804                    move || {
805                        let mut ctx = tt.backend().execute_context(&tt);
806                        let mut visited = FxHashSet::default();
807                        fn indent(s: &str) -> String {
808                            s.split_inclusive('\n')
809                                .flat_map(|line: &str| ["  ", line].into_iter())
810                                .collect::<String>()
811                        }
812                        fn get_info(
813                            ctx: &mut ExecuteContext<'_>,
814                            task_id: TaskId,
815                            parent_and_count: Option<(TaskId, i32)>,
816                            visited: &mut FxHashSet<TaskId>,
817                        ) -> String {
818                            let task = ctx.task(task_id, TaskDataCategory::All);
819                            let is_dirty = task.is_dirty();
820                            let in_progress =
821                                task.get_in_progress()
822                                    .map_or("not in progress", |p| match p {
823                                        InProgressState::InProgress(_) => "in progress",
824                                        InProgressState::Scheduled { .. } => "scheduled",
825                                        InProgressState::Canceled => "canceled",
826                                    });
827                            let activeness = task.get_activeness().map_or_else(
828                                || "not active".to_string(),
829                                |activeness| format!("{activeness:?}"),
830                            );
831                            let aggregation_number = get_aggregation_number(&task);
832                            let missing_upper = if let Some((parent_task_id, _)) = parent_and_count
833                            {
834                                let uppers = get_uppers(&task);
835                                !uppers.contains(&parent_task_id)
836                            } else {
837                                false
838                            };
839
840                            // Check the dirty count of the root node
841                            let has_dirty_containers = task.has_dirty_containers();
842
843                            let task_description = task.get_task_description();
844                            let is_dirty_label = if let Some(parent_priority) = is_dirty {
845                                format!(", dirty({parent_priority})")
846                            } else {
847                                String::new()
848                            };
849                            let has_dirty_containers_label = if has_dirty_containers {
850                                ", dirty containers"
851                            } else {
852                                ""
853                            };
854                            let count = if let Some((_, count)) = parent_and_count {
855                                format!(" {count}")
856                            } else {
857                                String::new()
858                            };
859                            let mut info = format!(
860                                "{task_id} {task_description}{count} (aggr={aggregation_number}, \
861                                 {in_progress}, \
862                                 {activeness}{is_dirty_label}{has_dirty_containers_label})",
863                            );
864                            let children: Vec<_> = task.dirty_containers_with_count().collect();
865                            drop(task);
866
867                            if missing_upper {
868                                info.push_str("\n  ERROR: missing upper connection");
869                            }
870
871                            if has_dirty_containers || !children.is_empty() {
872                                writeln!(info, "\n  dirty tasks:").unwrap();
873
874                                for (child_task_id, count) in children {
875                                    let task_description = ctx
876                                        .task(child_task_id, TaskDataCategory::Data)
877                                        .get_task_description();
878                                    if visited.insert(child_task_id) {
879                                        let child_info = get_info(
880                                            ctx,
881                                            child_task_id,
882                                            Some((task_id, count)),
883                                            visited,
884                                        );
885                                        info.push_str(&indent(&child_info));
886                                        if !info.ends_with('\n') {
887                                            info.push('\n');
888                                        }
889                                    } else {
890                                        writeln!(
891                                            info,
892                                            "  {child_task_id} {task_description} {count} \
893                                             (already visited)"
894                                        )
895                                        .unwrap();
896                                    }
897                                }
898                            }
899                            info
900                        }
901                        let info = get_info(&mut ctx, task_id, None, &mut visited);
902                        format!(
903                            "try_read_task_output (strongly consistent) from {reader:?}\n{info}"
904                        )
905                    }
906                });
907                drop(reader_task);
908                drop(task);
909                if !task_ids_to_schedule.is_empty() {
910                    let mut queue = AggregationUpdateQueue::new();
911                    queue.extend_find_and_schedule_dirty(task_ids_to_schedule);
912                    queue.execute(&mut ctx);
913                }
914
915                return Ok(ReadOutcome::InProgress(listener));
916            }
917        }
918
919        let reader_description = reader_task
920            .as_ref()
921            .map(|r| EventDescription::new(|| r.get_task_desc_fn()))
922            .or_else(|| {
923                need_reader_task.map(|reader_id| {
924                    EventDescription::new(move || move || format!("{reader_id:?}"))
925                })
926            });
927        if let Some(value) = check_in_progress(&task, reader_description.clone(), options.tracking)
928        {
929            return value;
930        }
931
932        if let Some(output) = task.get_output() {
933            let result = match output {
934                OutputValue::Cell(cell) => Ok(RawVc::task_cell(cell.task, cell.cell)),
935                OutputValue::Output(task) => Ok(RawVc::task_output(*task)),
936                OutputValue::Error(error) => Err(error.clone()),
937            };
938            if let Some(mut reader_task) = reader_task.take()
939                && options.tracking.should_track(result.is_err())
940            {
941                #[cfg(feature = "trace_task_output_dependencies")]
942                let _span = tracing::trace_span!(
943                    "add output dependency",
944                    task = %task_id,
945                    dependent_task = ?reader
946                )
947                .entered();
948                let mut queue = LeafDistanceUpdateQueue::new();
949                let reader = reader.unwrap();
950                if task.add_output_dependent(reader) {
951                    // Ensure that dependent leaf distance is strictly monotonic increasing
952                    let leaf_distance = task.get_leaf_distance().copied().unwrap_or_default();
953                    let reader_leaf_distance =
954                        reader_task.get_leaf_distance().copied().unwrap_or_default();
955                    if reader_leaf_distance.distance <= leaf_distance.distance {
956                        queue.push(
957                            reader,
958                            leaf_distance.distance,
959                            leaf_distance.max_distance_in_buffer,
960                        );
961                    }
962                }
963
964                drop(task);
965
966                // Note: We use `task_pair` earlier to lock the task and its reader at the same
967                // time. If we didn't and just locked the reader here, an invalidation could occur
968                // between grabbing the locks. If that happened, and if the task is "outdated" or
969                // doesn't have the dependency edge yet, the invalidation would be lost.
970
971                if !reader_task.remove_outdated_output_dependencies(&task_id) {
972                    let _ = reader_task.add_output_dependencies(task_id);
973                }
974                drop(reader_task);
975
976                queue.execute(&mut ctx);
977            } else {
978                drop(task);
979            }
980
981            return result.map(ReadOutcome::Value).map_err(|error| {
982                self.task_error_to_turbo_tasks_execution_error(&error, &mut ctx)
983                    .with_task_context(task_id, turbo_tasks.pin())
984                    .into()
985            });
986        }
987        drop(reader_task);
988
989        let note = EventDescription::new(|| {
990            move || {
991                if let Some(reader) = reader_description.as_ref() {
992                    format!("try_read_task_output (recompute) from {reader}",)
993                } else {
994                    "try_read_task_output (recompute, untracked)".to_string()
995                }
996            }
997        });
998
999        // Output doesn't exist. We need to schedule the task to compute it.
1000        let (in_progress_state, listener) = InProgressState::new_scheduled_with_listener(
1001            TaskExecutionReason::OutputNotAvailable,
1002            EventDescription::new(|| task.get_task_desc_fn()),
1003            note,
1004        );
1005
1006        // It's not possible that the task is InProgress at this point. If it is InProgress {
1007        // done: true } it must have Output and would early return.
1008        let old = task.set_in_progress(in_progress_state);
1009        debug_assert!(old.is_none(), "InProgress already exists");
1010        ctx.schedule_task(&task, TaskPriority::Recomputation);
1011
1012        Ok(ReadOutcome::Scheduled(listener))
1013    }
1014
1015    fn try_read_task_cell(
1016        &self,
1017        task_id: TaskId,
1018        reader: Option<TaskId>,
1019        cell: CellId,
1020        options: ReadCellOptions,
1021        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1022    ) -> Result<ReadOutcome<TypedCellContent>> {
1023        self.assert_not_persistent_calling_transient(reader, task_id);
1024
1025        fn add_cell_dependency(
1026            task_id: TaskId,
1027            mut task: TaskGuard<'_>,
1028            reader: Option<TaskId>,
1029            reader_task: Option<TaskGuard<'_>>,
1030            cell: CellId,
1031            key: Option<u64>,
1032        ) {
1033            if let Some(mut reader_task) = reader_task {
1034                let reader = reader.unwrap();
1035                let reverse = CellRef { task: reader, cell };
1036                if let Some(k) = key {
1037                    let _ = task.add_cell_dependents_hashed((reverse, k));
1038                } else {
1039                    let _ = task.add_cell_dependents(reverse);
1040                }
1041                drop(task);
1042
1043                // Note: We use `task_pair` earlier to lock the task and its reader at the same
1044                // time. If we didn't and just locked the reader here, an invalidation could occur
1045                // between grabbing the locks. If that happened, and if the task is "outdated" or
1046                // doesn't have the dependency edge yet, the invalidation would be lost.
1047
1048                let target = CellRef {
1049                    task: task_id,
1050                    cell,
1051                };
1052                if let Some(k) = key {
1053                    if !reader_task.remove_outdated_cell_dependencies_hashed(&(target, k)) {
1054                        let _ = reader_task.add_cell_dependencies_hashed((target, k));
1055                    }
1056                } else if !reader_task.remove_outdated_cell_dependencies(&target) {
1057                    let _ = reader_task.add_cell_dependencies(target);
1058                }
1059                drop(reader_task);
1060            }
1061        }
1062
1063        let ReadCellOptions {
1064            tracking,
1065            final_read_hint,
1066        } = options;
1067
1068        let mut ctx = self.execute_context(turbo_tasks);
1069        let need_reader_task = reader.and_then(|reader_id| {
1070            (self.should_track_dependencies()
1071                && !matches!(tracking, ReadCellTracking::Untracked)
1072                && reader_id != task_id)
1073                .then_some(reader_id)
1074        });
1075        let Some((mut task, reader_task)) =
1076            lock_task_and_optional_reader(&mut ctx, task_id, need_reader_task)
1077        else {
1078            return Err(collected_task_read_error(task_id, "read_task_cell"));
1079        };
1080
1081        let content = if final_read_hint {
1082            task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence)
1083        } else {
1084            task.get_cell_data(&cell).cloned()
1085        };
1086        if let Some(content) = content {
1087            if tracking.should_track(false) {
1088                add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1089            }
1090            return Ok(ReadOutcome::Value(TypedCellContent(
1091                cell.type_id(),
1092                CellContent(Some(content)),
1093            )));
1094        }
1095
1096        let in_progress = task.get_in_progress();
1097        if matches!(
1098            in_progress,
1099            Some(InProgressState::InProgress(..) | InProgressState::Scheduled { .. })
1100        ) {
1101            // Tell the reader whether the task is merely queued (it may take it over and execute it
1102            // itself) or already being executed by a worker (it can only wait).
1103            let started = matches!(in_progress, Some(InProgressState::InProgress(..)));
1104            let listener = self
1105                .listen_to_cell(&mut task, task_id, need_reader_task, &reader_task, cell)
1106                .0;
1107            return Ok(if started {
1108                ReadOutcome::InProgress(listener)
1109            } else {
1110                ReadOutcome::Scheduled(listener)
1111            });
1112        }
1113        let is_cancelled = matches!(in_progress, Some(InProgressState::Canceled));
1114
1115        // Check cell index range (cell might not exist at all)
1116        let max_id = task.get_cell_type_max_index(&cell.type_id()).copied();
1117        let Some(max_id) = max_id else {
1118            let task_desc = task.get_task_description();
1119            if tracking.should_track(true) {
1120                add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1121            }
1122            bail!(
1123                "Cell {cell:?} no longer exists in task {task_desc} (no cell of this type exists)",
1124            );
1125        };
1126        if cell.index() >= max_id {
1127            let task_desc = task.get_task_description();
1128            if tracking.should_track(true) {
1129                add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1130            }
1131            bail!("Cell {cell:?} no longer exists in task {task_desc} (index out of bounds)");
1132        }
1133
1134        // Cell should exist, but data was dropped or is not serializable. We need to recompute the
1135        // task to get the cell content.
1136
1137        // Bail early if the task was cancelled — no point in registering a listener
1138        // on a task that won't execute again.
1139        if is_cancelled {
1140            bail!("{} was canceled", task.get_task_description());
1141        }
1142
1143        // Listen to the cell and potentially schedule the task
1144        let (listener, new_listener) =
1145            self.listen_to_cell(&mut task, task_id, need_reader_task, &reader_task, cell);
1146        drop(reader_task);
1147        if !new_listener {
1148            // Somebody else is already waiting for this cell, so the task is being taken care of.
1149            return Ok(ReadOutcome::InProgress(listener));
1150        }
1151
1152        let _span = tracing::trace_span!(
1153            "recomputation",
1154            cell_type = get_value_type(cell.type_id()).ty.global_name,
1155            cell_index = cell.index()
1156        )
1157        .entered();
1158
1159        let _ = task.add_scheduled(
1160            TaskExecutionReason::CellNotAvailable,
1161            EventDescription::new(|| task.get_task_desc_fn()),
1162        );
1163        ctx.schedule_task(&task, TaskPriority::Recomputation);
1164
1165        Ok(ReadOutcome::Scheduled(listener))
1166    }
1167
1168    fn listen_to_cell(
1169        &self,
1170        task: &mut TaskGuard<'_>,
1171        task_id: TaskId,
1172        reader: Option<TaskId>,
1173        reader_task: &Option<TaskGuard<'_>>,
1174        cell: CellId,
1175    ) -> (EventListener, bool) {
1176        let note = || {
1177            let reader_desc = reader_task.as_ref().map(|r| r.get_task_desc_fn());
1178            move || {
1179                if let Some(reader_desc) = reader_desc.as_ref() {
1180                    format!("try_read_task_cell (in progress) from {}", (reader_desc)())
1181                } else if let Some(reader_id) = reader {
1182                    format!("try_read_task_cell (in progress) from {reader_id:?}")
1183                } else {
1184                    "try_read_task_cell (in progress, untracked)".to_string()
1185                }
1186            }
1187        };
1188        if let Some(in_progress) = task.get_in_progress_cells(&cell) {
1189            // Someone else is already computing the cell
1190            let listener = in_progress.event.listen_with_note(note);
1191            return (listener, false);
1192        }
1193        let in_progress = InProgressCellState::new(task_id, cell);
1194        let listener = in_progress.event.listen_with_note(note);
1195        let old = task.insert_in_progress_cells(cell, in_progress);
1196        debug_assert!(old.is_none(), "InProgressCell already exists");
1197        (listener, true)
1198    }
1199
1200    /// Runs a persistence cycle
1201    ///
1202    /// Returns `None` if the cycle was skipped: persisting is disabled after an earlier failure, or
1203    /// operations were active and the reason doesn't
1204    /// [wait for them](SnapshotReason::waits_for_operations). Otherwise returns
1205    /// `(snapshot_start, had_new_data, gc_outcome)`. `gc_outcome` is `None` when GC is
1206    /// disabled; it is returned rather than stashed on `self` so a test can inspect the pass it
1207    /// just triggered without the backend carrying test-only state. Production reads the same
1208    /// numbers off the `gc` span.
1209    ///
1210    /// A failure (an error or a panic) disables persisting for the rest of the session: later
1211    /// calls return `None` without starting a snapshot or GC.
1212    #[allow(clippy::type_complexity, reason = "only used for tests")]
1213    fn snapshot_and_persist(
1214        &self,
1215        parent_span: Option<tracing::Id>,
1216        reason: SnapshotReason,
1217        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1218    ) -> Result<Option<(Instant, bool, Option<(GcStats, GcPassResult)>)>> {
1219        // Serialize snapshots and GC for the entire persistence cycle.
1220        let mut status = self.snapshot_in_progress.lock();
1221        if *status == LastPersistenceStatus::Failed {
1222            return Ok(None);
1223        }
1224        // Mark the cycle failed up front and only reset it on success: encoding failures panic,
1225        // and a panic unwinds past any post-call check (`parking_lot` mutexes don't poison).
1226        *status = LastPersistenceStatus::Failed;
1227        let result = self.snapshot_and_persist_locked(parent_span, reason, turbo_tasks);
1228        if result.is_ok() {
1229            *status = LastPersistenceStatus::Ok;
1230        }
1231        result
1232    }
1233
1234    /// [`Self::snapshot_and_persist`], called while holding `snapshot_in_progress`.
1235    #[allow(clippy::type_complexity, reason = "only used for tests")]
1236    fn snapshot_and_persist_locked(
1237        &self,
1238        parent_span: Option<tracing::Id>,
1239        reason: SnapshotReason,
1240        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1241    ) -> Result<Option<(Instant, bool, Option<(GcStats, GcPassResult)>)>> {
1242        let snapshot_span =
1243            tracing::trace_span!(parent: parent_span.clone(), "snapshot", reason = reason.as_str())
1244                .entered();
1245
1246        // One exclusion covers the GC pass and the snapshot that follows it, so the collected
1247        // tasks' tombstones (derived from the `deleted` flag) ride this same commit and no
1248        // operation can resurrect a collected task in between.
1249        let start = Instant::now();
1250        // SystemTime for wall-clock timestamps in trace events (milliseconds
1251        // since epoch). Instant is monotonic but has no defined epoch, so it
1252        // can't be used for cross-process trace correlation.
1253        let wall_start = SystemTime::now();
1254        let snapshot_phase = if reason.waits_for_operations() {
1255            self.snapshot_coord
1256                .begin_snapshot(|slow| Self::report_slow_settle_for_snapshot(turbo_tasks, slow))
1257        } else {
1258            match self.snapshot_coord.try_begin_snapshot() {
1259                Some(phase) => phase,
1260                None => return Ok(None),
1261            }
1262        };
1263        let (gc_elapsed, gc_roots_to_persist, gc_outcome) = if self.gc_enabled {
1264            let gc_span = tracing::info_span!(
1265                "gc",
1266                stats = tracing::field::Empty,
1267                interrupted = tracing::field::Empty
1268            )
1269            .entered();
1270            let (stats, result, roots) =
1271                self.gc_collect(turbo_tasks, &snapshot_phase, reason.gc_is_interruptible());
1272            gc_span.record("stats", display(&stats));
1273            gc_span.record("interrupted", result.interrupted);
1274            if result.interrupted {
1275                // If we were interrupted also abandon the persistence loop.
1276                // This ensures that we don't persist roots that were not completely validated.
1277                drop(snapshot_phase);
1278                drop(gc_span);
1279                return Ok(Some((start, false, Some((stats, result)))));
1280            }
1281            (Some(start.elapsed()), roots, Some((stats, result)))
1282        } else {
1283            (None, None, None)
1284        };
1285
1286        debug_assert!(self.should_persist());
1287
1288        let snapshot_guard = self.storage.start_snapshot();
1289
1290        let snapshot_time = Instant::now();
1291
1292        #[cfg(feature = "print_cache_item_size")]
1293        #[derive(Default)]
1294        struct TaskCacheStats {
1295            data: usize,
1296            #[cfg(feature = "print_cache_item_size_with_compressed")]
1297            data_compressed: usize,
1298            data_count: usize,
1299            meta: usize,
1300            #[cfg(feature = "print_cache_item_size_with_compressed")]
1301            meta_compressed: usize,
1302            meta_count: usize,
1303            upper_count: usize,
1304            collectibles_count: usize,
1305            aggregated_collectibles_count: usize,
1306            children_count: usize,
1307            followers_count: usize,
1308            collectibles_dependents_count: usize,
1309            aggregated_dirty_containers_count: usize,
1310            output_size: usize,
1311        }
1312        /// Formats a byte size, optionally including the compressed size when the
1313        /// `print_cache_item_size_with_compressed` feature is enabled.
1314        #[cfg(feature = "print_cache_item_size")]
1315        struct FormatSizes {
1316            size: usize,
1317            #[cfg(feature = "print_cache_item_size_with_compressed")]
1318            compressed_size: usize,
1319        }
1320        #[cfg(feature = "print_cache_item_size")]
1321        impl std::fmt::Display for FormatSizes {
1322            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1323                use turbo_tasks::util::FormatBytes;
1324                #[cfg(feature = "print_cache_item_size_with_compressed")]
1325                {
1326                    write!(
1327                        f,
1328                        "{} ({} compressed)",
1329                        FormatBytes(self.size),
1330                        FormatBytes(self.compressed_size)
1331                    )
1332                }
1333                #[cfg(not(feature = "print_cache_item_size_with_compressed"))]
1334                {
1335                    write!(f, "{}", FormatBytes(self.size))
1336                }
1337            }
1338        }
1339        #[cfg(feature = "print_cache_item_size")]
1340        impl TaskCacheStats {
1341            #[cfg(feature = "print_cache_item_size_with_compressed")]
1342            fn compressed_size(data: &[u8]) -> usize {
1343                lz4_flex::block::compress(data).len()
1344            }
1345
1346            fn add_data(&mut self, data: &[u8]) {
1347                self.data += data.len();
1348                #[cfg(feature = "print_cache_item_size_with_compressed")]
1349                {
1350                    self.data_compressed += Self::compressed_size(data);
1351                }
1352                self.data_count += 1;
1353            }
1354
1355            fn add_meta(&mut self, data: &[u8]) {
1356                self.meta += data.len();
1357                #[cfg(feature = "print_cache_item_size_with_compressed")]
1358                {
1359                    self.meta_compressed += Self::compressed_size(data);
1360                }
1361                self.meta_count += 1;
1362            }
1363
1364            fn add_counts(&mut self, stats: &crate::backing_storage::SnapshotItemStats) {
1365                let counts = &stats.counts;
1366                self.upper_count += counts.upper;
1367                self.collectibles_count += counts.collectibles;
1368                self.aggregated_collectibles_count += counts.aggregated_collectibles;
1369                self.children_count += counts.children;
1370                self.followers_count += counts.followers;
1371                self.collectibles_dependents_count += counts.collectibles_dependents;
1372                self.aggregated_dirty_containers_count += counts.aggregated_dirty_containers;
1373                self.output_size += stats.output_size;
1374            }
1375
1376            /// Returns the primary sort key: compressed total when
1377            /// `print_cache_item_size_with_compressed` is enabled, raw total otherwise.
1378            fn sort_key(&self) -> usize {
1379                #[cfg(feature = "print_cache_item_size_with_compressed")]
1380                {
1381                    self.data_compressed + self.meta_compressed
1382                }
1383                #[cfg(not(feature = "print_cache_item_size_with_compressed"))]
1384                {
1385                    self.data + self.meta
1386                }
1387            }
1388
1389            fn format_total(&self) -> FormatSizes {
1390                FormatSizes {
1391                    size: self.data + self.meta,
1392                    #[cfg(feature = "print_cache_item_size_with_compressed")]
1393                    compressed_size: self.data_compressed + self.meta_compressed,
1394                }
1395            }
1396
1397            fn format_data(&self) -> FormatSizes {
1398                FormatSizes {
1399                    size: self.data,
1400                    #[cfg(feature = "print_cache_item_size_with_compressed")]
1401                    compressed_size: self.data_compressed,
1402                }
1403            }
1404
1405            fn format_avg_data(&self) -> FormatSizes {
1406                FormatSizes {
1407                    size: self.data.checked_div(self.data_count).unwrap_or(0),
1408                    #[cfg(feature = "print_cache_item_size_with_compressed")]
1409                    compressed_size: self
1410                        .data_compressed
1411                        .checked_div(self.data_count)
1412                        .unwrap_or(0),
1413                }
1414            }
1415
1416            fn format_meta(&self) -> FormatSizes {
1417                FormatSizes {
1418                    size: self.meta,
1419                    #[cfg(feature = "print_cache_item_size_with_compressed")]
1420                    compressed_size: self.meta_compressed,
1421                }
1422            }
1423
1424            fn format_avg_meta(&self) -> FormatSizes {
1425                FormatSizes {
1426                    size: self.meta.checked_div(self.meta_count).unwrap_or(0),
1427                    #[cfg(feature = "print_cache_item_size_with_compressed")]
1428                    compressed_size: self
1429                        .meta_compressed
1430                        .checked_div(self.meta_count)
1431                        .unwrap_or(0),
1432                }
1433            }
1434        }
1435        #[cfg(feature = "print_cache_item_size")]
1436        let task_cache_stats: Mutex<FxHashMap<_, TaskCacheStats>> =
1437            Mutex::new(FxHashMap::default());
1438
1439        // Encode each task's captured categories (`category`). Only categories with `modified` set
1440        // are captured, meaning the category was actually dirtied. Categories restored from disk
1441        // but never modified don't need re-persisting since the on-disk version is still valid.
1442        // Captured tasks that were modified again before being persisted were already encoded by
1443        // `track_modification` (see `Storage::snapshots`), and are yielded without calling this.
1444        let process = |task_id: TaskId,
1445                       inner: &TaskStorage,
1446                       category: TaskDataCategory,
1447                       buffer: &mut TurboBincodeBuffer| {
1448            debug_assert!(
1449                !self.gc_enabled || inner.flags.deleted() || !inner.gc_collectible(),
1450                "tasks scheduled for persistent must not be collectible, this implies a missed \
1451                 task during GC"
1452            );
1453            encode_snapshot_item(task_id, inner, category, buffer).unwrap_or_else(|err| {
1454                panic!(
1455                    "Serializing task {} failed: {:?}",
1456                    self.debug_get_task_description(task_id),
1457                    err
1458                )
1459            })
1460        };
1461
1462        // Called for every item persisted, including the ones encoded by `track_modification`.
1463        // The stats were captured when the item was encoded.
1464        let inspect_snapshot_item = |item: &SnapshotItem| {
1465            #[cfg(feature = "print_cache_item_size")]
1466            if let SnapshotItem::Put {
1467                meta, data, stats, ..
1468            } = item
1469                && (meta.is_some() || data.is_some())
1470            {
1471                let mut task_cache_stats = task_cache_stats.lock();
1472                let entry = task_cache_stats.entry(stats.task_name.clone()).or_default();
1473                entry.add_counts(stats);
1474                if let Some(meta) = meta {
1475                    entry.add_meta(meta);
1476                }
1477                if let Some(data) = data {
1478                    entry.add_data(data);
1479                }
1480            }
1481            #[cfg(not(feature = "print_cache_item_size"))]
1482            let _ = item;
1483        };
1484
1485        // The scan captures the modified tasks, so it needs the exclusion (`snapshot_phase`).
1486        // Encoding happens later, in the returned iterators, after the exclusion ends.
1487        let task_snapshots = self.storage.take_snapshot(
1488            &snapshot_phase,
1489            snapshot_guard,
1490            &process,
1491            &inspect_snapshot_item,
1492            reason.drain_entries(),
1493        );
1494        drop(snapshot_phase);
1495
1496        drop(snapshot_span);
1497        let snapshot_duration = start.elapsed();
1498        let task_count = task_snapshots.len();
1499
1500        if task_snapshots.is_empty() && gc_roots_to_persist.is_none() {
1501            // No task was modified since the last snapshot (or every modified task was both new
1502            // and deleted), so there is nothing to write.
1503            return Ok(Some((snapshot_time, false, gc_outcome)));
1504        }
1505
1506        let persist_start = Instant::now();
1507        let span = tracing::info_span!(
1508            parent: parent_span,
1509            "persist",
1510            reason = reason.as_str(),
1511            snapshot_meta = tracing::field::Empty,
1512        )
1513        .entered();
1514        // Tasks were already consumed by take_snapshot, so a future snapshot
1515        // would not re-persist them — returning an error signals to the caller
1516        // that further persist attempts would corrupt the task graph in storage.
1517        let snapshot_meta = self
1518            .backing_storage
1519            .save_snapshot(gc_roots_to_persist, task_snapshots)?;
1520        span.record("snapshot_meta", display(snapshot_meta));
1521
1522        #[cfg(feature = "print_cache_item_size")]
1523        {
1524            let mut task_cache_stats = task_cache_stats
1525                .into_inner()
1526                .into_iter()
1527                .collect::<Vec<_>>();
1528            if !task_cache_stats.is_empty() {
1529                use turbo_tasks::util::FormatBytes;
1530
1531                use crate::utils::markdown_table::print_markdown_table;
1532
1533                task_cache_stats.sort_unstable_by(|(key_a, stats_a), (key_b, stats_b)| {
1534                    (stats_b.sort_key(), key_b).cmp(&(stats_a.sort_key(), key_a))
1535                });
1536
1537                println!(
1538                    "Task cache stats: {}",
1539                    FormatSizes {
1540                        size: task_cache_stats
1541                            .iter()
1542                            .map(|(_, s)| s.data + s.meta)
1543                            .sum::<usize>(),
1544                        #[cfg(feature = "print_cache_item_size_with_compressed")]
1545                        compressed_size: task_cache_stats
1546                            .iter()
1547                            .map(|(_, s)| s.data_compressed + s.meta_compressed)
1548                            .sum::<usize>()
1549                    },
1550                );
1551
1552                print_markdown_table(
1553                    [
1554                        "Task",
1555                        " Total Size",
1556                        " Data Size",
1557                        " Data Count x Avg",
1558                        " Data Count x Avg",
1559                        " Meta Size",
1560                        " Meta Count x Avg",
1561                        " Meta Count x Avg",
1562                        " Uppers",
1563                        " Coll",
1564                        " Agg Coll",
1565                        " Children",
1566                        " Followers",
1567                        " Coll Deps",
1568                        " Agg Dirty",
1569                        " Output Size",
1570                    ],
1571                    task_cache_stats.iter(),
1572                    |(task_desc, stats)| {
1573                        [
1574                            task_desc.to_string(),
1575                            format!(" {}", stats.format_total()),
1576                            format!(" {}", stats.format_data()),
1577                            format!(" {} x", stats.data_count),
1578                            format!("{}", stats.format_avg_data()),
1579                            format!(" {}", stats.format_meta()),
1580                            format!(" {} x", stats.meta_count),
1581                            format!("{}", stats.format_avg_meta()),
1582                            format!(" {}", stats.upper_count),
1583                            format!(" {}", stats.collectibles_count),
1584                            format!(" {}", stats.aggregated_collectibles_count),
1585                            format!(" {}", stats.children_count),
1586                            format!(" {}", stats.followers_count),
1587                            format!(" {}", stats.collectibles_dependents_count),
1588                            format!(" {}", stats.aggregated_dirty_containers_count),
1589                            format!(" {}", FormatBytes(stats.output_size)),
1590                        ]
1591                    },
1592                );
1593            }
1594        }
1595
1596        let elapsed = start.elapsed();
1597        let persist_duration = persist_start.elapsed();
1598        // avoid spamming the event queue with information about fast operations
1599        if elapsed > Duration::from_secs(10) {
1600            turbo_tasks.send_compilation_event(Arc::new(TimingEvent::new(
1601                "Finished writing to filesystem cache".to_string(),
1602                elapsed,
1603            )));
1604        }
1605
1606        let (persist_wall_start, persist_wall_duration) = if let Some(gc_elapsed) = gc_elapsed {
1607            turbo_tasks.send_compilation_event(Arc::new(TraceEvent::new_with_duration(
1608                "turbopack-gc",
1609                wall_start,
1610                gc_elapsed,
1611                serde_json::json!([]),
1612            )));
1613            (wall_start + gc_elapsed, elapsed.saturating_sub(gc_elapsed))
1614        } else {
1615            (wall_start, elapsed)
1616        };
1617        turbo_tasks.send_compilation_event(Arc::new(TraceEvent::new_with_duration(
1618            "turbopack-persistence",
1619            persist_wall_start,
1620            persist_wall_duration,
1621            serde_json::json!([
1622                ["reason", reason.as_str()],
1623                [
1624                    "snapshot_duration_ms",
1625                    snapshot_duration.as_secs_f64() * 1000.0,
1626                ],
1627                [
1628                    "persist_duration_ms",
1629                    persist_duration.as_secs_f64() * 1000.0,
1630                ],
1631                ["task_count", task_count],
1632                ["bytes_written", snapshot_meta.bytes_written,],
1633                ["bytes_deleted", snapshot_meta.bytes_deleted,]
1634            ]),
1635        )));
1636
1637        Ok(Some((snapshot_time, true, gc_outcome)))
1638    }
1639
1640    fn startup(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1641        // Only when it should write regularly to the storage, we schedule the initial snapshot
1642        // job.
1643        if matches!(self.options.storage_mode, Some(StorageMode::ReadWrite)) {
1644            // Schedule the snapshot job
1645            let _span = trace_span!("persisting background job").entered();
1646            let _span = tracing::info_span!("thread").entered();
1647            turbo_tasks.schedule_backend_background_job(TurboTasksBackendJob::Snapshot);
1648        }
1649    }
1650
1651    fn stopping(&self) {
1652        // modify via a write guard so we synchronize with top level calls into try_execute_context
1653        *self.stopping.write() = true;
1654        self.stopping_event.notify(usize::MAX);
1655    }
1656
1657    #[allow(unused_variables)]
1658    fn stop(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1659        #[cfg(feature = "verify_aggregation_graph")]
1660        {
1661            self.is_idle.store(false, Ordering::Release);
1662            self.verify_aggregation_graph(turbo_tasks, false);
1663        }
1664        // eagerly drop the task cache before persisting
1665        self.storage.drop_task_cache();
1666        if self.should_persist()
1667            && let Err(err) =
1668                self.snapshot_and_persist(Span::current().into(), SnapshotReason::Stop, turbo_tasks)
1669        {
1670            // We don't treat this as a failure, just warn that the cache is broken and keep going.
1671            eprintln!("WARNING: Saving the filesystem cache failed:\n    {err:#}");
1672        }
1673        self.storage.drop_contents();
1674        // Do final cleanup, this is best effort and unfailable
1675        self.backing_storage.shutdown();
1676    }
1677
1678    #[allow(unused_variables)]
1679    fn idle_start(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1680        self.idle_start_event.notify(usize::MAX);
1681
1682        #[cfg(feature = "verify_aggregation_graph")]
1683        {
1684            use tokio::select;
1685
1686            self.is_idle.store(true, Ordering::Release);
1687            // The spawned task reaches the backend through the pinned `turbo_tasks` handle
1688            // rather than cloning `self`: pinning keeps the `TurboTasks` (and therefore the
1689            // backend it owns) alive for the task's lifetime.
1690            let turbo_tasks = turbo_tasks.pin();
1691            tokio::task::spawn(async move {
1692                let backend = &turbo_tasks.backend();
1693                select! {
1694                    _ = tokio::time::sleep(Duration::from_secs(5)) => {
1695                        // do nothing
1696                    }
1697                    _ = backend.idle_end_event.listen() => {
1698                        return;
1699                    }
1700                }
1701                if !backend.is_idle.load(Ordering::Relaxed) {
1702                    return;
1703                }
1704                backend.verify_aggregation_graph(&turbo_tasks, true);
1705            });
1706        }
1707    }
1708
1709    fn idle_end(&self) {
1710        #[cfg(feature = "verify_aggregation_graph")]
1711        self.is_idle.store(false, Ordering::Release);
1712        self.idle_end_event.notify(usize::MAX);
1713    }
1714
1715    fn get_or_create_task(
1716        &self,
1717        native_fn: &'static NativeFunction,
1718        this: Option<RawVc>,
1719        arg: &mut dyn DynTaskInputsStorage,
1720        parent_task: Option<TaskId>,
1721        persistence: TaskPersistence,
1722        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1723    ) -> TaskId {
1724        let transient = matches!(persistence, TaskPersistence::Transient);
1725
1726        if transient
1727            && let Some(parent_task) = parent_task
1728            && !parent_task.is_transient()
1729        {
1730            let task_type = CachedTaskType {
1731                native_fn,
1732                this,
1733                arg: arg.take_box(),
1734            };
1735            self.panic_persistent_calling_transient(
1736                self.debug_get_task_description(parent_task),
1737                Some(&task_type),
1738            );
1739        }
1740
1741        let is_root = native_fn.is_root;
1742
1743        // Compute hash and shard index once from borrowed components (no heap allocation).
1744        let arg_ref = arg.as_ref();
1745        let hash = CachedTaskType::hash_from_components(
1746            self.storage.task_cache.hasher(),
1747            native_fn,
1748            this,
1749            arg_ref,
1750        );
1751        // Locate the shard once so that the read-only lookup and any
1752        // write-lock retry below share the same reference (saves a modulo +
1753        // memory lookup on the miss path).
1754        let shard = get_shard(&self.storage.task_cache, hash);
1755
1756        let mut ctx = self.execute_context(turbo_tasks);
1757        let mut created_new = false;
1758        // Step 1: Fast read-only cache lookup (read lock, no allocation).
1759        // Use a read lock rather than a write lock to avoid contention. connect_child
1760        // may re-enter task_cache with a write lock, so we must not hold a write lock here.
1761        if let Some(task_id) =
1762            get_in_shard(shard, hash, |k| k.eq_components(native_fn, this, arg_ref))
1763        {
1764            self.track_cache_hit_by_fn(native_fn);
1765            connect_child(
1766                parent_task,
1767                task_id,
1768                /* release_construction_ref */ false,
1769                ctx,
1770            );
1771            return task_id;
1772        }
1773
1774        // Step 2: Check backing storage using borrowed components (no box needed yet).
1775
1776        // Task exists in backing storage.
1777        // We only need to insert it into the in-memory cache.
1778        let task_id = if !transient
1779            && let Some((task_id, stored_type)) = ctx.task_by_type(native_fn, this, arg_ref)
1780        {
1781            self.track_cache_hit_by_fn(native_fn);
1782            // Step 3a: Insert into in-memory cache using the pre-located shard.
1783            // Use the existing Arc from storage to avoid a duplicate allocation.
1784            with_entry_in_shard(
1785                shard,
1786                self.storage.task_cache.hasher(),
1787                hash,
1788                arg,
1789                |k, arg| k.eq_components(native_fn, this, arg.as_ref()),
1790                |entry, _arg| {
1791                    if let Entry::Vacant(entry) = entry {
1792                        entry.insert((stored_type, task_id));
1793                    }
1794                },
1795            );
1796            task_id
1797        } else {
1798            let (task_id, created) = with_entry_in_shard(
1799                shard,
1800                self.storage.task_cache.hasher(),
1801                hash,
1802                arg,
1803                |k, arg| k.eq_components(native_fn, this, arg.as_ref()),
1804                |entry, arg| match entry {
1805                    Entry::Occupied(entry) => {
1806                        // Another thread beat us to creating this task — use their task_id.
1807                        // They will handle logging the new task as modified.
1808                        (entry.get().1, false)
1809                    }
1810                    Entry::Vacant(entry) => {
1811                        // Only now do we force the allocation.
1812                        // NOTE: if our caller had to perform resolution, then this will have
1813                        // already been boxed and take_box just takes it.
1814                        let task_type = CachedTaskTypeArc::new(CachedTaskType {
1815                            native_fn,
1816                            this,
1817                            arg: arg.take_box(),
1818                        });
1819                        let task_id = if transient {
1820                            self.transient_task_id_factory.get()
1821                        } else {
1822                            self.persisted_task_id_factory.get()
1823                        };
1824                        // Initialize storage BEFORE making task_id visible in the cache.
1825                        // This ensures any thread that reads task_id from the cache sees
1826                        // the storage entry already initialized (restored flags set).
1827                        self.storage
1828                            .initialize_new_task(task_id, Some(task_type.clone()));
1829                        entry.insert((task_type, task_id));
1830                        (task_id, true)
1831                    }
1832                },
1833            );
1834
1835            // The entry closure has returned, so the task_cache shard lock is released before
1836            // cache tracking or aggregation updates can re-enter the backend.
1837            created_new = created;
1838            if created {
1839                self.track_cache_miss_by_fn(native_fn);
1840                // Update the aggregation number before connecting the child. We don't need this on
1841                // recovery paths because the aggregation number will already be set.
1842                if is_root {
1843                    AggregationUpdateQueue::run(
1844                        AggregationUpdateJob::UpdateAggregationNumber {
1845                            task_id,
1846                            base_aggregation_number: u32::MAX,
1847                            distance: None,
1848                        },
1849                        &mut ctx,
1850                    );
1851                } else if native_fn.is_session_dependent && self.should_track_dependencies() {
1852                    const SESSION_DEPENDENT_AGGREGATION_NUMBER: u32 = u32::MAX >> 2;
1853                    AggregationUpdateQueue::run(
1854                        AggregationUpdateJob::UpdateAggregationNumber {
1855                            task_id,
1856                            base_aggregation_number: SESSION_DEPENDENT_AGGREGATION_NUMBER,
1857                            distance: None,
1858                        },
1859                        &mut ctx,
1860                    );
1861                }
1862            } else {
1863                self.track_cache_hit_by_fn(native_fn);
1864            }
1865
1866            task_id
1867        };
1868
1869        // New tasks carry a transient ref so they survive construction. Release it while
1870        // connecting the task to the graph.
1871        connect_child(parent_task, task_id, created_new, ctx);
1872
1873        task_id
1874    }
1875
1876    fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1877        if !self.should_track_dependencies() {
1878            panic!("Dependency tracking is disabled so invalidation is not allowed");
1879        }
1880        invalidate(
1881            [task_id],
1882            #[cfg(feature = "task_dirty_cause")]
1883            TaskDirtyCause::Invalidator,
1884            self.execute_context(turbo_tasks),
1885        );
1886    }
1887
1888    fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1889        if !self.should_track_dependencies() {
1890            panic!("Dependency tracking is disabled so invalidation is not allowed");
1891        }
1892        // `invalidate` checks each chunk for duplicates, but a duplicate split across two chunks
1893        // would only be seen here.
1894        debug_assert!(
1895            tasks.len() <= 1 || tasks.iter().collect::<FxHashSet<_>>().len() == tasks.len(),
1896            "invalidate_tasks() must not be called with duplicate task ids: {tasks:?}"
1897        );
1898        // Large batches are invalidated in parallel chunks. The chunks may run on other threads,
1899        // which don't have the calling task's state, so its priority (the parent priority of the
1900        // dirty tasks) is read once here and passed to every chunk.
1901        let current_task_priority = turbo_tasks.get_current_task_priority();
1902        parallel::for_each_chunk(tasks, |chunk| {
1903            invalidate(
1904                chunk.iter().copied(),
1905                // Called by `TurboTasksApi::invalidate_many*`, i.e. batched `Invalidator`s.
1906                #[cfg(feature = "task_dirty_cause")]
1907                TaskDirtyCause::Invalidator,
1908                self.execute_context_with_priority(turbo_tasks, current_task_priority),
1909            );
1910        });
1911    }
1912
1913    fn invalidate_tasks_set(
1914        &self,
1915        tasks: &AutoSet<TaskId, BuildHasherDefault<FxHasher>, 2>,
1916        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1917    ) {
1918        if !self.should_track_dependencies() {
1919            panic!("Dependency tracking is disabled so invalidation is not allowed");
1920        }
1921        invalidate(
1922            tasks.iter().copied(),
1923            #[cfg(feature = "task_dirty_cause")]
1924            TaskDirtyCause::Unknown,
1925            self.execute_context(turbo_tasks),
1926        );
1927    }
1928
1929    fn invalidate_serialization(
1930        &self,
1931        task_id: TaskId,
1932        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1933    ) {
1934        if task_id.is_transient() {
1935            return;
1936        }
1937        let mut ctx = self.execute_context(turbo_tasks);
1938        let mut task = ctx.task(task_id, TaskDataCategory::Data);
1939        task.invalidate_serialization();
1940    }
1941
1942    fn debug_get_task_description(&self, task_id: TaskId) -> String {
1943        let task = self.storage.access_mut(task_id);
1944        if let Some(value) = task.get_persistent_task_type() {
1945            format!("{task_id:?} {}", value)
1946        } else if let Some(value) = task.get_transient_task_type() {
1947            format!("{task_id:?} {}", value)
1948        } else {
1949            format!("{task_id:?} unknown")
1950        }
1951    }
1952
1953    fn get_task_name(
1954        &self,
1955        task_id: TaskId,
1956        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1957    ) -> String {
1958        let mut ctx = self.execute_context(turbo_tasks);
1959        // Diagnostic path: the caller may name any id, including one that no longer exists, so this
1960        // must not assert existence or create storage for a missing task.
1961        let Some(task) = ctx.try_task(task_id, TaskDataCategory::Data) else {
1962            return "unknown".to_string();
1963        };
1964        if let Some(value) = task.get_persistent_task_type() {
1965            value.to_string()
1966        } else if let Some(value) = task.get_transient_task_type() {
1967            value.to_string()
1968        } else {
1969            // A stale transient id may open a blank task without a type; diagnostics should not
1970            // panic.
1971            "unknown".to_string()
1972        }
1973    }
1974
1975    fn debug_get_cached_task_type(&self, task_id: TaskId) -> Option<CachedTaskTypeArc> {
1976        let task = self.storage.access_mut(task_id);
1977        task.get_persistent_task_type().cloned()
1978    }
1979
1980    fn task_execution_canceled(
1981        &self,
1982        task_id: TaskId,
1983        turbo_tasks: &TurboTasks<TurboTasksBackend>,
1984    ) {
1985        let mut ctx = self.execute_context(turbo_tasks);
1986        let mut task = ctx.task(task_id, TaskDataCategory::All);
1987        if let Some(in_progress) = task.take_in_progress() {
1988            match in_progress {
1989                InProgressState::Scheduled {
1990                    done_event,
1991                    reason: _,
1992                } => done_event.notify(usize::MAX),
1993                InProgressState::InProgress(InProgressStateInner { done_event, .. }) => {
1994                    done_event.notify(usize::MAX)
1995                }
1996                InProgressState::Canceled => {}
1997            }
1998        }
1999        // Notify any readers waiting on in-progress cells so their listeners
2000        // resolve and foreground jobs can finish (prevents stop_and_wait hang).
2001        let in_progress_cells = task.take_in_progress_cells();
2002        if let Some(ref cells) = in_progress_cells {
2003            for state in cells.values() {
2004                state.event.notify(usize::MAX);
2005            }
2006        }
2007
2008        // Give the task a terminal output. `connect_children` treats a child with no output as
2009        // "not computed yet" and marks it dirty to be scheduled, which for a canceled task means
2010        // it stays a dirty container of its parent forever and any strongly consistent reader
2011        // above it never settles.
2012        task.set_output(OutputValue::Error(Arc::new(TaskError::Error(Box::new(
2013            TaskErrorItem {
2014                message: TurboTasksExecutionErrorMessage::PIISafe(std::borrow::Cow::Borrowed(
2015                    "task execution was canceled by shutdown",
2016                )),
2017                source: None,
2018            },
2019        )))));
2020
2021        // Mark the cancelled task as session-dependent dirty so it will be re-executed
2022        // in the next session. Without this, any reader that encounters the cancelled task
2023        // records an error in its output. That error is persisted and would poison
2024        // subsequent builds. By marking the task session-dependent dirty, the next build
2025        // re-executes it, which invalidates dependents and corrects the stale errors.
2026        let data_update = if self.should_track_dependencies() && !task_id.is_transient() {
2027            task.update_dirty_state(Some(Dirtyness::SessionDependent))
2028        } else {
2029            None
2030        };
2031
2032        let old = task.set_in_progress(InProgressState::Canceled);
2033        debug_assert!(old.is_none(), "InProgress already exists");
2034        drop(task);
2035
2036        if let Some(data_update) = data_update {
2037            AggregationUpdateQueue::run(data_update, &mut ctx);
2038        }
2039
2040        drop(in_progress_cells);
2041    }
2042
2043    fn try_start_task_execution(
2044        &self,
2045        task_id: TaskId,
2046        priority: TaskPriority,
2047        turbo_tasks: &TurboTasks<TurboTasksBackend>,
2048    ) -> Option<TaskExecutionSpec<'_>> {
2049        let execution_reason;
2050        let task_type;
2051        #[cfg(feature = "task_dirty_cause")]
2052        let cause;
2053        {
2054            let mut ctx = self.execute_context(turbo_tasks);
2055            let mut task = ctx.task(task_id, TaskDataCategory::All);
2056            task.assert_not_deleted("try_start_task_execution");
2057            task_type = task.get_task_type().to_owned();
2058            let once_task = matches!(task_type, TaskType::Transient(ref tt) if matches!(&**tt, TransientTask::Once(_)));
2059            if let Some(tasks) = task.prefetch() {
2060                drop(task);
2061                ctx.prepare_tasks(tasks, "prefetch");
2062                task = ctx.task(task_id, TaskDataCategory::All);
2063            }
2064            let in_progress = task.take_in_progress()?;
2065            let InProgressState::Scheduled { done_event, reason } = in_progress else {
2066                let old = task.set_in_progress(in_progress);
2067                debug_assert!(old.is_none(), "InProgress already exists");
2068                return None;
2069            };
2070            execution_reason = reason;
2071            #[cfg(feature = "task_dirty_cause")]
2072            {
2073                cause = match task.get_dirty() {
2074                    Some(Dirtyness::Dirty { cause, .. }) => Some(cause.clone()),
2075                    _ => None,
2076                };
2077            }
2078            let old = task.set_in_progress(InProgressState::InProgress(Box::new(
2079                InProgressStateInner {
2080                    stale: false,
2081                    once_task,
2082                    done_event,
2083                    marked_as_completed: false,
2084                    new_children: Default::default(),
2085                },
2086            )));
2087            debug_assert!(old.is_none(), "InProgress already exists");
2088
2089            // Make all current collectibles outdated (remove left-over outdated collectibles)
2090            enum Collectible {
2091                Current(CollectibleRef, i32),
2092                Outdated(CollectibleRef),
2093            }
2094            let collectibles = task
2095                .iter_collectibles()
2096                .map(|(&collectible, &value)| Collectible::Current(collectible, value))
2097                .chain(
2098                    task.iter_outdated_collectibles()
2099                        .map(|(collectible, _count)| Collectible::Outdated(*collectible)),
2100                )
2101                .collect::<Vec<_>>();
2102            for collectible in collectibles {
2103                match collectible {
2104                    Collectible::Current(collectible, value) => {
2105                        let _ = task.insert_outdated_collectible(collectible, value);
2106                    }
2107                    Collectible::Outdated(collectible) => {
2108                        if task
2109                            .collectibles()
2110                            .is_none_or(|m| m.get(&collectible).is_none())
2111                        {
2112                            task.remove_outdated_collectibles(&collectible);
2113                        }
2114                    }
2115                }
2116            }
2117
2118            if self.should_track_dependencies() {
2119                // Make all dependencies outdated
2120                // We copy to outdated which allows us to detect when we drop a dependency, but we
2121                // keep existing dependencies in place to avoid redoing the
2122                // dependency registration work if we re-read them
2123                let cell_dependencies = task.iter_cell_dependencies().collect();
2124                task.set_outdated_cell_dependencies(cell_dependencies);
2125                let cell_dependencies_hashed = task.iter_cell_dependencies_hashed().collect();
2126                task.set_outdated_cell_dependencies_hashed(cell_dependencies_hashed);
2127
2128                let outdated_output_dependencies = task.iter_output_dependencies().collect();
2129                task.set_outdated_output_dependencies(outdated_output_dependencies);
2130
2131                let outdated_collectibles_dependencies =
2132                    task.iter_collectibles_dependencies().collect();
2133                task.set_outdated_collectibles_dependencies(outdated_collectibles_dependencies);
2134            }
2135        }
2136
2137        let (span, future) = match task_type {
2138            TaskType::Cached(task_type) => {
2139                let CachedTaskType {
2140                    native_fn,
2141                    this,
2142                    arg,
2143                } = &*task_type;
2144                (
2145                    native_fn.span(
2146                        task_id.persistence(),
2147                        execution_reason,
2148                        priority,
2149                        #[cfg(feature = "task_dirty_cause")]
2150                        cause.as_ref(),
2151                    ),
2152                    native_fn.execute(*this, &**arg),
2153                )
2154            }
2155            TaskType::Transient(task_type) => {
2156                // `inline_execution` is recorded when a read executed this task on its own thread,
2157                // see `NativeFunction::span`.
2158                let span = tracing::trace_span!(
2159                    "turbo_tasks::root_task",
2160                    inline_execution = tracing::field::Empty
2161                );
2162                let future = match &*task_type {
2163                    TransientTask::Root(f) => f(),
2164                    TransientTask::Once(future_mutex) => take(&mut *future_mutex.lock())?,
2165                };
2166                (span, future)
2167            }
2168        };
2169        Some(TaskExecutionSpec { future, span })
2170    }
2171
2172    /// Returns `Some(priority)` if the task became stale during execution and needs to be
2173    /// re-scheduled at the given priority. The caller (turbo-tasks manager) hands it to the
2174    /// priority runner so re-execution doesn't inherit the original schedule priority.
2175    fn task_execution_completed(
2176        &self,
2177        task_id: TaskId,
2178        result: Result<RawVc, TurboTasksExecutionError>,
2179        cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2180        #[cfg(feature = "verify_determinism")] stateful: bool,
2181        has_invalidator: bool,
2182        turbo_tasks: &TurboTasks<TurboTasksBackend>,
2183    ) -> Option<TaskPriority> {
2184        // Task completion is a 3 step process:
2185        // 1. Remove old edges (dependencies, collectibles, children, cells) and update the
2186        //    aggregation number of the task and the new children.
2187        // 2. Connect the new children to the task (and do the relevant aggregation updates).
2188        // 3. Remove the in-progress state and the dirty flag (and propagate that to uppers), remove
2189        //    no longer existing cells and shrink the task memory. The changes to the task itself
2190        //    happen under one lock, so the next execution can't start before the cleanup is done.
2191
2192        // The snapshot waits for this entire completion path, including its scoped child work,
2193        // before it observes the graph.
2194
2195        // The task might be invalidated during this process, so we need to check the stale flag
2196        // at the start of every step.
2197
2198        #[cfg(not(feature = "trace_task_details"))]
2199        let span = tracing::trace_span!(
2200            "task execution completed",
2201            new_children = tracing::field::Empty
2202        )
2203        .entered();
2204        #[cfg(feature = "trace_task_details")]
2205        let span = tracing::trace_span!(
2206            "task execution completed",
2207            task_id = display(task_id),
2208            result = match result.as_ref() {
2209                Ok(value) => display(either::Either::Left(value)),
2210                Err(err) => display(either::Either::Right(err)),
2211            },
2212            new_children = tracing::field::Empty,
2213            immutable = tracing::field::Empty,
2214            new_output = tracing::field::Empty,
2215            output_dependents = tracing::field::Empty,
2216            aggregation_number = tracing::field::Empty,
2217            stale = tracing::field::Empty,
2218        )
2219        .entered();
2220
2221        let is_error = result.is_err();
2222
2223        let mut ctx = self.execute_context(turbo_tasks);
2224
2225        let TaskExecutionCompletePrepareResult {
2226            new_children,
2227            is_now_immutable,
2228            #[cfg(feature = "verify_determinism")]
2229            no_output_set,
2230            new_output,
2231            #[cfg(feature = "task_dirty_cause")]
2232            function_id,
2233            output_dependent_tasks,
2234            is_recomputation,
2235            is_session_dependent,
2236        } = match self.task_execution_completed_prepare(
2237            &mut ctx,
2238            #[cfg(feature = "trace_task_details")]
2239            &span,
2240            task_id,
2241            result,
2242            cell_counters,
2243            #[cfg(feature = "verify_determinism")]
2244            stateful,
2245            has_invalidator,
2246        ) {
2247            Ok(r) => r,
2248            Err(stale_priority) => {
2249                // Task was stale and has been rescheduled
2250                #[cfg(feature = "trace_task_details")]
2251                span.record("stale", "prepare");
2252                return Some(stale_priority);
2253            }
2254        };
2255
2256        #[cfg(feature = "trace_task_details")]
2257        span.record("new_output", new_output.is_some());
2258        #[cfg(feature = "trace_task_details")]
2259        span.record("output_dependents", output_dependent_tasks.len());
2260
2261        if !output_dependent_tasks.is_empty() {
2262            self.task_execution_completed_invalidate_output_dependent(
2263                &mut ctx,
2264                task_id,
2265                #[cfg(feature = "task_dirty_cause")]
2266                function_id,
2267                output_dependent_tasks,
2268            );
2269        }
2270
2271        let has_new_children = !new_children.is_empty();
2272        span.record("new_children", new_children.len());
2273
2274        if has_new_children
2275            && let Some(stale_priority) =
2276                self.task_execution_completed_connect(&mut ctx, task_id, new_children)
2277        {
2278            // Task was stale and has been rescheduled
2279            #[cfg(feature = "trace_task_details")]
2280            span.record("stale", "connect");
2281            return Some(stale_priority);
2282        }
2283
2284        let (stale_priority, in_progress_cells, removed_data) = self
2285            .task_execution_completed_finish(
2286                &mut ctx,
2287                task_id,
2288                #[cfg(feature = "verify_determinism")]
2289                no_output_set,
2290                new_output,
2291                is_now_immutable,
2292                is_session_dependent,
2293                cell_counters,
2294                is_error,
2295                is_recomputation,
2296            );
2297        if let Some(stale_priority) = stale_priority {
2298            // Task was stale and has been rescheduled
2299            #[cfg(feature = "trace_task_details")]
2300            span.record("stale", "finish");
2301            return Some(stale_priority);
2302        }
2303
2304        // Drop data outside of critical sections
2305        drop(removed_data);
2306        drop(in_progress_cells);
2307
2308        None
2309    }
2310
2311    fn task_execution_completed_prepare(
2312        &self,
2313        ctx: &mut ExecuteContext<'_>,
2314        #[cfg(feature = "trace_task_details")] span: &Span,
2315        task_id: TaskId,
2316        result: Result<RawVc, TurboTasksExecutionError>,
2317        cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2318        #[cfg(feature = "verify_determinism")] stateful: bool,
2319        has_invalidator: bool,
2320    ) -> Result<TaskExecutionCompletePrepareResult, TaskPriority> {
2321        let mut task = ctx.task(task_id, TaskDataCategory::All);
2322        let is_recomputation = task.is_dirty().is_none();
2323        // Without dependency tracking, the SessionDependent dirty state is never read (no session
2324        // restore), so skip the work
2325        let is_session_dependent = self.should_track_dependencies()
2326            && matches!(task.get_task_type(), TaskTypeRef::Cached(tt) if tt.native_fn.is_session_dependent);
2327        let Some(in_progress) = task.get_in_progress_mut() else {
2328            panic!("Task execution completed, but task is not in progress: {task:#?}");
2329        };
2330        if matches!(in_progress, InProgressState::Canceled) {
2331            return Ok(TaskExecutionCompletePrepareResult {
2332                new_children: Default::default(),
2333                is_now_immutable: false,
2334                #[cfg(feature = "verify_determinism")]
2335                no_output_set: false,
2336                #[cfg(feature = "task_dirty_cause")]
2337                function_id: None,
2338                new_output: None,
2339                output_dependent_tasks: Default::default(),
2340                is_recomputation,
2341                is_session_dependent,
2342            });
2343        }
2344        let &mut InProgressState::InProgress(InProgressStateInner {
2345            stale,
2346            ref mut new_children,
2347            once_task: is_once_task,
2348            ..
2349        }) = in_progress
2350        else {
2351            panic!("Task execution completed, but task is not in progress: {task:#?}");
2352        };
2353
2354        // If the task is stale, reschedule it
2355        #[cfg(not(feature = "no_fast_stale"))]
2356        if stale && !is_once_task {
2357            let stale_priority = compute_stale_priority(&task);
2358            let Some(InProgressState::InProgress(InProgressStateInner {
2359                done_event,
2360                mut new_children,
2361                ..
2362            })) = task.take_in_progress()
2363            else {
2364                unreachable!();
2365            };
2366            let old = task.set_in_progress(InProgressState::Scheduled {
2367                done_event,
2368                reason: TaskExecutionReason::Stale,
2369            });
2370            debug_assert!(old.is_none(), "InProgress already exists");
2371            // Remove old children from new_children to leave only the children that had their
2372            // active count increased
2373            for task in task.iter_children() {
2374                new_children.remove(&task);
2375            }
2376            drop(task);
2377
2378            // We need to undo the active count increase for the children since we throw away the
2379            // new_children list now.
2380            AggregationUpdateQueue::run(
2381                AggregationUpdateJob::DecreaseActiveCounts {
2382                    task_ids: new_children.into_iter().collect(),
2383                },
2384                ctx,
2385            );
2386            return Err(stale_priority);
2387        }
2388
2389        // take the children from the task to process them
2390        let mut new_children = take(new_children);
2391
2392        // get the function id for the dirty cause
2393        #[cfg(feature = "task_dirty_cause")]
2394        let function_id = match task.get_task_type() {
2395            TaskTypeRef::Cached(task_type) => {
2396                Some(turbo_tasks::registry::get_function_id(task_type.native_fn))
2397            }
2398            TaskTypeRef::Transient(_) => None,
2399        };
2400
2401        // handle stateful (only tracked when verify_determinism is enabled)
2402        #[cfg(feature = "verify_determinism")]
2403        if stateful {
2404            task.set_stateful(true);
2405        }
2406
2407        // handle has_invalidator
2408        if has_invalidator {
2409            task.set_invalidator(true);
2410        }
2411
2412        // handle cell counters: update max index and remove cells that are no longer used
2413        // On error, skip this update: the task may have failed before creating all cells it
2414        // normally creates, so cell_counters is incomplete. Clearing cell_type_max_index entries
2415        // based on partial counters would cause "cell no longer exists" errors for tasks that
2416        // still hold dependencies on those cells. The old cell data is preserved on error
2417        // (see task_execution_completed_cleanup), so keeping cell_type_max_index consistent with
2418        // that data is correct.
2419        // NOTE: This must stay in sync with task_execution_completed_cleanup, which similarly
2420        // skips cell data removal on error.
2421        if result.is_ok() || is_recomputation {
2422            let old_counters: FxHashMap<_, _> = task
2423                .iter_cell_type_max_index()
2424                .map(|(&k, &v)| (k, v))
2425                .collect();
2426            let mut counters_to_remove = old_counters.clone();
2427
2428            for (&cell_type, &max_index) in cell_counters.iter() {
2429                if let Some(old_max_index) = counters_to_remove.remove(&cell_type) {
2430                    if old_max_index != max_index {
2431                        task.insert_cell_type_max_index(cell_type, max_index);
2432                    }
2433                } else {
2434                    task.insert_cell_type_max_index(cell_type, max_index);
2435                }
2436            }
2437            for (cell_type, _) in counters_to_remove {
2438                task.remove_cell_type_max_index(&cell_type);
2439            }
2440        }
2441
2442        let mut queue = AggregationUpdateQueue::new();
2443
2444        let mut old_edges = Vec::new();
2445
2446        let has_children = !new_children.is_empty();
2447        let is_immutable = task.immutable();
2448        let task_dependencies_for_immutable =
2449            // Task was previously marked as immutable
2450            if !is_immutable
2451            // Task is not session dependent (session dependent tasks can change between sessions)
2452            && !is_session_dependent
2453            // Task has no invalidator
2454            && !task.invalidator()
2455            // Task has no dependencies on collectibles
2456            && task.is_collectibles_dependencies_empty()
2457        {
2458            Some(
2459                // Collect all dependencies on tasks to check if all dependencies are immutable.
2460                task.iter_output_dependencies()
2461                    .chain(task.iter_cell_dependencies().map(|r| r.task))
2462                    .chain(task.iter_cell_dependencies_hashed().map(|(r, _)| r.task))
2463                    .collect::<FxHashSet<_>>(),
2464            )
2465        } else {
2466            None
2467        };
2468
2469        if has_children {
2470            // Prepare all new children
2471            let _aggregation_number =
2472                prepare_new_children(task_id, &mut task, &new_children, &mut queue);
2473
2474            #[cfg(feature = "trace_task_details")]
2475            span.record("aggregation_number", _aggregation_number);
2476
2477            // Filter actual new children
2478            old_edges.extend(
2479                task.iter_children()
2480                    .filter(|task| !new_children.remove(task))
2481                    .map(OutdatedEdge::Child),
2482            );
2483        } else {
2484            old_edges.extend(task.iter_children().map(OutdatedEdge::Child));
2485        }
2486
2487        old_edges.extend(
2488            task.iter_outdated_collectibles()
2489                .map(|(&collectible, &count)| OutdatedEdge::Collectible(collectible, count)),
2490        );
2491
2492        if self.should_track_dependencies() {
2493            // IMPORTANT: Use iter_outdated_* here, NOT iter_* (active dependencies).
2494            // At execution start, active deps are copied to outdated as a "before" snapshot.
2495            // During execution, new deps are added to active.
2496            // Here at completion, we clean up only the OUTDATED deps (the "before" snapshot).
2497            // Using iter_* (active) instead would incorrectly clean up deps that are still valid,
2498            // breaking dependency tracking.
2499            old_edges.extend(task.iter_outdated_cell_dependencies().map(|cell| {
2500                OutdatedEdge::CellDependency {
2501                    dependent: task_id,
2502                    cell,
2503                }
2504            }));
2505            old_edges.extend(
2506                task.iter_outdated_cell_dependencies_hashed()
2507                    .map(|(cell, key)| OutdatedEdge::HashedCellDependency {
2508                        dependent: task_id,
2509                        cell,
2510                        key,
2511                    }),
2512            );
2513            old_edges.extend(task.iter_outdated_output_dependencies().map(|output_task| {
2514                OutdatedEdge::OutputDependency {
2515                    dependent: task_id,
2516                    output_task,
2517                }
2518            }));
2519            old_edges.extend(
2520                task.iter_outdated_collectibles_dependencies()
2521                    .map(OutdatedEdge::CollectiblesDependency),
2522            );
2523        }
2524
2525        // Check if output need to be updated
2526        let current_output = task.get_output();
2527        #[cfg(feature = "verify_determinism")]
2528        let no_output_set = current_output.is_none();
2529        let new_output = match result.map(RawVc::unpack) {
2530            Ok(RawVcUnpacked::TaskOutput(output_task_id)) => {
2531                if let Some(OutputValue::Output(current_task_id)) = current_output
2532                    && *current_task_id == output_task_id
2533                {
2534                    None
2535                } else {
2536                    Some(OutputValue::Output(output_task_id))
2537                }
2538            }
2539            Ok(RawVcUnpacked::TaskCell(output_task_id, cell)) => {
2540                if let Some(OutputValue::Cell(CellRef {
2541                    task: current_task_id,
2542                    cell: current_cell,
2543                })) = current_output
2544                    && *current_task_id == output_task_id
2545                    && *current_cell == cell
2546                {
2547                    None
2548                } else {
2549                    Some(OutputValue::Cell(CellRef {
2550                        task: output_task_id,
2551                        cell,
2552                    }))
2553                }
2554            }
2555            Ok(RawVcUnpacked::LocalOutput(..)) => {
2556                panic!("Non-local tasks must not return a local Vc");
2557            }
2558            Err(err) => {
2559                if let Some(OutputValue::Error(old_error)) = current_output
2560                    && **old_error == err
2561                {
2562                    None
2563                } else {
2564                    Some(OutputValue::Error(Arc::new((&err).into())))
2565                }
2566            }
2567        };
2568        let mut output_dependent_tasks = SmallVec::<[_; 4]>::new();
2569        // When output has changed, grab the dependent tasks
2570        if new_output.is_some() && ctx.should_track_dependencies() {
2571            output_dependent_tasks = task.iter_output_dependent().collect();
2572        }
2573
2574        drop(task);
2575
2576        // Check if the task can be marked as immutable
2577        let mut is_now_immutable = false;
2578        if let Some(dependencies) = task_dependencies_for_immutable
2579            && dependencies
2580                .iter()
2581                .all(|&task_id| ctx.task(task_id, TaskDataCategory::Data).immutable())
2582        {
2583            is_now_immutable = true;
2584        }
2585        #[cfg(feature = "trace_task_details")]
2586        span.record("immutable", is_immutable || is_now_immutable);
2587
2588        if !queue.is_empty() || !old_edges.is_empty() {
2589            #[cfg(any(
2590                feature = "trace_task_completion",
2591                feature = "trace_aggregation_update_stats"
2592            ))]
2593            let _span = tracing::trace_span!(
2594                "remove old edges and prepare new children",
2595                stats = tracing::field::Empty
2596            )
2597            .entered();
2598            // Remove outdated edges first, before removing in_progress+dirty flag.
2599            // We need to make sure all outdated edges are removed before the task can potentially
2600            // be scheduled and executed again
2601            #[cfg(feature = "trace_aggregation_update_stats")]
2602            {
2603                let stats = cleanup_old_edges(task_id, old_edges, queue, ctx);
2604                _span.record("stats", tracing::field::debug(stats));
2605            }
2606            #[cfg(not(feature = "trace_aggregation_update_stats"))]
2607            cleanup_old_edges(task_id, old_edges, queue, ctx);
2608        }
2609
2610        Ok(TaskExecutionCompletePrepareResult {
2611            new_children,
2612            is_now_immutable,
2613            #[cfg(feature = "verify_determinism")]
2614            no_output_set,
2615            #[cfg(feature = "task_dirty_cause")]
2616            function_id,
2617            new_output,
2618            output_dependent_tasks,
2619            is_recomputation,
2620            is_session_dependent,
2621        })
2622    }
2623
2624    fn task_execution_completed_invalidate_output_dependent(
2625        &self,
2626        ctx: &mut ExecuteContext<'_>,
2627        task_id: TaskId,
2628        #[cfg(feature = "task_dirty_cause")] function_id: Option<FunctionId>,
2629        output_dependent_tasks: SmallVec<[TaskId; 4]>,
2630    ) {
2631        debug_assert!(!output_dependent_tasks.is_empty());
2632
2633        #[cfg(feature = "task_dirty_cause")]
2634        let cause = match function_id {
2635            Some(function) => TaskDirtyCause::OutputChange { function },
2636            None => TaskDirtyCause::RootOutputChange,
2637        };
2638
2639        if output_dependent_tasks.len() > 1 {
2640            ctx.prepare_tasks(
2641                output_dependent_tasks
2642                    .iter()
2643                    .map(|&id| (id, TaskDataCategory::All)),
2644                "invalidate output dependents",
2645            );
2646        }
2647
2648        fn process_output_dependents(
2649            ctx: &mut ExecuteContext<'_>,
2650            task_id: TaskId,
2651            #[cfg(feature = "task_dirty_cause")] cause: &TaskDirtyCause,
2652            dependent_task_id: TaskId,
2653            queue: &mut AggregationUpdateQueue,
2654        ) {
2655            #[cfg(feature = "trace_task_output_dependencies")]
2656            let span = tracing::trace_span!(
2657                "invalidate output dependency",
2658                task = %task_id,
2659                dependent_task = %dependent_task_id,
2660                result = tracing::field::Empty,
2661            )
2662            .entered();
2663            let mut make_stale = true;
2664            let mut dependent = ctx.task(dependent_task_id, TaskDataCategory::All);
2665            let transient_task_type = dependent.get_transient_task_type();
2666            if transient_task_type.is_some_and(|tt| matches!(&**tt, TransientTask::Once(_))) {
2667                // once tasks are never invalidated
2668                #[cfg(feature = "trace_task_output_dependencies")]
2669                span.record("result", "once task");
2670                return;
2671            }
2672            if dependent.outdated_output_dependencies_contains(&task_id) {
2673                #[cfg(feature = "trace_task_output_dependencies")]
2674                span.record("result", "outdated dependency");
2675                // output dependency is outdated, so it hasn't read the output yet
2676                // and doesn't need to be invalidated
2677                // But importantly we still need to make the task dirty as it should no longer
2678                // be considered as "recomputation".
2679                make_stale = false;
2680            } else if !dependent.output_dependencies_contains(&task_id) {
2681                // output dependency has been removed, so the task doesn't depend on the
2682                // output anymore and doesn't need to be invalidated
2683                #[cfg(feature = "trace_task_output_dependencies")]
2684                span.record("result", "no backward dependency");
2685                return;
2686            }
2687            make_task_dirty_internal(
2688                &mut dependent,
2689                make_stale,
2690                #[cfg(feature = "task_dirty_cause")]
2691                cause.clone(),
2692                queue,
2693                ctx,
2694            );
2695            #[cfg(feature = "trace_task_output_dependencies")]
2696            span.record("result", "marked dirty");
2697        }
2698
2699        if output_dependent_tasks.len() > DEPENDENT_TASKS_DIRTY_PARALLELIZATION_THRESHOLD {
2700            let chunk_size = good_chunk_size(output_dependent_tasks.len());
2701            let chunks = into_chunks(output_dependent_tasks.to_vec(), chunk_size);
2702            let _ = scope_bounded(chunks.len(), |scope| {
2703                for chunk in chunks {
2704                    let child_ctx = ctx.child_context();
2705                    #[cfg(feature = "task_dirty_cause")]
2706                    let cause = &cause;
2707                    scope.spawn(move || {
2708                        let mut ctx = child_ctx.create();
2709                        let mut queue = AggregationUpdateQueue::new();
2710                        for dependent_task_id in chunk {
2711                            process_output_dependents(
2712                                &mut ctx,
2713                                task_id,
2714                                #[cfg(feature = "task_dirty_cause")]
2715                                cause,
2716                                dependent_task_id,
2717                                &mut queue,
2718                            )
2719                        }
2720                        queue.execute(&mut ctx);
2721                    });
2722                }
2723            });
2724        } else {
2725            let mut queue = AggregationUpdateQueue::new();
2726            for dependent_task_id in output_dependent_tasks {
2727                process_output_dependents(
2728                    ctx,
2729                    task_id,
2730                    #[cfg(feature = "task_dirty_cause")]
2731                    &cause,
2732                    dependent_task_id,
2733                    &mut queue,
2734                );
2735            }
2736            queue.execute(ctx);
2737        }
2738    }
2739
2740    fn task_execution_completed_connect(
2741        &self,
2742        ctx: &mut ExecuteContext<'_>,
2743        task_id: TaskId,
2744        new_children: FxHashSet<TaskId>,
2745    ) -> Option<TaskPriority> {
2746        debug_assert!(!new_children.is_empty());
2747
2748        let mut task = ctx.task(task_id, TaskDataCategory::All);
2749        let Some(in_progress) = task.get_in_progress() else {
2750            panic!("Task execution completed, but task is not in progress: {task:#?}");
2751        };
2752        if matches!(in_progress, InProgressState::Canceled) {
2753            // Task was canceled in the meantime, so we don't connect the children
2754            return None;
2755        }
2756        let InProgressState::InProgress(InProgressStateInner {
2757            #[cfg(not(feature = "no_fast_stale"))]
2758            stale,
2759            once_task: is_once_task,
2760            ..
2761        }) = in_progress
2762        else {
2763            panic!("Task execution completed, but task is not in progress: {task:#?}");
2764        };
2765
2766        // If the task is stale, reschedule it
2767        #[cfg(not(feature = "no_fast_stale"))]
2768        if *stale && !is_once_task {
2769            let stale_priority = compute_stale_priority(&task);
2770            let Some(InProgressState::InProgress(InProgressStateInner { done_event, .. })) =
2771                task.take_in_progress()
2772            else {
2773                unreachable!();
2774            };
2775            let old = task.set_in_progress(InProgressState::Scheduled {
2776                done_event,
2777                reason: TaskExecutionReason::Stale,
2778            });
2779            debug_assert!(old.is_none(), "InProgress already exists");
2780            drop(task);
2781
2782            // All `new_children` are currently hold active with an active count and we need to undo
2783            // that. (We already filtered out the old children from that list)
2784            AggregationUpdateQueue::run(
2785                AggregationUpdateJob::DecreaseActiveCounts {
2786                    task_ids: new_children.into_iter().collect(),
2787                },
2788                ctx,
2789            );
2790            return Some(stale_priority);
2791        }
2792
2793        let has_active_count = ctx.should_track_activeness()
2794            && task
2795                .get_activeness()
2796                .is_some_and(|activeness| activeness.active_counter > 0);
2797        connect_children(
2798            ctx,
2799            task_id,
2800            task,
2801            new_children,
2802            has_active_count,
2803            ctx.should_track_activeness(),
2804        );
2805
2806        None
2807    }
2808
2809    #[allow(clippy::type_complexity)]
2810    fn task_execution_completed_finish(
2811        &self,
2812        ctx: &mut ExecuteContext<'_>,
2813        task_id: TaskId,
2814        #[cfg(feature = "verify_determinism")] no_output_set: bool,
2815        new_output: Option<OutputValue>,
2816        is_now_immutable: bool,
2817        is_session_dependent: bool,
2818        cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2819        is_error: bool,
2820        is_recomputation: bool,
2821    ) -> (
2822        Option<TaskPriority>,
2823        Option<
2824            auto_hash_map::AutoMap<CellId, InProgressCellState, BuildHasherDefault<FxHasher>, 1>,
2825        >,
2826        Vec<SharedReference>,
2827    ) {
2828        let mut task = ctx.task(task_id, TaskDataCategory::All);
2829        let Some(in_progress) = task.take_in_progress() else {
2830            panic!("Task execution completed, but task is not in progress: {task:#?}");
2831        };
2832        if matches!(in_progress, InProgressState::Canceled) {
2833            // Task was canceled in the meantime, so we don't finish it
2834            let removed_data = Self::task_execution_completed_cleanup(
2835                &mut task,
2836                cell_counters,
2837                is_error,
2838                is_recomputation,
2839            );
2840            return (None, None, removed_data);
2841        }
2842        let InProgressState::InProgress(InProgressStateInner {
2843            done_event,
2844            once_task: is_once_task,
2845            stale,
2846            marked_as_completed: _,
2847            new_children,
2848        }) = in_progress
2849        else {
2850            panic!("Task execution completed, but task is not in progress: {task:#?}");
2851        };
2852        debug_assert!(new_children.is_empty());
2853
2854        // If the task is stale, reschedule it
2855        if stale && !is_once_task {
2856            let stale_priority = compute_stale_priority(&task);
2857            let old = task.set_in_progress(InProgressState::Scheduled {
2858                done_event,
2859                reason: TaskExecutionReason::Stale,
2860            });
2861            debug_assert!(old.is_none(), "InProgress already exists");
2862            return (Some(stale_priority), None, Vec::new());
2863        }
2864
2865        // Set the output if it has changed
2866        let mut old_content = None;
2867        if let Some(value) = new_output {
2868            old_content = task.set_output(value);
2869        }
2870
2871        // If the task has no invalidator and has no mutable dependencies, it does not have a way
2872        // to be invalidated and we can mark it as immutable.
2873        if is_now_immutable {
2874            task.set_immutable(true);
2875        }
2876
2877        // Notify in progress cells and remove all of them
2878        let in_progress_cells = task.take_in_progress_cells();
2879        if let Some(ref cells) = in_progress_cells {
2880            for state in cells.values() {
2881                state.event.notify(usize::MAX);
2882            }
2883        }
2884
2885        // Compute and apply the new dirty state, propagating to aggregating ancestors
2886        let new_dirtyness = if is_session_dependent {
2887            Some(Dirtyness::SessionDependent)
2888        } else {
2889            None
2890        };
2891        #[cfg(feature = "verify_determinism")]
2892        let dirty_changed = task.get_dirty().cloned() != new_dirtyness;
2893        let data_update = task.update_dirty_state(new_dirtyness);
2894
2895        // Under verify_determinism we re-run a task whose dirty state or output unexpectedly
2896        // changed during execution to confirm determinism. The leaf priority keeps these
2897        // verification reruns at the highest priority.
2898        #[cfg(feature = "verify_determinism")]
2899        let stale_priority: Option<TaskPriority> =
2900            ((dirty_changed || no_output_set) && !task_id.is_transient() && !is_once_task)
2901                .then(TaskPriority::leaf);
2902        #[cfg(not(feature = "verify_determinism"))]
2903        let stale_priority: Option<TaskPriority> = None;
2904        let removed_data = if stale_priority.is_some() {
2905            let old = task.set_in_progress(InProgressState::Scheduled {
2906                done_event,
2907                reason: TaskExecutionReason::Stale,
2908            });
2909            debug_assert!(old.is_none(), "InProgress already exists");
2910            drop(task);
2911            Vec::new()
2912        } else {
2913            // Clean up before the completion becomes visible: once the guard is dropped, the next
2914            // execution can start, and cleaning up after that would remove its state.
2915            let removed_data = Self::task_execution_completed_cleanup(
2916                &mut task,
2917                cell_counters,
2918                is_error,
2919                is_recomputation,
2920            );
2921            drop(task);
2922
2923            // Notify dependent tasks that are waiting for this task to finish
2924            done_event.notify(usize::MAX);
2925            removed_data
2926        };
2927
2928        drop(old_content);
2929
2930        if let Some(data_update) = data_update {
2931            AggregationUpdateQueue::run(data_update, ctx);
2932        }
2933
2934        // We return so the data can be dropped outside of critical sections
2935        (stale_priority, in_progress_cells, removed_data)
2936    }
2937
2938    /// Removes the cells this execution no longer has and frees the per-execution bookkeeping
2939    /// (`cleanup_after_execution`).
2940    ///
2941    /// Runs under the same task guard that completes the execution, before the completion is
2942    /// published (`in_progress` removed and `done_event` notified). Otherwise a waiter could start
2943    /// the next execution in between, and this cleanup would remove that execution's state, e.g.
2944    /// the outdated dependencies it was just given.
2945    ///
2946    /// Returns the removed cell data, so it can be dropped outside of the critical section.
2947    fn task_execution_completed_cleanup(
2948        task: &mut TaskGuard<'_>,
2949        cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2950        is_error: bool,
2951        is_recomputation: bool,
2952    ) -> Vec<SharedReference> {
2953        let mut removed_cell_data = Vec::new();
2954        // An error is potentially caused by a eventual consistency, so we avoid updating cells
2955        // after an error as it is likely transient and we want to keep the dependent tasks
2956        // clean to avoid re-executions.
2957        // NOTE: This must stay in sync with task_execution_completed_prepare, which similarly
2958        // skips cell_type_max_index updates on error.
2959        if !is_error || is_recomputation {
2960            // Remove no longer existing cells and
2961            // find all outdated data items (removed cells, outdated edges)
2962            // Note: We do not mark the tasks as dirty here, as these tasks are unused or stale
2963            // anyway and we want to avoid needless re-executions. When the cells become
2964            // used again, they are invalidated from the update cell operation.
2965            let to_remove: Vec<_> = task
2966                .iter_cell_data()
2967                .filter_map(|(cell, _)| {
2968                    cell_counters
2969                        .get(&cell.type_id())
2970                        .is_none_or(|start_index| cell.index() >= *start_index)
2971                        .then_some(*cell)
2972                })
2973                .collect();
2974            removed_cell_data.reserve_exact(to_remove.len());
2975            for cell in to_remove {
2976                if let Some(data) =
2977                    task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence)
2978                {
2979                    removed_cell_data.push(data);
2980                }
2981            }
2982            // Remove cell_data_hash entries for cells that no longer exist
2983            let to_remove_hash: Vec<_> = task
2984                .iter_cell_data_hash()
2985                .filter_map(|(cell, _)| {
2986                    cell_counters
2987                        .get(&cell.type_id())
2988                        .is_none_or(|start_index| cell.index() >= *start_index)
2989                        .then_some(*cell)
2990                })
2991                .collect();
2992            for cell in to_remove_hash {
2993                task.remove_cell_data_hash(&cell);
2994            }
2995        }
2996
2997        // Free memory now that execution is complete.
2998        task.cleanup_after_execution();
2999
3000        // Return so we can drop outside of critical sections
3001        removed_cell_data
3002    }
3003
3004    /// Warns through the compilation event queue (so Next.js logs it) that a snapshot is still
3005    /// waiting for active operations to finish.
3006    fn report_slow_settle_for_snapshot(
3007        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3008        slow: SlowSettle,
3009    ) {
3010        let SlowSettle {
3011            waited,
3012            active_at_start,
3013            active_now,
3014        } = slow;
3015        turbo_tasks.send_compilation_event(Arc::new(DiagnosticEvent::new(
3016            Severity::Warning,
3017            format!(
3018                "Writing to the filesystem cache has been waiting {}s for active operations to \
3019                 finish ({active_at_start} active when it started, {active_now} active now)",
3020                waited.as_secs(),
3021            ),
3022        )));
3023    }
3024
3025    /// Prints the standard message emitted when the background persisting process stops due to an
3026    /// unrecoverable write error. The caller is responsible for returning from the background job.
3027    fn log_unrecoverable_persist_error() {
3028        eprintln!(
3029            "Persisting is disabled for this session due to an unrecoverable error. Stopping the \
3030             background persisting process."
3031        );
3032    }
3033
3034    fn run_backend_job<'a>(
3035        &'a self,
3036        job: TurboTasksBackendJob,
3037        turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
3038    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
3039        Box::pin(async move {
3040            match job {
3041                TurboTasksBackendJob::Snapshot => {
3042                    debug_assert!(self.should_persist());
3043
3044                    /// Configurable idle timeout for snapshot persistence.
3045                    /// Defaults to 2 seconds if not set or if the value is invalid.
3046                    static IDLE_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
3047                        std::env::var("TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS")
3048                            .ok()
3049                            .and_then(|v| v.parse::<u64>().ok())
3050                            .map(Duration::from_millis)
3051                            .unwrap_or(Duration::from_secs(2))
3052                    });
3053
3054                    /// Minimum accumulated compilation (non-idle) time since the last persisted
3055                    /// snapshot before a periodic snapshot is worth its fixed
3056                    /// overhead/ Persistence exists to save work on the next run, and
3057                    /// accumulated compilation time is a good proxy for how
3058                    /// much work a snapshot would save. Snapshots triggered by
3059                    /// shutdown or tests bypass this.
3060                    /// Defaults to 1s
3061                    static MIN_SNAPSHOT_ACTIVE_TIME: LazyLock<Duration> = LazyLock::new(|| {
3062                        std::env::var("TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS")
3063                            .ok()
3064                            .and_then(|v| v.parse::<u64>().ok())
3065                            .map(Duration::from_millis)
3066                            .unwrap_or(Duration::from_secs(1))
3067                    });
3068
3069                    let mut last_snapshot = self.start_time;
3070                    let mut idle_start_listener = self.idle_start_event.listen();
3071                    let mut idle_end_listener = self.idle_end_event.listen();
3072                    // Whether to immediately set an idle timeout if possible.
3073                    // Set to false if we don't persist anything in a cycle.
3074                    let mut fresh_idle = true;
3075                    let mut is_first = true;
3076                    // Owns the eviction policy (mode + threshold state); decides each
3077                    // cycle whether an eviction sweep is worthwhile.
3078                    let mut eviction_control = EvictionControl::new(self.options.eviction_mode);
3079                    // Accumulated compilation (non-idle) time since the last persisted
3080                    // snapshot. Runs while not idle, stops while idle. Used to skip periodic
3081                    // snapshots that wouldn't save enough work to justify their fixed
3082                    // overhead. Reset after each completed snapshot attempt.
3083                    let mut active_time = Stopwatch::new();
3084                    'outer: loop {
3085                        const FIRST_SNAPSHOT_WAIT: Duration = Duration::from_secs(300);
3086                        const SNAPSHOT_INTERVAL: Duration = Duration::from_secs(120);
3087                        let idle_timeout = *IDLE_TIMEOUT;
3088                        let (time, mut reason) = if is_first {
3089                            (FIRST_SNAPSHOT_WAIT, SnapshotReason::InitialSnapshotTimeout)
3090                        } else {
3091                            (SNAPSHOT_INTERVAL, SnapshotReason::RegularSnapshotInterval)
3092                        };
3093
3094                        // Begin (or continue) timing if we're not idle. Transitions below
3095                        // keep this in sync via the idle events.
3096                        if !turbo_tasks.is_idle() {
3097                            active_time.start();
3098                        }
3099
3100                        let until = last_snapshot + time;
3101                        if until > Instant::now() {
3102                            let mut stop_listener = self.stopping_event.listen();
3103                            if *self.stopping.read() {
3104                                return;
3105                            }
3106                            let mut idle_time = if turbo_tasks.is_idle() && fresh_idle {
3107                                Instant::now() + idle_timeout
3108                            } else {
3109                                far_future()
3110                            };
3111                            loop {
3112                                tokio::select! {
3113                                    _ = &mut stop_listener => {
3114                                        return;
3115                                    },
3116                                    _ = &mut idle_start_listener => {
3117                                        // Became idle: stop accumulating active time.
3118                                        active_time.stop();
3119                                        idle_time = Instant::now() + idle_timeout;
3120                                        idle_start_listener = self.idle_start_event.listen()
3121                                    },
3122                                    _ = &mut idle_end_listener => {
3123                                        // Became active: resume accumulating active time.
3124                                        active_time.start();
3125                                        idle_time = far_future();
3126                                        idle_end_listener = self.idle_end_event.listen()
3127                                    },
3128                                    _ = tokio::time::sleep_until(until) => {
3129                                        break;
3130                                    },
3131                                    _ = tokio::time::sleep_until(idle_time) => {
3132                                        if turbo_tasks.is_idle() {
3133                                            reason = SnapshotReason::IdleTimeout;
3134                                            break;
3135                                        }
3136                                    },
3137                                }
3138                            }
3139                        }
3140
3141                        // Persistence exists to save work; accumulated compilation time is
3142                        // the proxy for how much work a snapshot would save. Skip periodic
3143                        // snapshots below the threshold. Shutdown (Stop) and tests must always
3144                        // flush, but they don't run through this loop (Stop is handled in
3145                        // stop(), Test in snapshot_and_evict_for_testing), so every reason
3146                        // reaching here is subject to the threshold.
3147                        if active_time.elapsed() < *MIN_SNAPSHOT_ACTIVE_TIME {
3148                            // Not enough compilation time accumulated — skip the (potentially
3149                            // expensive) snapshot. Advance scheduling state as if we had run,
3150                            // but keep active_time running so it carries toward the next cycle.
3151                            // Clear fresh_idle (as a no-data snapshot would) so we don't re-arm
3152                            // the short idle timeout every cycle and spin while idle; a new
3153                            // active period (idle_end) re-triggers timing and progress.
3154                            fresh_idle = false;
3155                            is_first = false;
3156                            last_snapshot = Instant::now();
3157                            continue 'outer;
3158                        }
3159
3160                        // Create a root span shared by both the snapshot/persist
3161                        // work and the subsequent compaction so they appear
3162                        // grouped together in trace viewers.
3163                        let background_span =
3164                            tracing::info_span!(parent: None, "background snapshot");
3165                        match self.snapshot_and_persist(background_span.id(), reason, turbo_tasks) {
3166                            Err(err) => {
3167                                // save_snapshot consumed persisted_task_cache_log entries;
3168                                // further snapshots would corrupt the task graph.
3169                                eprintln!("Persisting failed: {err:?}");
3170                                Self::log_unrecoverable_persist_error();
3171                                return;
3172                            }
3173                            Ok(None) => {
3174                                // An idle snapshot found operations in flight. Nothing was
3175                                // persisted, so keep the accumulated active time and scheduling
3176                                // state. Re-arm the idle timeout so we retry if we are still
3177                                // idle; otherwise the next idle period or interval picks it up.
3178                                fresh_idle = true;
3179                                continue 'outer;
3180                            }
3181                            Ok(Some((snapshot_start, new_data, _gc_outcome))) => {
3182                                // if we see 'new_data' then the next idle transition is 'fresh'
3183                                fresh_idle = new_data;
3184                                is_first = false;
3185                                last_snapshot = snapshot_start;
3186                                // A completed snapshot attempt means the accumulated
3187                                // compilation time has been accounted for: either it produced
3188                                // work we just persisted (new_data), or it produced nothing to
3189                                // persist — in which case that time amounted to no savable
3190                                // state. Either way, reset so stale time doesn't carry into the
3191                                // next cycle. If still timing an active period, reset() keeps it
3192                                // running from now so time before this snapshot isn't counted.
3193                                active_time.reset();
3194
3195                                // Polls the idle-end event without blocking. Returns
3196                                // `true` and refreshes the listener if idle has ended,
3197                                // `false` if we are still idle.
3198                                macro_rules! check_idle_ended {
3199                                    () => {{
3200                                        tokio::select! {
3201                                            biased;
3202                                            _ = &mut idle_end_listener => {
3203                                                idle_end_listener = self.idle_end_event.listen();
3204                                                true
3205                                            },
3206                                            _ = std::future::ready(()) => false,
3207                                        }
3208                                    }};
3209                                }
3210                                // Evict persisted tasks from memory to reclaim space.
3211                                // Like compaction, this runs after snapshot_and_persist
3212                                // as a separate concern.
3213                                //
3214                                // TODO: improve the eviction policy further. Better
3215                                // strategies to consider:
3216                                //   - Recency data: track last-access time per task and evict
3217                                //     least-recently-used entries first rather than all at once.
3218                                //   - Eviction intensity: partial sweeps (evict a fraction of
3219                                //     eligible tasks per cycle) to reduce latency spikes.
3220                                // `eviction_control` owns the mode + threshold decision. On a
3221                                // skipped cycle its baseline and `ran_eviction` stay untouched
3222                                // so growth accumulates toward the next cycle.
3223                                let ran_eviction = if eviction_control.should_evict(new_data) {
3224                                    // NOTE: we do not check for idle here, eviction is fast and
3225                                    // when enabled we should expect it to reclaim substantial
3226                                    // memory so racing with execution is as likely to save time as
3227                                    // cost it.
3228                                    self.storage.evict_after_snapshot(background_span.id());
3229                                    true
3230                                } else {
3231                                    false
3232                                };
3233
3234                                // Compact while idle (up to limit), regardless of
3235                                // whether the snapshot had new data.
3236                                // `background_span` is not entered here because
3237                                // `EnteredSpan` is `!Send` and would prevent the
3238                                // future from being sent across threads when it
3239                                // suspends at the `select!` await below.
3240                                let mut ran_compaction = false;
3241                                const MAX_IDLE_COMPACTION_PASSES: usize = 10;
3242                                for _ in 0..MAX_IDLE_COMPACTION_PASSES {
3243                                    if check_idle_ended!() {
3244                                        continue 'outer;
3245                                    }
3246                                    // Enter the span only around the synchronous
3247                                    // compact() call so we never hold an
3248                                    // `EnteredSpan` across an await point.
3249                                    let compact_span = tracing::info_span!(
3250                                        parent: background_span.id(),
3251                                        "compact database",
3252                                        stats = tracing::field::Empty,
3253                                    )
3254                                    .entered();
3255                                    match self.backing_storage.compact() {
3256                                        Ok(Some(stats)) => {
3257                                            compact_span.record("stats", display(stats));
3258                                            ran_compaction = true;
3259                                        }
3260                                        Ok(None) => break,
3261                                        Err(err) => {
3262                                            eprintln!("Compaction failed: {err:?}");
3263                                            if self.backing_storage.has_unrecoverable_write_error()
3264                                            {
3265                                                Self::log_unrecoverable_persist_error();
3266                                                return;
3267                                            }
3268                                            break;
3269                                        }
3270                                    }
3271                                }
3272                                // After running snapshotting/eviction/compaction we have churned a
3273                                // _lot_ of memory if we are still
3274                                // idle tell `mimalloc` that now would be a good time to release
3275                                // memory back to the OS
3276                                if !check_idle_ended!()
3277                                    && (new_data || ran_compaction || ran_eviction)
3278                                {
3279                                    TurboMalloc::collect(true);
3280                                }
3281
3282                                // Sample the new baseline after the collect above, which is what
3283                                // makes the evicted memory show up in `memory_usage`.
3284                                if ran_eviction {
3285                                    eviction_control.record_eviction();
3286                                }
3287                            }
3288                        }
3289                    }
3290                }
3291            }
3292        })
3293    }
3294
3295    fn try_read_own_task_cell(
3296        &self,
3297        task_id: TaskId,
3298        cell: CellId,
3299        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3300    ) -> Result<TypedCellContent> {
3301        let mut ctx = self.execute_context(turbo_tasks);
3302        let task = ctx.task(task_id, TaskDataCategory::Data);
3303        task.assert_not_deleted("try_read_own_task_cell");
3304        if let Some(content) = task.get_cell_data(&cell).cloned() {
3305            Ok(CellContent(Some(content)).into_typed(cell.type_id()))
3306        } else {
3307            Ok(CellContent(None).into_typed(cell.type_id()))
3308        }
3309    }
3310
3311    fn read_task_collectibles(
3312        &self,
3313        task_id: TaskId,
3314        collectible_type: TraitTypeId,
3315        reader_id: Option<TaskId>,
3316        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3317    ) -> AutoMap<RawVc, i32, BuildHasherDefault<FxHasher>, 1> {
3318        let mut ctx = self.execute_context(turbo_tasks);
3319        let mut collectibles = AutoMap::default();
3320        {
3321            let mut task = ctx.task(task_id, TaskDataCategory::All);
3322            task.assert_not_deleted("read_task_collectibles");
3323            if task
3324                .get_persistent_task_type()
3325                .is_some_and(|t| !t.native_fn.is_root)
3326            {
3327                drop(task);
3328                panic!(
3329                    "Reading collectibles of non-root task {} (reader: {}). The `root` attribute \
3330                     is missing on the task.",
3331                    self.debug_get_task_description(task_id),
3332                    reader_id.map_or_else(
3333                        || "unknown".to_string(),
3334                        |r| self.debug_get_task_description(r)
3335                    )
3336                );
3337            }
3338            for (collectible, count) in task.iter_aggregated_collectibles() {
3339                if *count > 0 && collectible.collectible_type == collectible_type {
3340                    *collectibles
3341                        .entry(RawVc::task_cell(
3342                            collectible.cell.task,
3343                            collectible.cell.cell,
3344                        ))
3345                        .or_insert(0) += 1;
3346                }
3347            }
3348            for (&collectible, &count) in task.iter_collectibles() {
3349                if collectible.collectible_type == collectible_type {
3350                    *collectibles
3351                        .entry(RawVc::task_cell(
3352                            collectible.cell.task,
3353                            collectible.cell.cell,
3354                        ))
3355                        .or_insert(0) += count;
3356                }
3357            }
3358            if let Some(reader_id) = reader_id {
3359                let _ = task.add_collectibles_dependents((collectible_type, reader_id));
3360            }
3361        }
3362        if let Some(reader_id) = reader_id {
3363            let mut reader = ctx.task(reader_id, TaskDataCategory::Data);
3364            let target = CollectiblesRef {
3365                task: task_id,
3366                collectible_type,
3367            };
3368            if !reader.remove_outdated_collectibles_dependencies(&target) {
3369                let _ = reader.add_collectibles_dependencies(target);
3370            }
3371        }
3372        collectibles
3373    }
3374
3375    fn emit_collectible(
3376        &self,
3377        collectible_type: TraitTypeId,
3378        collectible: RawVc,
3379        task_id: TaskId,
3380        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3381    ) {
3382        self.assert_valid_collectible(task_id, collectible);
3383
3384        let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3385            panic!("Collectibles need to be resolved");
3386        };
3387        let cell = CellRef {
3388            task: collectible_task,
3389            cell,
3390        };
3391        operation::UpdateCollectibleOperation::run(
3392            task_id,
3393            CollectibleRef {
3394                collectible_type,
3395                cell,
3396            },
3397            1,
3398            self.execute_context(turbo_tasks),
3399        );
3400    }
3401
3402    fn unemit_collectible(
3403        &self,
3404        collectible_type: TraitTypeId,
3405        collectible: RawVc,
3406        count: u32,
3407        task_id: TaskId,
3408        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3409    ) {
3410        self.assert_valid_collectible(task_id, collectible);
3411
3412        let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3413            panic!("Collectibles need to be resolved");
3414        };
3415        let cell = CellRef {
3416            task: collectible_task,
3417            cell,
3418        };
3419        operation::UpdateCollectibleOperation::run(
3420            task_id,
3421            CollectibleRef {
3422                collectible_type,
3423                cell,
3424            },
3425            -(i32::try_from(count).unwrap()),
3426            self.execute_context(turbo_tasks),
3427        );
3428    }
3429
3430    fn unemit_collectibles(
3431        &self,
3432        collectible_type: TraitTypeId,
3433        collectibles: &TaskCollectiblesMap,
3434        task_id: TaskId,
3435        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3436    ) {
3437        let updates: SmallVec<[_; 1]> = collectibles
3438            .iter()
3439            .filter(|&(_, &count)| count > 0)
3440            .map(|(&collectible, &count)| {
3441                self.assert_valid_collectible(task_id, collectible);
3442                let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3443                    panic!("Collectibles need to be resolved");
3444                };
3445                (
3446                    CollectibleRef {
3447                        collectible_type,
3448                        cell: CellRef {
3449                            task: collectible_task,
3450                            cell,
3451                        },
3452                    },
3453                    -count,
3454                )
3455            })
3456            .collect();
3457        operation::UpdateCollectibleOperation::run_batch(
3458            task_id,
3459            updates,
3460            self.execute_context(turbo_tasks),
3461        );
3462    }
3463
3464    fn update_task_cell(
3465        &self,
3466        task_id: TaskId,
3467        cell: CellId,
3468        content: CellContent,
3469        updated_key_hashes: Option<SmallVec<[u64; 2]>>,
3470        content_hash: Option<CellHash>,
3471        verification_mode: VerificationMode,
3472        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3473    ) {
3474        update_cell(
3475            task_id,
3476            cell,
3477            content,
3478            updated_key_hashes,
3479            content_hash,
3480            verification_mode,
3481            self.execute_context(turbo_tasks),
3482        );
3483    }
3484
3485    fn mark_own_task_as_finished(&self, task: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
3486        let mut ctx = self.execute_context(turbo_tasks);
3487        let mut task = ctx.task(task, TaskDataCategory::Data);
3488        if let Some(InProgressState::InProgress(InProgressStateInner {
3489            marked_as_completed,
3490            ..
3491        })) = task.get_in_progress_mut()
3492        {
3493            *marked_as_completed = true;
3494            // TODO this should remove the dirty state (also check session_dependent)
3495            // but this would break some assumptions for strongly consistent reads.
3496            // Client tasks are not connected yet, so we wouldn't wait for them.
3497            // Maybe that's ok in cases where mark_finished() is used? Seems like it?
3498        }
3499    }
3500
3501    fn connect_task(
3502        &self,
3503        task: TaskId,
3504        parent_task: Option<TaskId>,
3505        turbo_tasks: &TurboTasks<TurboTasksBackend>,
3506    ) {
3507        self.assert_not_persistent_calling_transient(parent_task, task);
3508        let mut ctx = self.execute_context(turbo_tasks);
3509        // An `OperationVc` held without a pin can name a collected task (soft-deleted or already
3510        // gone). That is a bug in whoever held it, so fail loudly here, in the connecting task,
3511        // rather than letting the dead id spread into this task's output or another task's
3512        // arguments. Panic before `connect_child`, which would materialize and schedule an entry
3513        // for the missing task.
3514        if ctx.try_task(task, TaskDataCategory::Meta).is_none() {
3515            panic!(
3516                "connect_task: task {task} no longer exists (it was garbage collected). An \
3517                 `OperationVc` to it was held without a pin; hold it in a `GcRoot` or connect it \
3518                 from its parent task."
3519            );
3520        }
3521        connect_child(
3522            parent_task,
3523            task,
3524            /* release_construction_ref */ false,
3525            ctx,
3526        );
3527    }
3528
3529    fn create_transient_task(&self, task_type: TransientTaskType) -> TaskId {
3530        let task_id = self.transient_task_id_factory.get();
3531        {
3532            let mut task = self.storage.access_mut(task_id);
3533            task.init_transient_task(task_id, task_type, self.should_track_activeness());
3534        }
3535        #[cfg(feature = "verify_aggregation_graph")]
3536        self.root_tasks.lock().insert(task_id);
3537        task_id
3538    }
3539
3540    fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
3541        // Once stopping, it is too late to tear down tasks safely. Holding the context returned
3542        // here also blocks `stop()` from tearing storage down while this runs -- this is called
3543        // from JS (`root_task_dispose`, or `SubscriptionTask::drop`) on a thread that
3544        // `stop_and_wait` does not drain.
3545        let Some(mut ctx) = self.try_execute_context(turbo_tasks) else {
3546            return;
3547        };
3548
3549        #[cfg(feature = "verify_aggregation_graph")]
3550        self.root_tasks.lock().remove(&task_id);
3551
3552        let mut task = ctx.task(task_id, TaskDataCategory::All);
3553        let is_dirty = task.is_dirty();
3554        let has_dirty_containers = task.has_dirty_containers();
3555        if is_dirty.is_some() || has_dirty_containers {
3556            if let Some(activeness_state) = task.get_activeness_mut() {
3557                // We will finish the task, but it would be removed after the task is done
3558                activeness_state.unset_root_type();
3559                activeness_state.set_active_until_clean();
3560            };
3561        } else {
3562            if let Some(activeness_state) = task.take_activeness() {
3563                // Technically nobody should be listening to this event, but just in case
3564                // we notify it anyway
3565                activeness_state.all_clean_event.notify(usize::MAX);
3566            }
3567            // Remove all the outgoing edges of this task.
3568            let old_edges = capture_all_edges(&task);
3569            drop(task);
3570
3571            if !old_edges.is_empty() {
3572                cleanup_old_edges(task_id, old_edges, AggregationUpdateQueue::new(), &mut ctx);
3573            }
3574        }
3575    }
3576
3577    #[cfg(feature = "verify_aggregation_graph")]
3578    fn verify_aggregation_graph(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>, idle: bool) {
3579        if env::var("TURBO_ENGINE_VERIFY_GRAPH").ok().as_deref() == Some("0") {
3580            return;
3581        }
3582        use std::{collections::VecDeque, env, io::stdout};
3583
3584        use crate::backend::operation::{get_uppers, is_aggregating_node};
3585
3586        let mut ctx = self.execute_context(turbo_tasks);
3587        let root_tasks = self.root_tasks.lock().clone();
3588
3589        for task_id in root_tasks.into_iter() {
3590            let mut queue = VecDeque::new();
3591            let mut visited = FxHashSet::default();
3592            let mut aggregated_nodes = FxHashSet::default();
3593            let mut collectibles = FxHashMap::default();
3594            let root_task_id = task_id;
3595            visited.insert(task_id);
3596            aggregated_nodes.insert(task_id);
3597            queue.push_back(task_id);
3598            let mut counter = 0;
3599            while let Some(task_id) = queue.pop_front() {
3600                counter += 1;
3601                if counter % 100000 == 0 {
3602                    println!(
3603                        "queue={}, visited={}, aggregated_nodes={}",
3604                        queue.len(),
3605                        visited.len(),
3606                        aggregated_nodes.len()
3607                    );
3608                }
3609                let task = ctx.task(task_id, TaskDataCategory::All);
3610                if idle && !self.is_idle.load(Ordering::Relaxed) {
3611                    return;
3612                }
3613
3614                let uppers = get_uppers(&task);
3615                if task_id != root_task_id
3616                    && !uppers.iter().any(|upper| aggregated_nodes.contains(upper))
3617                {
3618                    panic!(
3619                        "Task {} {} doesn't report to any root but is reachable from one (uppers: \
3620                         {:?})",
3621                        task_id,
3622                        task.get_task_description(),
3623                        uppers
3624                    );
3625                }
3626
3627                for (collectible, _) in task.iter_aggregated_collectibles() {
3628                    collectibles
3629                        .entry(*collectible)
3630                        .or_insert_with(|| (false, Vec::new()))
3631                        .1
3632                        .push(task_id);
3633                }
3634
3635                for (&collectible, &value) in task.iter_collectibles() {
3636                    if value > 0 {
3637                        if let Some((flag, _)) = collectibles.get_mut(&collectible) {
3638                            *flag = true
3639                        } else {
3640                            panic!(
3641                                "Task {} has a collectible {:?} that is not in any upper task",
3642                                task_id, collectible
3643                            );
3644                        }
3645                    }
3646                }
3647
3648                let is_dirty = task.has_dirty();
3649                let has_dirty_container = task.has_dirty_containers();
3650                let should_be_in_upper = is_dirty || has_dirty_container;
3651
3652                let aggregation_number = get_aggregation_number(&task);
3653                if is_aggregating_node(aggregation_number) {
3654                    aggregated_nodes.insert(task_id);
3655                }
3656                // println!(
3657                //     "{task_id}: {} agg_num = {aggregation_number}, uppers = {:#?}",
3658                //     ctx.get_task_description(task_id),
3659                //     uppers
3660                // );
3661
3662                for child_id in task.iter_children() {
3663                    // println!("{task_id}: child -> {child_id}");
3664                    if visited.insert(child_id) {
3665                        queue.push_back(child_id);
3666                    }
3667                }
3668                drop(task);
3669
3670                if should_be_in_upper {
3671                    for upper_id in uppers {
3672                        let upper = ctx.task(upper_id, TaskDataCategory::All);
3673                        let in_upper = upper
3674                            .get_aggregated_dirty_containers(&task_id)
3675                            .is_some_and(|&dirty| dirty > 0);
3676                        if !in_upper {
3677                            let containers: Vec<_> = upper
3678                                .iter_aggregated_dirty_containers()
3679                                .map(|(&k, &v)| (k, v))
3680                                .collect();
3681                            let upper_task_desc = upper.get_task_description();
3682                            drop(upper);
3683                            panic!(
3684                                "Task {} ({}) is dirty, but is not listed in the upper task {} \
3685                                 ({})\nThese dirty containers are present:\n{:#?}",
3686                                task_id,
3687                                ctx.task(task_id, TaskDataCategory::Data)
3688                                    .get_task_description(),
3689                                upper_id,
3690                                upper_task_desc,
3691                                containers,
3692                            );
3693                        }
3694                    }
3695                }
3696            }
3697
3698            for (collectible, (flag, task_ids)) in collectibles {
3699                if !flag {
3700                    use std::io::Write;
3701                    let mut stdout = stdout().lock();
3702                    writeln!(
3703                        stdout,
3704                        "{:?} that is not emitted in any child task but in these aggregated \
3705                         tasks: {:#?}",
3706                        collectible,
3707                        task_ids
3708                            .iter()
3709                            .map(|t| format!(
3710                                "{t} {}",
3711                                ctx.task(*t, TaskDataCategory::Data).get_task_description()
3712                            ))
3713                            .collect::<Vec<_>>()
3714                    )
3715                    .unwrap();
3716
3717                    let task_id = collectible.cell.task;
3718                    let mut queue = {
3719                        let task = ctx.task(task_id, TaskDataCategory::All);
3720                        get_uppers(&task)
3721                    };
3722                    let mut visited = FxHashSet::default();
3723                    for &upper_id in queue.iter() {
3724                        visited.insert(upper_id);
3725                        writeln!(stdout, "{task_id:?} -> {upper_id:?}").unwrap();
3726                    }
3727                    while let Some(task_id) = queue.pop() {
3728                        let task = ctx.task(task_id, TaskDataCategory::All);
3729                        let desc = task.get_task_description();
3730                        let aggregated_collectible = task
3731                            .get_aggregated_collectibles(&collectible)
3732                            .copied()
3733                            .unwrap_or_default();
3734                        let uppers = get_uppers(&task);
3735                        drop(task);
3736                        writeln!(
3737                            stdout,
3738                            "upper {task_id} {desc} collectible={aggregated_collectible}"
3739                        )
3740                        .unwrap();
3741                        if task_ids.contains(&task_id) {
3742                            writeln!(
3743                                stdout,
3744                                "Task has an upper connection to an aggregated task that doesn't \
3745                                 reference it. Upper connection is invalid!"
3746                            )
3747                            .unwrap();
3748                        }
3749                        for upper_id in uppers {
3750                            writeln!(stdout, "{task_id:?} -> {upper_id:?}").unwrap();
3751                            if !visited.contains(&upper_id) {
3752                                queue.push(upper_id);
3753                            }
3754                        }
3755                    }
3756                    panic!("See stdout for more details");
3757                }
3758            }
3759        }
3760    }
3761
3762    fn assert_not_persistent_calling_transient(&self, parent_id: Option<TaskId>, child_id: TaskId) {
3763        if let Some(parent_id) = parent_id
3764            && !parent_id.is_transient()
3765            && child_id.is_transient()
3766        {
3767            self.panic_persistent_calling_transient(
3768                self.debug_get_task_description(parent_id),
3769                self.debug_get_cached_task_type(child_id).as_deref(),
3770            );
3771        }
3772    }
3773
3774    fn panic_persistent_calling_transient(
3775        &self,
3776        parent: String,
3777        child: Option<&CachedTaskType>,
3778    ) -> ! {
3779        panic!(
3780            "Persistent task {} is not allowed to call, read, or connect to transient task {}.",
3781            parent,
3782            child.map_or("unknown", |t| t.get_name()),
3783        );
3784    }
3785
3786    fn assert_valid_collectible(&self, task_id: TaskId, collectible: RawVc) {
3787        // these checks occur in a potentially hot codepath, but they're cheap
3788        let Some((col_task_id, _)) = collectible.as_task_cell() else {
3789            // This should never happen: The collectible APIs use ResolvedVc
3790            let task_info = if let Some(col_task_ty) = collectible
3791                .try_get_task_id()
3792                .map(|t| self.debug_get_task_description(t))
3793            {
3794                Cow::Owned(format!(" (return type of {col_task_ty})"))
3795            } else {
3796                Cow::Borrowed("")
3797            };
3798            panic!("Collectible{task_info} must be a ResolvedVc")
3799        };
3800        if col_task_id.is_transient() && !task_id.is_transient() {
3801            // this should never happen: How would a persistent function get a transient Vc?
3802            panic!(
3803                "Collectible is transient; transient collectibles cannot be emitted from \
3804                 persistent tasks"
3805            )
3806        }
3807    }
3808}
3809
3810impl Backend for TurboTasksBackend {
3811    fn startup(&self, turbo_tasks: &TurboTasks<Self>) {
3812        self.startup(turbo_tasks);
3813    }
3814
3815    fn stopping(&self, _turbo_tasks: &TurboTasks<Self>) {
3816        self.stopping();
3817    }
3818
3819    fn stop(&self, turbo_tasks: &TurboTasks<Self>) {
3820        self.stop(turbo_tasks);
3821    }
3822
3823    fn idle_start(&self, turbo_tasks: &TurboTasks<Self>) {
3824        self.idle_start(turbo_tasks);
3825    }
3826
3827    fn idle_end(&self, _turbo_tasks: &TurboTasks<Self>) {
3828        self.idle_end();
3829    }
3830
3831    fn get_or_create_task(
3832        &self,
3833        native_fn: &'static NativeFunction,
3834        this: Option<RawVc>,
3835        arg: &mut dyn DynTaskInputsStorage,
3836        parent_task: Option<TaskId>,
3837        persistence: TaskPersistence,
3838        turbo_tasks: &TurboTasks<Self>,
3839    ) -> TaskId {
3840        self.get_or_create_task(native_fn, this, arg, parent_task, persistence, turbo_tasks)
3841    }
3842
3843    fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3844        self.invalidate_task(task_id, turbo_tasks);
3845    }
3846
3847    fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks<Self>) {
3848        self.invalidate_tasks(tasks, turbo_tasks);
3849    }
3850
3851    fn invalidate_tasks_set(
3852        &self,
3853        tasks: &AutoSet<TaskId, BuildHasherDefault<FxHasher>, 2>,
3854        turbo_tasks: &TurboTasks<Self>,
3855    ) {
3856        self.invalidate_tasks_set(tasks, turbo_tasks);
3857    }
3858
3859    fn invalidate_serialization(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3860        self.invalidate_serialization(task_id, turbo_tasks);
3861    }
3862
3863    fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
3864        self.task_execution_canceled(task, turbo_tasks)
3865    }
3866
3867    fn try_start_task_execution(
3868        &self,
3869        task_id: TaskId,
3870        priority: TaskPriority,
3871        turbo_tasks: &TurboTasks<Self>,
3872    ) -> Option<TaskExecutionSpec<'_>> {
3873        self.try_start_task_execution(task_id, priority, turbo_tasks)
3874    }
3875
3876    fn task_execution_completed(
3877        &self,
3878        task_id: TaskId,
3879        result: Result<RawVc, TurboTasksExecutionError>,
3880        cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
3881        #[cfg(feature = "verify_determinism")] stateful: bool,
3882        has_invalidator: bool,
3883        turbo_tasks: &TurboTasks<Self>,
3884    ) -> Option<TaskPriority> {
3885        self.task_execution_completed(
3886            task_id,
3887            result,
3888            cell_counters,
3889            #[cfg(feature = "verify_determinism")]
3890            stateful,
3891            has_invalidator,
3892            turbo_tasks,
3893        )
3894    }
3895
3896    type BackendJob = TurboTasksBackendJob;
3897
3898    fn run_backend_job<'a>(
3899        &'a self,
3900        job: Self::BackendJob,
3901        turbo_tasks: &'a TurboTasks<Self>,
3902    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
3903        self.run_backend_job(job, turbo_tasks)
3904    }
3905
3906    fn try_read_task_output(
3907        &self,
3908        task_id: TaskId,
3909        reader: Option<TaskId>,
3910        options: ReadOutputOptions,
3911        turbo_tasks: &TurboTasks<Self>,
3912    ) -> Result<ReadOutcome<RawVc>> {
3913        self.try_read_task_output(task_id, reader, options, turbo_tasks)
3914    }
3915
3916    fn try_read_task_cell(
3917        &self,
3918        task_id: TaskId,
3919        cell: CellId,
3920        reader: Option<TaskId>,
3921        options: ReadCellOptions,
3922        turbo_tasks: &TurboTasks<Self>,
3923    ) -> Result<ReadOutcome<TypedCellContent>> {
3924        self.try_read_task_cell(task_id, reader, cell, options, turbo_tasks)
3925    }
3926
3927    fn try_read_own_task_cell(
3928        &self,
3929        task_id: TaskId,
3930        cell: CellId,
3931        turbo_tasks: &TurboTasks<Self>,
3932    ) -> Result<TypedCellContent> {
3933        self.try_read_own_task_cell(task_id, cell, turbo_tasks)
3934    }
3935
3936    fn read_task_collectibles(
3937        &self,
3938        task_id: TaskId,
3939        collectible_type: TraitTypeId,
3940        reader: Option<TaskId>,
3941        turbo_tasks: &TurboTasks<Self>,
3942    ) -> AutoMap<RawVc, i32, BuildHasherDefault<FxHasher>, 1> {
3943        self.read_task_collectibles(task_id, collectible_type, reader, turbo_tasks)
3944    }
3945
3946    fn emit_collectible(
3947        &self,
3948        collectible_type: TraitTypeId,
3949        collectible: RawVc,
3950        task_id: TaskId,
3951        turbo_tasks: &TurboTasks<Self>,
3952    ) {
3953        self.emit_collectible(collectible_type, collectible, task_id, turbo_tasks)
3954    }
3955
3956    fn unemit_collectible(
3957        &self,
3958        collectible_type: TraitTypeId,
3959        collectible: RawVc,
3960        count: u32,
3961        task_id: TaskId,
3962        turbo_tasks: &TurboTasks<Self>,
3963    ) {
3964        self.unemit_collectible(collectible_type, collectible, count, task_id, turbo_tasks)
3965    }
3966
3967    fn unemit_collectibles(
3968        &self,
3969        collectible_type: TraitTypeId,
3970        collectibles: &TaskCollectiblesMap,
3971        task_id: TaskId,
3972        turbo_tasks: &TurboTasks<Self>,
3973    ) {
3974        self.unemit_collectibles(collectible_type, collectibles, task_id, turbo_tasks)
3975    }
3976
3977    fn update_task_cell(
3978        &self,
3979        task_id: TaskId,
3980        cell: CellId,
3981        content: CellContent,
3982        updated_key_hashes: Option<SmallVec<[u64; 2]>>,
3983        content_hash: Option<CellHash>,
3984        verification_mode: VerificationMode,
3985        turbo_tasks: &TurboTasks<Self>,
3986    ) {
3987        self.update_task_cell(
3988            task_id,
3989            cell,
3990            content,
3991            updated_key_hashes,
3992            content_hash,
3993            verification_mode,
3994            turbo_tasks,
3995        );
3996    }
3997
3998    fn mark_own_task_as_finished(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3999        self.mark_own_task_as_finished(task_id, turbo_tasks);
4000    }
4001
4002    fn pin_task_for_gc(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
4003        self.pin_task_for_gc(task, turbo_tasks);
4004    }
4005
4006    fn unpin_task_for_gc(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
4007        self.unpin_task_for_gc(task, turbo_tasks);
4008    }
4009
4010    fn connect_task(
4011        &self,
4012        task: TaskId,
4013        parent_task: Option<TaskId>,
4014        turbo_tasks: &TurboTasks<Self>,
4015    ) {
4016        self.connect_task(task, parent_task, turbo_tasks);
4017    }
4018
4019    fn create_transient_task(
4020        &self,
4021        task_type: TransientTaskType,
4022        _turbo_tasks: &TurboTasks<Self>,
4023    ) -> TaskId {
4024        self.create_transient_task(task_type)
4025    }
4026
4027    fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
4028        self.dispose_root_task(task_id, turbo_tasks);
4029    }
4030
4031    fn task_statistics(&self) -> &TaskStatisticsApi {
4032        &self.task_statistics
4033    }
4034
4035    fn is_tracking_dependencies(&self) -> bool {
4036        self.options.dependency_tracking
4037    }
4038
4039    fn get_task_name(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) -> String {
4040        self.get_task_name(task, turbo_tasks)
4041    }
4042}
4043
4044// from https://github.com/tokio-rs/tokio/blob/29cd6ec1ec6f90a7ee1ad641c03e0e00badbcb0e/tokio/src/time/instant.rs#L57-L63
4045fn far_future() -> Instant {
4046    // Roughly 30 years from now.
4047    // API does not provide a way to obtain max `Instant`
4048    // or convert specific date in the future to instant.
4049    // 1000 years overflows on macOS, 100 years overflows on FreeBSD.
4050    Instant::now() + Duration::from_secs(86400 * 365 * 30)
4051}
4052
4053#[cfg(test)]
4054mod persist_failure_tests {
4055    use turbo_tasks::{TaskId, TurboTasks};
4056
4057    use crate::{
4058        backend::{
4059            BackendOptions, LastPersistenceStatus, SnapshotReason, StorageMode, TurboTasksBackend,
4060            storage::SpecificTaskDataCategory,
4061        },
4062        noop_backing_storage,
4063    };
4064
4065    /// A failed persist (the noop backing storage rejects writes) disables persisting for the
4066    /// session: a later snapshot doesn't start, so it leaves the task's modification as it is.
4067    #[tokio::test(flavor = "multi_thread")]
4068    async fn failed_persist_disables_persisting() {
4069        let tt = TurboTasks::new(TurboTasksBackend::new(
4070            BackendOptions {
4071                storage_mode: Some(StorageMode::ReadWrite),
4072                num_workers: Some(1),
4073                small_preallocation: true,
4074                ..Default::default()
4075            },
4076            noop_backing_storage(),
4077        ));
4078        let backend = tt.backend();
4079        let modify = |id: u32| {
4080            let task_id = TaskId::new(id).unwrap();
4081            let mut task = backend.storage.access_mut(task_id);
4082            let _ = task.track_modification(SpecificTaskDataCategory::Data, "test");
4083            task_id
4084        };
4085
4086        let status = || *backend.snapshot_in_progress.lock();
4087
4088        modify(1);
4089        assert_eq!(status(), LastPersistenceStatus::Ok);
4090        let _ = backend.snapshot_and_evict_for_testing(&tt);
4091        assert_eq!(status(), LastPersistenceStatus::Failed);
4092
4093        let task_id = modify(2);
4094        let result = backend
4095            .snapshot_and_persist(None, SnapshotReason::Test, &tt)
4096            .expect("a disabled persist is skipped, not an error");
4097        assert!(result.is_none(), "persisting must stay disabled");
4098        assert_eq!(status(), LastPersistenceStatus::Failed);
4099        let task = backend.storage.access_mut(task_id);
4100        assert!(
4101            task.flags.data_modified(),
4102            "the second snapshot must not start"
4103        );
4104        assert!(!task.flags.any_snapshot_pending());
4105    }
4106}