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