Skip to main content

turbo_tasks_backend/backend/
mod.rs

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