1mod cell_data;
2mod counter_map;
3mod eviction;
4mod gc;
5mod operation;
6mod snapshot_coordinator;
7mod storage;
8pub mod storage_schema;
9
10#[cfg(feature = "verify_aggregation_graph")]
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::{
15 borrow::Cow,
16 fmt::Write,
17 future::Future,
18 hash::BuildHasherDefault,
19 mem::take,
20 pin::Pin,
21 sync::{Arc, LazyLock},
22 time::SystemTime,
23};
24
25use anyhow::{Result, bail};
26use auto_hash_map::{AutoMap, AutoSet};
27use gc::DEFAULT_GC_ROOT_TTL;
28pub use gc::{GcPassResult, GcStats, TtlCounter};
29use hashbrown::hash_table::Entry;
30use parking_lot::{Mutex, RwLock};
31use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
32use smallvec::SmallVec;
33use tokio::time::{Duration, Instant};
34use tracing::{Span, field::display, trace_span};
35use turbo_bincode::TurboBincodeBuffer;
36use turbo_tasks::{
37 CellId, DynTaskInputsStorage, RawVc, RawVcUnpacked, ReadCellOptions, ReadCellTracking,
38 ReadConsistency, ReadOutcome, ReadOutputOptions, ReadTracking, SharedReference,
39 TRANSIENT_TASK_BIT, TaskExecutionReason, TaskId, TaskPersistence, TaskPriority, TraitTypeId,
40 TurboTasks, TurboTasksCallApi, TurboTasksPanic, ValueTypeId,
41 backend::{
42 Backend, CachedTaskType, CachedTaskTypeArc, CellContent, CellHash, TaskCollectiblesMap,
43 TaskExecutionSpec, TransientTaskType, TurboTaskContextError, TurboTaskLocalContextError,
44 TurboTasksError, TurboTasksExecutionError, TurboTasksExecutionErrorMessage,
45 TypedCellContent, VerificationMode,
46 },
47 event::{Event, EventDescription, EventListener},
48 macro_helpers::NativeFunction,
49 message_queue::{DiagnosticEvent, Severity, TimingEvent, TraceEvent},
50 parallel,
51 registry::get_value_type,
52 scope_bounded::scope_bounded,
53 task_statistics::TaskStatisticsApi,
54 util::{IdFactoryWithReuse, good_chunk_size, into_chunks},
55};
56#[cfg(feature = "task_dirty_cause")]
57use turbo_tasks::{FunctionId, TaskDirtyCause};
58use turbo_tasks_malloc::TurboMalloc;
59
60use self::eviction::EvictionControl;
61#[cfg(test)]
62pub(crate) use self::storage::TrackOutcome;
63pub use self::{
64 eviction::EvictionMode,
65 storage::{EvictionCounts, SpecificTaskDataCategory, TaskDataCategory},
66};
67use crate::{
68 backend::{
69 operation::{
70 AggregationUpdateJob, AggregationUpdateQueue, ExecuteContext, LeafDistanceUpdateQueue,
71 OutdatedEdge, TaskGuard, TaskType, TaskTypeRef, capture_all_edges, cleanup_old_edges,
72 connect_child, connect_children, get_aggregation_number, get_uppers, invalidate,
73 make_task_dirty_internal, prepare_new_children, update_cell,
74 },
75 snapshot_coordinator::{OperationGuard, SlowSettle, SnapshotCoordinator},
76 storage::{Storage, StorageOptions, encode_snapshot_item},
77 storage_schema::{TaskStorage, TaskStorageAccessors},
78 },
79 backing_storage::SnapshotItem,
80 data::{
81 ActivenessState, CellRef, CollectibleRef, CollectiblesRef, Dirtyness, InProgressCellState,
82 InProgressState, InProgressStateInner, OutputValue, TransientTask,
83 },
84 error::{TaskError, TaskErrorItem},
85 kv_backing_storage::TurboBackingStorage,
86 utils::{
87 dash_map_entry::{get_in_shard, get_shard, with_entry_in_shard},
88 shard_amount::compute_shard_amount,
89 stopwatch::Stopwatch,
90 },
91};
92
93const DEPENDENT_TASKS_DIRTY_PARALLELIZATION_THRESHOLD: usize = 10000;
97
98const GC_MIN_PROGRESS: Duration = Duration::from_millis(100);
102
103fn compute_stale_priority(task: &TaskGuard<'_>) -> TaskPriority {
110 TaskPriority::invalidation(
111 task.get_leaf_distance()
112 .copied()
113 .unwrap_or_default()
114 .distance,
115 )
116 .in_parent(task.is_dirty().unwrap_or(TaskPriority::leaf()))
117}
118
119#[derive(PartialEq, Eq)]
120pub enum StorageMode {
121 ReadOnly,
123 ReadWrite,
126 ReadWriteOnShutdown,
129}
130
131pub struct BackendOptions {
132 pub dependency_tracking: bool,
137
138 pub active_tracking: bool,
144
145 pub storage_mode: Option<StorageMode>,
147
148 pub num_workers: Option<usize>,
151
152 pub small_preallocation: bool,
154
155 pub eviction_mode: EvictionMode,
158
159 pub gc: Option<bool>,
163
164 pub gc_root_ttl: Option<Duration>,
167
168 pub gc_min_progress: Option<Duration>,
171}
172
173impl Default for BackendOptions {
174 fn default() -> Self {
175 Self {
176 dependency_tracking: true,
177 active_tracking: true,
178 storage_mode: Some(StorageMode::ReadWrite),
179 num_workers: None,
180 small_preallocation: false,
181 eviction_mode: EvictionMode::Off,
182 gc: None,
183 gc_root_ttl: None,
184 gc_min_progress: None,
185 }
186 }
187}
188
189pub enum TurboTasksBackendJob {
190 Snapshot,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195enum SnapshotReason {
196 Test,
197 Stop,
198 InitialSnapshotTimeout,
199 RegularSnapshotInterval,
200 IdleTimeout,
201}
202
203impl SnapshotReason {
204 fn as_str(self) -> &'static str {
205 match self {
206 SnapshotReason::Test => "test",
207 SnapshotReason::Stop => "stop",
208 SnapshotReason::InitialSnapshotTimeout => "initial snapshot timeout",
209 SnapshotReason::RegularSnapshotInterval => "regular snapshot interval",
210 SnapshotReason::IdleTimeout => "idle timeout",
211 }
212 }
213
214 fn waits_for_operations(self) -> bool {
217 !matches!(self, SnapshotReason::IdleTimeout)
220 }
221
222 fn gc_is_interruptible(self) -> bool {
224 matches!(self, SnapshotReason::IdleTimeout | SnapshotReason::Test)
225 }
226
227 fn drain_entries(self) -> bool {
231 matches!(self, SnapshotReason::Stop)
232 }
233}
234
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241enum LastPersistenceStatus {
242 Ok,
243 Failed,
244}
245
246pub struct TurboTasksBackend {
247 options: BackendOptions,
248
249 start_time: Instant,
250
251 persisted_task_id_factory: IdFactoryWithReuse<TaskId>,
252 transient_task_id_factory: IdFactoryWithReuse<TaskId>,
253
254 storage: Storage,
255
256 snapshot_coord: SnapshotCoordinator,
259 snapshot_in_progress: Mutex<LastPersistenceStatus>,
266
267 gc_enabled: bool,
269
270 stopping: RwLock<bool>,
271 stopping_event: Event,
272 idle_start_event: Event,
273 idle_end_event: Event,
274 #[cfg(feature = "verify_aggregation_graph")]
275 is_idle: AtomicBool,
276
277 task_statistics: TaskStatisticsApi,
278
279 backing_storage: TurboBackingStorage,
280 gc_root_ttl: Duration,
282
283 gc_min_progress: Duration,
285
286 #[cfg(feature = "verify_aggregation_graph")]
287 root_tasks: Mutex<FxHashSet<TaskId>>,
288}
289
290#[doc(hidden)]
292#[derive(Default)]
293pub struct TestSnapshotOutcome {
294 pub had_new_data: bool,
296 pub eviction_counts: EvictionCounts,
298 pub gc: Option<(GcStats, GcPassResult)>,
301}
302
303impl TestSnapshotOutcome {
304 pub fn gc_stats(&self) -> &GcStats {
306 &self
307 .gc
308 .as_ref()
309 .expect("no GC pass ran: the backend needs `BackendOptions::gc = Some(true)`")
310 .0
311 }
312
313 pub fn gc_interrupted(&self) -> bool {
315 self.gc
316 .as_ref()
317 .is_some_and(|(_, result)| result.interrupted)
318 }
319}
320
321impl TurboTasksBackend {
322 pub fn invalidate_storage(&self, reason_code: &str) -> Result<()> {
328 self.backing_storage.invalidate(reason_code)
329 }
330
331 pub fn new(mut options: BackendOptions, backing_storage: TurboBackingStorage) -> Self {
332 let shard_amount = compute_shard_amount(options.num_workers, options.small_preallocation);
333 if !options.dependency_tracking {
334 options.active_tracking = false;
335 }
336 let small_preallocation = options.small_preallocation;
337 let gc_root_ttl = options.gc_root_ttl.unwrap_or(DEFAULT_GC_ROOT_TTL);
338 let gc_min_progress = options.gc_min_progress.unwrap_or(GC_MIN_PROGRESS);
339 let next_task_id = backing_storage
340 .next_free_task_id()
341 .expect("Failed to get task id");
342
343 let mut gc_enabled = options.gc.unwrap_or(false);
344 if gc_enabled
345 && options.storage_mode == Some(StorageMode::ReadWrite)
346 && options.eviction_mode == EvictionMode::Off
347 {
348 eprintln!(
349 "warning: GC is enabled but eviction is disabled; GC would leave collected tasks \
350 resident forever. Forcing GC off. Enable eviction ('auto'/'full') to use GC in \
351 this mode."
352 );
353 gc_enabled = false;
354 }
355
356 Self {
357 options,
358 gc_enabled,
359 start_time: Instant::now(),
360 persisted_task_id_factory: IdFactoryWithReuse::new(
361 next_task_id,
362 TaskId::try_from(TRANSIENT_TASK_BIT - 1).unwrap(),
363 ),
364 transient_task_id_factory: IdFactoryWithReuse::new(
365 TaskId::try_from(TRANSIENT_TASK_BIT).unwrap(),
366 TaskId::MAX,
367 ),
368 storage: Storage::new(StorageOptions {
369 shard_amount,
370 small_preallocation,
371 }),
372 snapshot_coord: SnapshotCoordinator::new(),
373 snapshot_in_progress: Mutex::new(LastPersistenceStatus::Ok),
374 stopping: RwLock::new(false),
375 stopping_event: Event::new(|| || "TurboTasksBackend::stopping_event".to_string()),
376 idle_start_event: Event::new(|| || "TurboTasksBackend::idle_start_event".to_string()),
377 idle_end_event: Event::new(|| || "TurboTasksBackend::idle_end_event".to_string()),
378 #[cfg(feature = "verify_aggregation_graph")]
379 is_idle: AtomicBool::new(false),
380 task_statistics: TaskStatisticsApi::default(),
381 backing_storage,
382 gc_root_ttl,
383 gc_min_progress,
384 #[cfg(feature = "verify_aggregation_graph")]
385 root_tasks: Default::default(),
386 }
387 }
388
389 fn execute_context<'a>(
390 &'a self,
391 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
392 ) -> ExecuteContext<'a> {
393 ExecuteContext::new(self, turbo_tasks)
394 }
395
396 fn execute_context_with_priority<'a>(
400 &'a self,
401 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
402 current_task_priority: TaskPriority,
403 ) -> ExecuteContext<'a> {
404 ExecuteContext::new(self, turbo_tasks).with_current_task_priority(current_task_priority)
405 }
406
407 fn try_execute_context<'a>(
415 &'a self,
416 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
417 ) -> Option<ExecuteContext<'a>> {
418 let stopping = self.stopping.read();
419 if *stopping {
420 return None;
421 }
422 Some(ExecuteContext::new_with_shutdown_guard(
423 self,
424 turbo_tasks,
425 stopping,
426 ))
427 }
428
429 pub(crate) fn start_operation(&self) -> Option<OperationGuard<'_>> {
430 if !self.should_persist() {
431 return None;
432 }
433 Some(self.snapshot_coord.begin_operation())
434 }
435
436 fn should_persist(&self) -> bool {
437 matches!(
438 self.options.storage_mode,
439 Some(StorageMode::ReadWrite) | Some(StorageMode::ReadWriteOnShutdown)
440 )
441 }
442
443 #[doc(hidden)]
448 pub fn snapshot_and_evict_for_testing(
449 &self,
450 turbo_tasks: &TurboTasks<TurboTasksBackend>,
451 ) -> TestSnapshotOutcome {
452 assert!(
453 self.should_persist(),
454 "snapshot_and_evict requires persistence"
455 );
456 let snapshot_result = self.snapshot_and_persist(None, SnapshotReason::Test, turbo_tasks);
457 let (had_new_data, gc_outcome) = match snapshot_result {
458 Ok(Some((_, new_data, gc_outcome))) => (new_data, gc_outcome),
459 Ok(None) | Err(_) => {
462 return TestSnapshotOutcome::default();
466 }
467 };
468 let eviction_counts = self.storage.evict_after_snapshot(None);
469 TestSnapshotOutcome {
470 had_new_data,
471 eviction_counts,
472 gc: gc_outcome,
473 }
474 }
475
476 #[doc(hidden)]
478 pub fn resident_task_count_for_testing(&self) -> usize {
479 self.storage.resident_task_count_for_testing()
480 }
481
482 #[doc(hidden)]
485 pub fn dirty_parent_priority_for_testing(
486 &self,
487 task: TaskId,
488 turbo_tasks: &TurboTasks<TurboTasksBackend>,
489 ) -> Option<TaskPriority> {
490 let mut ctx = self.execute_context(turbo_tasks);
491 let task = ctx.try_task(task, TaskDataCategory::All)?;
492 match task.get_dirty() {
493 Some(Dirtyness::Dirty {
494 parent_priority, ..
495 }) => Some(*parent_priority),
496 _ => None,
497 }
498 }
499
500 #[doc(hidden)]
503 pub fn parent_count_for_testing(&self, task: TaskId) -> u32 {
504 self.storage
505 .with_task(task, |t| t.gc_parent_count())
506 .unwrap_or(0)
507 }
508
509 #[doc(hidden)]
511 pub fn transient_ref_count_for_testing(&self, task: TaskId) -> u32 {
512 self.storage
513 .with_task(task, |t| t.gc_transient_ref_count())
514 .unwrap_or(0)
515 }
516
517 #[doc(hidden)]
519 pub fn persisted_gc_roots_for_testing(&self) -> Vec<(TaskId, TtlCounter)> {
520 self.backing_storage.roots().unwrap_or_default()
521 }
522 #[doc(hidden)]
526 pub fn assert_task_exists_for_testing(
527 &self,
528 task: TaskId,
529 turbo_tasks: &TurboTasks<TurboTasksBackend>,
530 ) {
531 let mut ctx = self.execute_context(turbo_tasks);
532 let _ = ctx.task(task, TaskDataCategory::All);
533 }
534
535 fn should_restore(&self) -> bool {
536 self.options.storage_mode.is_some()
537 }
538
539 fn should_track_dependencies(&self) -> bool {
540 self.options.dependency_tracking
541 }
542
543 fn should_track_activeness(&self) -> bool {
544 self.options.active_tracking
545 }
546
547 fn track_cache_hit_by_fn(&self, native_fn: &'static NativeFunction) {
548 self.task_statistics
549 .map(|stats| stats.increment_cache_hit(native_fn));
550 }
551
552 fn track_cache_miss_by_fn(&self, native_fn: &'static NativeFunction) {
553 self.task_statistics
554 .map(|stats| stats.increment_cache_miss(native_fn));
555 }
556
557 fn task_error_to_turbo_tasks_execution_error(
562 &self,
563 error: &TaskError,
564 ctx: &mut ExecuteContext<'_>,
565 ) -> TurboTasksExecutionError {
566 match error {
567 TaskError::Panic(panic) => TurboTasksExecutionError::Panic(panic.clone()),
568 TaskError::Error(item) => TurboTasksExecutionError::Error(Arc::new(TurboTasksError {
569 message: item.message.clone(),
570 source: item
571 .source
572 .as_ref()
573 .map(|e| self.task_error_to_turbo_tasks_execution_error(e, ctx)),
574 })),
575 TaskError::LocalTaskContext(local_task_context) => {
576 TurboTasksExecutionError::LocalTaskContext(Arc::new(TurboTaskLocalContextError {
577 name: local_task_context.name.clone(),
578 source: local_task_context
579 .source
580 .as_ref()
581 .map(|e| self.task_error_to_turbo_tasks_execution_error(e, ctx)),
582 }))
583 }
584 TaskError::TaskChain(chain) => {
585 let task_id = chain.last().unwrap();
586 let error = {
587 let task = ctx.task(*task_id, TaskDataCategory::Meta);
588 if let Some(OutputValue::Error(error)) = task.get_output() {
589 Some(error.clone())
590 } else {
591 None
592 }
593 };
594 let error = error.map_or_else(
595 || {
596 TurboTasksExecutionError::Panic(Arc::new(TurboTasksPanic {
598 message: TurboTasksExecutionErrorMessage::PIISafe(Cow::Borrowed(
599 "Error no longer available",
600 )),
601 location: None,
602 }))
603 },
604 |e| self.task_error_to_turbo_tasks_execution_error(&e, ctx),
605 );
606 let mut current_error = error;
607 for &task_id in chain.iter().rev() {
608 current_error =
609 TurboTasksExecutionError::TaskContext(Arc::new(TurboTaskContextError {
610 task_id,
611 source: Some(current_error),
612 turbo_tasks: ctx.turbo_tasks(),
613 }));
614 }
615 current_error
616 }
617 }
618 }
619}
620
621struct TaskExecutionCompletePrepareResult {
623 pub new_children: FxHashSet<TaskId>,
624 pub is_now_immutable: bool,
625 #[cfg(feature = "verify_determinism")]
626 pub no_output_set: bool,
627 #[cfg(feature = "task_dirty_cause")]
628 pub function_id: Option<FunctionId>,
629 pub new_output: Option<OutputValue>,
630 pub output_dependent_tasks: SmallVec<[TaskId; 4]>,
631 pub is_recomputation: bool,
632 pub is_session_dependent: bool,
633}
634
635fn lock_task_and_optional_reader<'e>(
639 ctx: &mut ExecuteContext<'e>,
640 task_id: TaskId,
641 reader_id: Option<TaskId>,
642) -> Option<(TaskGuard<'e>, Option<TaskGuard<'e>>)> {
643 let task = ctx.try_task(task_id, TaskDataCategory::All)?;
644 let Some(reader_id) = reader_id else {
645 return Some((task, None));
646 };
647
648 if task.immutable() && !cfg!(feature = "verify_immutable") {
652 Some((task, None))
653 } else {
654 drop(task);
655
656 let (task, reader) = ctx.task_pair(task_id, reader_id, TaskDataCategory::All);
660 if task.immutable() && !cfg!(feature = "verify_immutable") {
664 drop(reader);
665 Some((task, None))
666 } else {
667 Some((task, Some(reader)))
668 }
669 }
670}
671
672fn collected_task_read_error(task_id: TaskId, operation: &str) -> anyhow::Error {
674 anyhow::anyhow!(
675 "{operation}: task {task_id} no longer exists (it was garbage collected). The reading \
676 task holds a stale reference to it and is expected to be dropped by its parent."
677 )
678}
679
680impl TurboTasksBackend {
682 fn try_read_task_output(
683 &self,
684 task_id: TaskId,
685 reader: Option<TaskId>,
686 options: ReadOutputOptions,
687 turbo_tasks: &TurboTasks<TurboTasksBackend>,
688 ) -> Result<ReadOutcome<RawVc>> {
689 self.assert_not_persistent_calling_transient(reader, task_id);
690
691 let mut ctx = self.execute_context(turbo_tasks);
692 let need_reader_task = reader.and_then(|reader_id| {
693 (self.should_track_dependencies()
694 && !matches!(options.tracking, ReadTracking::Untracked)
695 && reader_id != task_id)
696 .then_some(reader_id)
697 });
698 let Some((mut task, mut reader_task)) =
699 lock_task_and_optional_reader(&mut ctx, task_id, need_reader_task)
700 else {
701 return Err(collected_task_read_error(task_id, "read_task_output"));
702 };
703
704 fn listen_to_done_event(
705 reader_description: Option<EventDescription>,
706 tracking: ReadTracking,
707 done_event: &Event,
708 ) -> EventListener {
709 done_event.listen_with_note(move || {
710 move || {
711 if let Some(reader_description) = reader_description.as_ref() {
712 format!(
713 "try_read_task_output from {} ({})",
714 reader_description, tracking
715 )
716 } else {
717 format!("try_read_task_output ({})", tracking)
718 }
719 }
720 })
721 }
722
723 fn check_in_progress<T>(
727 task: &TaskGuard<'_>,
728 reader_description: Option<EventDescription>,
729 tracking: ReadTracking,
730 ) -> Option<Result<ReadOutcome<T>>> {
731 match task.get_in_progress() {
732 Some(InProgressState::Scheduled { done_event, .. }) => {
733 Some(Ok(ReadOutcome::Scheduled(listen_to_done_event(
734 reader_description,
735 tracking,
736 done_event,
737 ))))
738 }
739 Some(InProgressState::InProgress(InProgressStateInner { done_event, .. })) => {
740 Some(Ok(ReadOutcome::InProgress(listen_to_done_event(
741 reader_description,
742 tracking,
743 done_event,
744 ))))
745 }
746 Some(InProgressState::Canceled) => Some(Err(anyhow::anyhow!(
747 "{} was canceled",
748 task.get_task_description()
749 ))),
750 None => None,
751 }
752 }
753
754 if matches!(options.consistency, ReadConsistency::Strong) {
755 if task
756 .get_persistent_task_type()
757 .is_some_and(|t| !t.native_fn.is_root)
758 {
759 drop(task);
760 drop(reader_task);
761 panic!(
762 "Strongly consistent read of non-root task {} (reader: {}). The `root` \
763 attribute is missing on the task.",
764 self.debug_get_task_description(task_id),
765 reader.map_or_else(
766 || "unknown".to_string(),
767 |r| self.debug_get_task_description(r)
768 )
769 );
770 }
771
772 let is_dirty = task.is_dirty();
773
774 let has_dirty_containers = task.has_dirty_containers();
776 if has_dirty_containers || is_dirty.is_some() {
777 let activeness = task.get_activeness_mut();
778 let mut task_ids_to_schedule: Vec<_> = Vec::new();
779 let activeness = if let Some(activeness) = activeness {
781 activeness.set_active_until_clean();
785 activeness
786 } else {
787 if ctx.should_track_activeness() {
791 task_ids_to_schedule = task.dirty_containers().collect();
793 task_ids_to_schedule.push(task_id);
794 }
795 let activeness =
796 task.get_activeness_mut_or_insert_with(|| ActivenessState::new(task_id));
797 activeness.set_active_until_clean();
798 activeness
799 };
800 let listener = activeness.all_clean_event.listen_with_note(move || {
801 let tt = turbo_tasks.pin();
804 move || {
805 let mut ctx = tt.backend().execute_context(&tt);
806 let mut visited = FxHashSet::default();
807 fn indent(s: &str) -> String {
808 s.split_inclusive('\n')
809 .flat_map(|line: &str| [" ", line].into_iter())
810 .collect::<String>()
811 }
812 fn get_info(
813 ctx: &mut ExecuteContext<'_>,
814 task_id: TaskId,
815 parent_and_count: Option<(TaskId, i32)>,
816 visited: &mut FxHashSet<TaskId>,
817 ) -> String {
818 let task = ctx.task(task_id, TaskDataCategory::All);
819 let is_dirty = task.is_dirty();
820 let in_progress =
821 task.get_in_progress()
822 .map_or("not in progress", |p| match p {
823 InProgressState::InProgress(_) => "in progress",
824 InProgressState::Scheduled { .. } => "scheduled",
825 InProgressState::Canceled => "canceled",
826 });
827 let activeness = task.get_activeness().map_or_else(
828 || "not active".to_string(),
829 |activeness| format!("{activeness:?}"),
830 );
831 let aggregation_number = get_aggregation_number(&task);
832 let missing_upper = if let Some((parent_task_id, _)) = parent_and_count
833 {
834 let uppers = get_uppers(&task);
835 !uppers.contains(&parent_task_id)
836 } else {
837 false
838 };
839
840 let has_dirty_containers = task.has_dirty_containers();
842
843 let task_description = task.get_task_description();
844 let is_dirty_label = if let Some(parent_priority) = is_dirty {
845 format!(", dirty({parent_priority})")
846 } else {
847 String::new()
848 };
849 let has_dirty_containers_label = if has_dirty_containers {
850 ", dirty containers"
851 } else {
852 ""
853 };
854 let count = if let Some((_, count)) = parent_and_count {
855 format!(" {count}")
856 } else {
857 String::new()
858 };
859 let mut info = format!(
860 "{task_id} {task_description}{count} (aggr={aggregation_number}, \
861 {in_progress}, \
862 {activeness}{is_dirty_label}{has_dirty_containers_label})",
863 );
864 let children: Vec<_> = task.dirty_containers_with_count().collect();
865 drop(task);
866
867 if missing_upper {
868 info.push_str("\n ERROR: missing upper connection");
869 }
870
871 if has_dirty_containers || !children.is_empty() {
872 writeln!(info, "\n dirty tasks:").unwrap();
873
874 for (child_task_id, count) in children {
875 let task_description = ctx
876 .task(child_task_id, TaskDataCategory::Data)
877 .get_task_description();
878 if visited.insert(child_task_id) {
879 let child_info = get_info(
880 ctx,
881 child_task_id,
882 Some((task_id, count)),
883 visited,
884 );
885 info.push_str(&indent(&child_info));
886 if !info.ends_with('\n') {
887 info.push('\n');
888 }
889 } else {
890 writeln!(
891 info,
892 " {child_task_id} {task_description} {count} \
893 (already visited)"
894 )
895 .unwrap();
896 }
897 }
898 }
899 info
900 }
901 let info = get_info(&mut ctx, task_id, None, &mut visited);
902 format!(
903 "try_read_task_output (strongly consistent) from {reader:?}\n{info}"
904 )
905 }
906 });
907 drop(reader_task);
908 drop(task);
909 if !task_ids_to_schedule.is_empty() {
910 let mut queue = AggregationUpdateQueue::new();
911 queue.extend_find_and_schedule_dirty(task_ids_to_schedule);
912 queue.execute(&mut ctx);
913 }
914
915 return Ok(ReadOutcome::InProgress(listener));
916 }
917 }
918
919 let reader_description = reader_task
920 .as_ref()
921 .map(|r| EventDescription::new(|| r.get_task_desc_fn()))
922 .or_else(|| {
923 need_reader_task.map(|reader_id| {
924 EventDescription::new(move || move || format!("{reader_id:?}"))
925 })
926 });
927 if let Some(value) = check_in_progress(&task, reader_description.clone(), options.tracking)
928 {
929 return value;
930 }
931
932 if let Some(output) = task.get_output() {
933 let result = match output {
934 OutputValue::Cell(cell) => Ok(RawVc::task_cell(cell.task, cell.cell)),
935 OutputValue::Output(task) => Ok(RawVc::task_output(*task)),
936 OutputValue::Error(error) => Err(error.clone()),
937 };
938 if let Some(mut reader_task) = reader_task.take()
939 && options.tracking.should_track(result.is_err())
940 {
941 #[cfg(feature = "trace_task_output_dependencies")]
942 let _span = tracing::trace_span!(
943 "add output dependency",
944 task = %task_id,
945 dependent_task = ?reader
946 )
947 .entered();
948 let mut queue = LeafDistanceUpdateQueue::new();
949 let reader = reader.unwrap();
950 if task.add_output_dependent(reader) {
951 let leaf_distance = task.get_leaf_distance().copied().unwrap_or_default();
953 let reader_leaf_distance =
954 reader_task.get_leaf_distance().copied().unwrap_or_default();
955 if reader_leaf_distance.distance <= leaf_distance.distance {
956 queue.push(
957 reader,
958 leaf_distance.distance,
959 leaf_distance.max_distance_in_buffer,
960 );
961 }
962 }
963
964 drop(task);
965
966 if !reader_task.remove_outdated_output_dependencies(&task_id) {
972 let _ = reader_task.add_output_dependencies(task_id);
973 }
974 drop(reader_task);
975
976 queue.execute(&mut ctx);
977 } else {
978 drop(task);
979 }
980
981 return result.map(ReadOutcome::Value).map_err(|error| {
982 self.task_error_to_turbo_tasks_execution_error(&error, &mut ctx)
983 .with_task_context(task_id, turbo_tasks.pin())
984 .into()
985 });
986 }
987 drop(reader_task);
988
989 let note = EventDescription::new(|| {
990 move || {
991 if let Some(reader) = reader_description.as_ref() {
992 format!("try_read_task_output (recompute) from {reader}",)
993 } else {
994 "try_read_task_output (recompute, untracked)".to_string()
995 }
996 }
997 });
998
999 let (in_progress_state, listener) = InProgressState::new_scheduled_with_listener(
1001 TaskExecutionReason::OutputNotAvailable,
1002 EventDescription::new(|| task.get_task_desc_fn()),
1003 note,
1004 );
1005
1006 let old = task.set_in_progress(in_progress_state);
1009 debug_assert!(old.is_none(), "InProgress already exists");
1010 ctx.schedule_task(&task, TaskPriority::Recomputation);
1011
1012 Ok(ReadOutcome::Scheduled(listener))
1013 }
1014
1015 fn try_read_task_cell(
1016 &self,
1017 task_id: TaskId,
1018 reader: Option<TaskId>,
1019 cell: CellId,
1020 options: ReadCellOptions,
1021 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1022 ) -> Result<ReadOutcome<TypedCellContent>> {
1023 self.assert_not_persistent_calling_transient(reader, task_id);
1024
1025 fn add_cell_dependency(
1026 task_id: TaskId,
1027 mut task: TaskGuard<'_>,
1028 reader: Option<TaskId>,
1029 reader_task: Option<TaskGuard<'_>>,
1030 cell: CellId,
1031 key: Option<u64>,
1032 ) {
1033 if let Some(mut reader_task) = reader_task {
1034 let reader = reader.unwrap();
1035 let reverse = CellRef { task: reader, cell };
1036 if let Some(k) = key {
1037 let _ = task.add_cell_dependents_hashed((reverse, k));
1038 } else {
1039 let _ = task.add_cell_dependents(reverse);
1040 }
1041 drop(task);
1042
1043 let target = CellRef {
1049 task: task_id,
1050 cell,
1051 };
1052 if let Some(k) = key {
1053 if !reader_task.remove_outdated_cell_dependencies_hashed(&(target, k)) {
1054 let _ = reader_task.add_cell_dependencies_hashed((target, k));
1055 }
1056 } else if !reader_task.remove_outdated_cell_dependencies(&target) {
1057 let _ = reader_task.add_cell_dependencies(target);
1058 }
1059 drop(reader_task);
1060 }
1061 }
1062
1063 let ReadCellOptions {
1064 tracking,
1065 final_read_hint,
1066 } = options;
1067
1068 let mut ctx = self.execute_context(turbo_tasks);
1069 let need_reader_task = reader.and_then(|reader_id| {
1070 (self.should_track_dependencies()
1071 && !matches!(tracking, ReadCellTracking::Untracked)
1072 && reader_id != task_id)
1073 .then_some(reader_id)
1074 });
1075 let Some((mut task, reader_task)) =
1076 lock_task_and_optional_reader(&mut ctx, task_id, need_reader_task)
1077 else {
1078 return Err(collected_task_read_error(task_id, "read_task_cell"));
1079 };
1080
1081 let content = if final_read_hint {
1082 task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence)
1083 } else {
1084 task.get_cell_data(&cell).cloned()
1085 };
1086 if let Some(content) = content {
1087 if tracking.should_track(false) {
1088 add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1089 }
1090 return Ok(ReadOutcome::Value(TypedCellContent(
1091 cell.type_id(),
1092 CellContent(Some(content)),
1093 )));
1094 }
1095
1096 let in_progress = task.get_in_progress();
1097 if matches!(
1098 in_progress,
1099 Some(InProgressState::InProgress(..) | InProgressState::Scheduled { .. })
1100 ) {
1101 let started = matches!(in_progress, Some(InProgressState::InProgress(..)));
1104 let listener = self
1105 .listen_to_cell(&mut task, task_id, need_reader_task, &reader_task, cell)
1106 .0;
1107 return Ok(if started {
1108 ReadOutcome::InProgress(listener)
1109 } else {
1110 ReadOutcome::Scheduled(listener)
1111 });
1112 }
1113 let is_cancelled = matches!(in_progress, Some(InProgressState::Canceled));
1114
1115 let max_id = task.get_cell_type_max_index(&cell.type_id()).copied();
1117 let Some(max_id) = max_id else {
1118 let task_desc = task.get_task_description();
1119 if tracking.should_track(true) {
1120 add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1121 }
1122 bail!(
1123 "Cell {cell:?} no longer exists in task {task_desc} (no cell of this type exists)",
1124 );
1125 };
1126 if cell.index() >= max_id {
1127 let task_desc = task.get_task_description();
1128 if tracking.should_track(true) {
1129 add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1130 }
1131 bail!("Cell {cell:?} no longer exists in task {task_desc} (index out of bounds)");
1132 }
1133
1134 if is_cancelled {
1140 bail!("{} was canceled", task.get_task_description());
1141 }
1142
1143 let (listener, new_listener) =
1145 self.listen_to_cell(&mut task, task_id, need_reader_task, &reader_task, cell);
1146 drop(reader_task);
1147 if !new_listener {
1148 return Ok(ReadOutcome::InProgress(listener));
1150 }
1151
1152 let _span = tracing::trace_span!(
1153 "recomputation",
1154 cell_type = get_value_type(cell.type_id()).ty.global_name,
1155 cell_index = cell.index()
1156 )
1157 .entered();
1158
1159 let _ = task.add_scheduled(
1160 TaskExecutionReason::CellNotAvailable,
1161 EventDescription::new(|| task.get_task_desc_fn()),
1162 );
1163 ctx.schedule_task(&task, TaskPriority::Recomputation);
1164
1165 Ok(ReadOutcome::Scheduled(listener))
1166 }
1167
1168 fn listen_to_cell(
1169 &self,
1170 task: &mut TaskGuard<'_>,
1171 task_id: TaskId,
1172 reader: Option<TaskId>,
1173 reader_task: &Option<TaskGuard<'_>>,
1174 cell: CellId,
1175 ) -> (EventListener, bool) {
1176 let note = || {
1177 let reader_desc = reader_task.as_ref().map(|r| r.get_task_desc_fn());
1178 move || {
1179 if let Some(reader_desc) = reader_desc.as_ref() {
1180 format!("try_read_task_cell (in progress) from {}", (reader_desc)())
1181 } else if let Some(reader_id) = reader {
1182 format!("try_read_task_cell (in progress) from {reader_id:?}")
1183 } else {
1184 "try_read_task_cell (in progress, untracked)".to_string()
1185 }
1186 }
1187 };
1188 if let Some(in_progress) = task.get_in_progress_cells(&cell) {
1189 let listener = in_progress.event.listen_with_note(note);
1191 return (listener, false);
1192 }
1193 let in_progress = InProgressCellState::new(task_id, cell);
1194 let listener = in_progress.event.listen_with_note(note);
1195 let old = task.insert_in_progress_cells(cell, in_progress);
1196 debug_assert!(old.is_none(), "InProgressCell already exists");
1197 (listener, true)
1198 }
1199
1200 #[allow(clippy::type_complexity, reason = "only used for tests")]
1213 fn snapshot_and_persist(
1214 &self,
1215 parent_span: Option<tracing::Id>,
1216 reason: SnapshotReason,
1217 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1218 ) -> Result<Option<(Instant, bool, Option<(GcStats, GcPassResult)>)>> {
1219 let mut status = self.snapshot_in_progress.lock();
1221 if *status == LastPersistenceStatus::Failed {
1222 return Ok(None);
1223 }
1224 *status = LastPersistenceStatus::Failed;
1227 let result = self.snapshot_and_persist_locked(parent_span, reason, turbo_tasks);
1228 if result.is_ok() {
1229 *status = LastPersistenceStatus::Ok;
1230 }
1231 result
1232 }
1233
1234 #[allow(clippy::type_complexity, reason = "only used for tests")]
1236 fn snapshot_and_persist_locked(
1237 &self,
1238 parent_span: Option<tracing::Id>,
1239 reason: SnapshotReason,
1240 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1241 ) -> Result<Option<(Instant, bool, Option<(GcStats, GcPassResult)>)>> {
1242 let snapshot_span =
1243 tracing::trace_span!(parent: parent_span.clone(), "snapshot", reason = reason.as_str())
1244 .entered();
1245
1246 let start = Instant::now();
1250 let wall_start = SystemTime::now();
1254 let snapshot_phase = if reason.waits_for_operations() {
1255 self.snapshot_coord
1256 .begin_snapshot(|slow| Self::report_slow_settle_for_snapshot(turbo_tasks, slow))
1257 } else {
1258 match self.snapshot_coord.try_begin_snapshot() {
1259 Some(phase) => phase,
1260 None => return Ok(None),
1261 }
1262 };
1263 let (gc_elapsed, gc_roots_to_persist, gc_outcome) = if self.gc_enabled {
1264 let gc_span = tracing::info_span!(
1265 "gc",
1266 stats = tracing::field::Empty,
1267 interrupted = tracing::field::Empty
1268 )
1269 .entered();
1270 let (stats, result, roots) =
1271 self.gc_collect(turbo_tasks, &snapshot_phase, reason.gc_is_interruptible());
1272 gc_span.record("stats", display(&stats));
1273 gc_span.record("interrupted", result.interrupted);
1274 if result.interrupted {
1275 drop(snapshot_phase);
1278 drop(gc_span);
1279 return Ok(Some((start, false, Some((stats, result)))));
1280 }
1281 (Some(start.elapsed()), roots, Some((stats, result)))
1282 } else {
1283 (None, None, None)
1284 };
1285
1286 debug_assert!(self.should_persist());
1287
1288 let snapshot_guard = self.storage.start_snapshot();
1289
1290 let snapshot_time = Instant::now();
1291
1292 #[cfg(feature = "print_cache_item_size")]
1293 #[derive(Default)]
1294 struct TaskCacheStats {
1295 data: usize,
1296 #[cfg(feature = "print_cache_item_size_with_compressed")]
1297 data_compressed: usize,
1298 data_count: usize,
1299 meta: usize,
1300 #[cfg(feature = "print_cache_item_size_with_compressed")]
1301 meta_compressed: usize,
1302 meta_count: usize,
1303 upper_count: usize,
1304 collectibles_count: usize,
1305 aggregated_collectibles_count: usize,
1306 children_count: usize,
1307 followers_count: usize,
1308 collectibles_dependents_count: usize,
1309 aggregated_dirty_containers_count: usize,
1310 output_size: usize,
1311 }
1312 #[cfg(feature = "print_cache_item_size")]
1315 struct FormatSizes {
1316 size: usize,
1317 #[cfg(feature = "print_cache_item_size_with_compressed")]
1318 compressed_size: usize,
1319 }
1320 #[cfg(feature = "print_cache_item_size")]
1321 impl std::fmt::Display for FormatSizes {
1322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1323 use turbo_tasks::util::FormatBytes;
1324 #[cfg(feature = "print_cache_item_size_with_compressed")]
1325 {
1326 write!(
1327 f,
1328 "{} ({} compressed)",
1329 FormatBytes(self.size),
1330 FormatBytes(self.compressed_size)
1331 )
1332 }
1333 #[cfg(not(feature = "print_cache_item_size_with_compressed"))]
1334 {
1335 write!(f, "{}", FormatBytes(self.size))
1336 }
1337 }
1338 }
1339 #[cfg(feature = "print_cache_item_size")]
1340 impl TaskCacheStats {
1341 #[cfg(feature = "print_cache_item_size_with_compressed")]
1342 fn compressed_size(data: &[u8]) -> usize {
1343 lz4_flex::block::compress(data).len()
1344 }
1345
1346 fn add_data(&mut self, data: &[u8]) {
1347 self.data += data.len();
1348 #[cfg(feature = "print_cache_item_size_with_compressed")]
1349 {
1350 self.data_compressed += Self::compressed_size(data);
1351 }
1352 self.data_count += 1;
1353 }
1354
1355 fn add_meta(&mut self, data: &[u8]) {
1356 self.meta += data.len();
1357 #[cfg(feature = "print_cache_item_size_with_compressed")]
1358 {
1359 self.meta_compressed += Self::compressed_size(data);
1360 }
1361 self.meta_count += 1;
1362 }
1363
1364 fn add_counts(&mut self, stats: &crate::backing_storage::SnapshotItemStats) {
1365 let counts = &stats.counts;
1366 self.upper_count += counts.upper;
1367 self.collectibles_count += counts.collectibles;
1368 self.aggregated_collectibles_count += counts.aggregated_collectibles;
1369 self.children_count += counts.children;
1370 self.followers_count += counts.followers;
1371 self.collectibles_dependents_count += counts.collectibles_dependents;
1372 self.aggregated_dirty_containers_count += counts.aggregated_dirty_containers;
1373 self.output_size += stats.output_size;
1374 }
1375
1376 fn sort_key(&self) -> usize {
1379 #[cfg(feature = "print_cache_item_size_with_compressed")]
1380 {
1381 self.data_compressed + self.meta_compressed
1382 }
1383 #[cfg(not(feature = "print_cache_item_size_with_compressed"))]
1384 {
1385 self.data + self.meta
1386 }
1387 }
1388
1389 fn format_total(&self) -> FormatSizes {
1390 FormatSizes {
1391 size: self.data + self.meta,
1392 #[cfg(feature = "print_cache_item_size_with_compressed")]
1393 compressed_size: self.data_compressed + self.meta_compressed,
1394 }
1395 }
1396
1397 fn format_data(&self) -> FormatSizes {
1398 FormatSizes {
1399 size: self.data,
1400 #[cfg(feature = "print_cache_item_size_with_compressed")]
1401 compressed_size: self.data_compressed,
1402 }
1403 }
1404
1405 fn format_avg_data(&self) -> FormatSizes {
1406 FormatSizes {
1407 size: self.data.checked_div(self.data_count).unwrap_or(0),
1408 #[cfg(feature = "print_cache_item_size_with_compressed")]
1409 compressed_size: self
1410 .data_compressed
1411 .checked_div(self.data_count)
1412 .unwrap_or(0),
1413 }
1414 }
1415
1416 fn format_meta(&self) -> FormatSizes {
1417 FormatSizes {
1418 size: self.meta,
1419 #[cfg(feature = "print_cache_item_size_with_compressed")]
1420 compressed_size: self.meta_compressed,
1421 }
1422 }
1423
1424 fn format_avg_meta(&self) -> FormatSizes {
1425 FormatSizes {
1426 size: self.meta.checked_div(self.meta_count).unwrap_or(0),
1427 #[cfg(feature = "print_cache_item_size_with_compressed")]
1428 compressed_size: self
1429 .meta_compressed
1430 .checked_div(self.meta_count)
1431 .unwrap_or(0),
1432 }
1433 }
1434 }
1435 #[cfg(feature = "print_cache_item_size")]
1436 let task_cache_stats: Mutex<FxHashMap<_, TaskCacheStats>> =
1437 Mutex::new(FxHashMap::default());
1438
1439 let process = |task_id: TaskId,
1445 inner: &TaskStorage,
1446 category: TaskDataCategory,
1447 buffer: &mut TurboBincodeBuffer| {
1448 debug_assert!(
1449 !self.gc_enabled || inner.flags.deleted() || !inner.gc_collectible(),
1450 "tasks scheduled for persistent must not be collectible, this implies a missed \
1451 task during GC"
1452 );
1453 encode_snapshot_item(task_id, inner, category, buffer).unwrap_or_else(|err| {
1454 panic!(
1455 "Serializing task {} failed: {:?}",
1456 self.debug_get_task_description(task_id),
1457 err
1458 )
1459 })
1460 };
1461
1462 let inspect_snapshot_item = |item: &SnapshotItem| {
1465 #[cfg(feature = "print_cache_item_size")]
1466 if let SnapshotItem::Put {
1467 meta, data, stats, ..
1468 } = item
1469 && (meta.is_some() || data.is_some())
1470 {
1471 let mut task_cache_stats = task_cache_stats.lock();
1472 let entry = task_cache_stats.entry(stats.task_name.clone()).or_default();
1473 entry.add_counts(stats);
1474 if let Some(meta) = meta {
1475 entry.add_meta(meta);
1476 }
1477 if let Some(data) = data {
1478 entry.add_data(data);
1479 }
1480 }
1481 #[cfg(not(feature = "print_cache_item_size"))]
1482 let _ = item;
1483 };
1484
1485 let task_snapshots = self.storage.take_snapshot(
1488 &snapshot_phase,
1489 snapshot_guard,
1490 &process,
1491 &inspect_snapshot_item,
1492 reason.drain_entries(),
1493 );
1494 drop(snapshot_phase);
1495
1496 drop(snapshot_span);
1497 let snapshot_duration = start.elapsed();
1498 let task_count = task_snapshots.len();
1499
1500 if task_snapshots.is_empty() && gc_roots_to_persist.is_none() {
1501 return Ok(Some((snapshot_time, false, gc_outcome)));
1504 }
1505
1506 let persist_start = Instant::now();
1507 let span = tracing::info_span!(
1508 parent: parent_span,
1509 "persist",
1510 reason = reason.as_str(),
1511 snapshot_meta = tracing::field::Empty,
1512 )
1513 .entered();
1514 let snapshot_meta = self
1518 .backing_storage
1519 .save_snapshot(gc_roots_to_persist, task_snapshots)?;
1520 span.record("snapshot_meta", display(snapshot_meta));
1521
1522 #[cfg(feature = "print_cache_item_size")]
1523 {
1524 let mut task_cache_stats = task_cache_stats
1525 .into_inner()
1526 .into_iter()
1527 .collect::<Vec<_>>();
1528 if !task_cache_stats.is_empty() {
1529 use turbo_tasks::util::FormatBytes;
1530
1531 use crate::utils::markdown_table::print_markdown_table;
1532
1533 task_cache_stats.sort_unstable_by(|(key_a, stats_a), (key_b, stats_b)| {
1534 (stats_b.sort_key(), key_b).cmp(&(stats_a.sort_key(), key_a))
1535 });
1536
1537 println!(
1538 "Task cache stats: {}",
1539 FormatSizes {
1540 size: task_cache_stats
1541 .iter()
1542 .map(|(_, s)| s.data + s.meta)
1543 .sum::<usize>(),
1544 #[cfg(feature = "print_cache_item_size_with_compressed")]
1545 compressed_size: task_cache_stats
1546 .iter()
1547 .map(|(_, s)| s.data_compressed + s.meta_compressed)
1548 .sum::<usize>()
1549 },
1550 );
1551
1552 print_markdown_table(
1553 [
1554 "Task",
1555 " Total Size",
1556 " Data Size",
1557 " Data Count x Avg",
1558 " Data Count x Avg",
1559 " Meta Size",
1560 " Meta Count x Avg",
1561 " Meta Count x Avg",
1562 " Uppers",
1563 " Coll",
1564 " Agg Coll",
1565 " Children",
1566 " Followers",
1567 " Coll Deps",
1568 " Agg Dirty",
1569 " Output Size",
1570 ],
1571 task_cache_stats.iter(),
1572 |(task_desc, stats)| {
1573 [
1574 task_desc.to_string(),
1575 format!(" {}", stats.format_total()),
1576 format!(" {}", stats.format_data()),
1577 format!(" {} x", stats.data_count),
1578 format!("{}", stats.format_avg_data()),
1579 format!(" {}", stats.format_meta()),
1580 format!(" {} x", stats.meta_count),
1581 format!("{}", stats.format_avg_meta()),
1582 format!(" {}", stats.upper_count),
1583 format!(" {}", stats.collectibles_count),
1584 format!(" {}", stats.aggregated_collectibles_count),
1585 format!(" {}", stats.children_count),
1586 format!(" {}", stats.followers_count),
1587 format!(" {}", stats.collectibles_dependents_count),
1588 format!(" {}", stats.aggregated_dirty_containers_count),
1589 format!(" {}", FormatBytes(stats.output_size)),
1590 ]
1591 },
1592 );
1593 }
1594 }
1595
1596 let elapsed = start.elapsed();
1597 let persist_duration = persist_start.elapsed();
1598 if elapsed > Duration::from_secs(10) {
1600 turbo_tasks.send_compilation_event(Arc::new(TimingEvent::new(
1601 "Finished writing to filesystem cache".to_string(),
1602 elapsed,
1603 )));
1604 }
1605
1606 let (persist_wall_start, persist_wall_duration) = if let Some(gc_elapsed) = gc_elapsed {
1607 turbo_tasks.send_compilation_event(Arc::new(TraceEvent::new_with_duration(
1608 "turbopack-gc",
1609 wall_start,
1610 gc_elapsed,
1611 serde_json::json!([]),
1612 )));
1613 (wall_start + gc_elapsed, elapsed.saturating_sub(gc_elapsed))
1614 } else {
1615 (wall_start, elapsed)
1616 };
1617 turbo_tasks.send_compilation_event(Arc::new(TraceEvent::new_with_duration(
1618 "turbopack-persistence",
1619 persist_wall_start,
1620 persist_wall_duration,
1621 serde_json::json!([
1622 ["reason", reason.as_str()],
1623 [
1624 "snapshot_duration_ms",
1625 snapshot_duration.as_secs_f64() * 1000.0,
1626 ],
1627 [
1628 "persist_duration_ms",
1629 persist_duration.as_secs_f64() * 1000.0,
1630 ],
1631 ["task_count", task_count],
1632 ["bytes_written", snapshot_meta.bytes_written,],
1633 ["bytes_deleted", snapshot_meta.bytes_deleted,]
1634 ]),
1635 )));
1636
1637 Ok(Some((snapshot_time, true, gc_outcome)))
1638 }
1639
1640 fn startup(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1641 if matches!(self.options.storage_mode, Some(StorageMode::ReadWrite)) {
1644 let _span = trace_span!("persisting background job").entered();
1646 let _span = tracing::info_span!("thread").entered();
1647 turbo_tasks.schedule_backend_background_job(TurboTasksBackendJob::Snapshot);
1648 }
1649 }
1650
1651 fn stopping(&self) {
1652 *self.stopping.write() = true;
1654 self.stopping_event.notify(usize::MAX);
1655 }
1656
1657 #[allow(unused_variables)]
1658 fn stop(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1659 #[cfg(feature = "verify_aggregation_graph")]
1660 {
1661 self.is_idle.store(false, Ordering::Release);
1662 self.verify_aggregation_graph(turbo_tasks, false);
1663 }
1664 self.storage.drop_task_cache();
1666 if self.should_persist()
1667 && let Err(err) =
1668 self.snapshot_and_persist(Span::current().into(), SnapshotReason::Stop, turbo_tasks)
1669 {
1670 eprintln!("WARNING: Saving the filesystem cache failed:\n {err:#}");
1672 }
1673 self.storage.drop_contents();
1674 self.backing_storage.shutdown();
1676 }
1677
1678 #[allow(unused_variables)]
1679 fn idle_start(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1680 self.idle_start_event.notify(usize::MAX);
1681
1682 #[cfg(feature = "verify_aggregation_graph")]
1683 {
1684 use tokio::select;
1685
1686 self.is_idle.store(true, Ordering::Release);
1687 let turbo_tasks = turbo_tasks.pin();
1691 tokio::task::spawn(async move {
1692 let backend = &turbo_tasks.backend();
1693 select! {
1694 _ = tokio::time::sleep(Duration::from_secs(5)) => {
1695 }
1697 _ = backend.idle_end_event.listen() => {
1698 return;
1699 }
1700 }
1701 if !backend.is_idle.load(Ordering::Relaxed) {
1702 return;
1703 }
1704 backend.verify_aggregation_graph(&turbo_tasks, true);
1705 });
1706 }
1707 }
1708
1709 fn idle_end(&self) {
1710 #[cfg(feature = "verify_aggregation_graph")]
1711 self.is_idle.store(false, Ordering::Release);
1712 self.idle_end_event.notify(usize::MAX);
1713 }
1714
1715 fn get_or_create_task(
1716 &self,
1717 native_fn: &'static NativeFunction,
1718 this: Option<RawVc>,
1719 arg: &mut dyn DynTaskInputsStorage,
1720 parent_task: Option<TaskId>,
1721 persistence: TaskPersistence,
1722 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1723 ) -> TaskId {
1724 let transient = matches!(persistence, TaskPersistence::Transient);
1725
1726 if transient
1727 && let Some(parent_task) = parent_task
1728 && !parent_task.is_transient()
1729 {
1730 let task_type = CachedTaskType {
1731 native_fn,
1732 this,
1733 arg: arg.take_box(),
1734 };
1735 self.panic_persistent_calling_transient(
1736 self.debug_get_task_description(parent_task),
1737 Some(&task_type),
1738 );
1739 }
1740
1741 let is_root = native_fn.is_root;
1742
1743 let arg_ref = arg.as_ref();
1745 let hash = CachedTaskType::hash_from_components(
1746 self.storage.task_cache.hasher(),
1747 native_fn,
1748 this,
1749 arg_ref,
1750 );
1751 let shard = get_shard(&self.storage.task_cache, hash);
1755
1756 let mut ctx = self.execute_context(turbo_tasks);
1757 let mut created_new = false;
1758 if let Some(task_id) =
1762 get_in_shard(shard, hash, |k| k.eq_components(native_fn, this, arg_ref))
1763 {
1764 self.track_cache_hit_by_fn(native_fn);
1765 connect_child(
1766 parent_task,
1767 task_id,
1768 false,
1769 ctx,
1770 );
1771 return task_id;
1772 }
1773
1774 let task_id = if !transient
1779 && let Some((task_id, stored_type)) = ctx.task_by_type(native_fn, this, arg_ref)
1780 {
1781 self.track_cache_hit_by_fn(native_fn);
1782 with_entry_in_shard(
1785 shard,
1786 self.storage.task_cache.hasher(),
1787 hash,
1788 arg,
1789 |k, arg| k.eq_components(native_fn, this, arg.as_ref()),
1790 |entry, _arg| {
1791 if let Entry::Vacant(entry) = entry {
1792 entry.insert((stored_type, task_id));
1793 }
1794 },
1795 );
1796 task_id
1797 } else {
1798 let (task_id, created) = with_entry_in_shard(
1799 shard,
1800 self.storage.task_cache.hasher(),
1801 hash,
1802 arg,
1803 |k, arg| k.eq_components(native_fn, this, arg.as_ref()),
1804 |entry, arg| match entry {
1805 Entry::Occupied(entry) => {
1806 (entry.get().1, false)
1809 }
1810 Entry::Vacant(entry) => {
1811 let task_type = CachedTaskTypeArc::new(CachedTaskType {
1815 native_fn,
1816 this,
1817 arg: arg.take_box(),
1818 });
1819 let task_id = if transient {
1820 self.transient_task_id_factory.get()
1821 } else {
1822 self.persisted_task_id_factory.get()
1823 };
1824 self.storage
1828 .initialize_new_task(task_id, Some(task_type.clone()));
1829 entry.insert((task_type, task_id));
1830 (task_id, true)
1831 }
1832 },
1833 );
1834
1835 created_new = created;
1838 if created {
1839 self.track_cache_miss_by_fn(native_fn);
1840 if is_root {
1843 AggregationUpdateQueue::run(
1844 AggregationUpdateJob::UpdateAggregationNumber {
1845 task_id,
1846 base_aggregation_number: u32::MAX,
1847 distance: None,
1848 },
1849 &mut ctx,
1850 );
1851 } else if native_fn.is_session_dependent && self.should_track_dependencies() {
1852 const SESSION_DEPENDENT_AGGREGATION_NUMBER: u32 = u32::MAX >> 2;
1853 AggregationUpdateQueue::run(
1854 AggregationUpdateJob::UpdateAggregationNumber {
1855 task_id,
1856 base_aggregation_number: SESSION_DEPENDENT_AGGREGATION_NUMBER,
1857 distance: None,
1858 },
1859 &mut ctx,
1860 );
1861 }
1862 } else {
1863 self.track_cache_hit_by_fn(native_fn);
1864 }
1865
1866 task_id
1867 };
1868
1869 connect_child(parent_task, task_id, created_new, ctx);
1872
1873 task_id
1874 }
1875
1876 fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1877 if !self.should_track_dependencies() {
1878 panic!("Dependency tracking is disabled so invalidation is not allowed");
1879 }
1880 invalidate(
1881 [task_id],
1882 #[cfg(feature = "task_dirty_cause")]
1883 TaskDirtyCause::Invalidator,
1884 self.execute_context(turbo_tasks),
1885 );
1886 }
1887
1888 fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1889 if !self.should_track_dependencies() {
1890 panic!("Dependency tracking is disabled so invalidation is not allowed");
1891 }
1892 debug_assert!(
1895 tasks.len() <= 1 || tasks.iter().collect::<FxHashSet<_>>().len() == tasks.len(),
1896 "invalidate_tasks() must not be called with duplicate task ids: {tasks:?}"
1897 );
1898 let current_task_priority = turbo_tasks.get_current_task_priority();
1902 parallel::for_each_chunk(tasks, |chunk| {
1903 invalidate(
1904 chunk.iter().copied(),
1905 #[cfg(feature = "task_dirty_cause")]
1907 TaskDirtyCause::Invalidator,
1908 self.execute_context_with_priority(turbo_tasks, current_task_priority),
1909 );
1910 });
1911 }
1912
1913 fn invalidate_tasks_set(
1914 &self,
1915 tasks: &AutoSet<TaskId, BuildHasherDefault<FxHasher>, 2>,
1916 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1917 ) {
1918 if !self.should_track_dependencies() {
1919 panic!("Dependency tracking is disabled so invalidation is not allowed");
1920 }
1921 invalidate(
1922 tasks.iter().copied(),
1923 #[cfg(feature = "task_dirty_cause")]
1924 TaskDirtyCause::Unknown,
1925 self.execute_context(turbo_tasks),
1926 );
1927 }
1928
1929 fn invalidate_serialization(
1930 &self,
1931 task_id: TaskId,
1932 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1933 ) {
1934 if task_id.is_transient() {
1935 return;
1936 }
1937 let mut ctx = self.execute_context(turbo_tasks);
1938 let mut task = ctx.task(task_id, TaskDataCategory::Data);
1939 task.invalidate_serialization();
1940 }
1941
1942 fn debug_get_task_description(&self, task_id: TaskId) -> String {
1943 let task = self.storage.access_mut(task_id);
1944 if let Some(value) = task.get_persistent_task_type() {
1945 format!("{task_id:?} {}", value)
1946 } else if let Some(value) = task.get_transient_task_type() {
1947 format!("{task_id:?} {}", value)
1948 } else {
1949 format!("{task_id:?} unknown")
1950 }
1951 }
1952
1953 fn get_task_name(
1954 &self,
1955 task_id: TaskId,
1956 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1957 ) -> String {
1958 let mut ctx = self.execute_context(turbo_tasks);
1959 let Some(task) = ctx.try_task(task_id, TaskDataCategory::Data) else {
1962 return "unknown".to_string();
1963 };
1964 if let Some(value) = task.get_persistent_task_type() {
1965 value.to_string()
1966 } else if let Some(value) = task.get_transient_task_type() {
1967 value.to_string()
1968 } else {
1969 "unknown".to_string()
1972 }
1973 }
1974
1975 fn debug_get_cached_task_type(&self, task_id: TaskId) -> Option<CachedTaskTypeArc> {
1976 let task = self.storage.access_mut(task_id);
1977 task.get_persistent_task_type().cloned()
1978 }
1979
1980 fn task_execution_canceled(
1981 &self,
1982 task_id: TaskId,
1983 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1984 ) {
1985 let mut ctx = self.execute_context(turbo_tasks);
1986 let mut task = ctx.task(task_id, TaskDataCategory::All);
1987 if let Some(in_progress) = task.take_in_progress() {
1988 match in_progress {
1989 InProgressState::Scheduled {
1990 done_event,
1991 reason: _,
1992 } => done_event.notify(usize::MAX),
1993 InProgressState::InProgress(InProgressStateInner { done_event, .. }) => {
1994 done_event.notify(usize::MAX)
1995 }
1996 InProgressState::Canceled => {}
1997 }
1998 }
1999 let in_progress_cells = task.take_in_progress_cells();
2002 if let Some(ref cells) = in_progress_cells {
2003 for state in cells.values() {
2004 state.event.notify(usize::MAX);
2005 }
2006 }
2007
2008 task.set_output(OutputValue::Error(Arc::new(TaskError::Error(Box::new(
2013 TaskErrorItem {
2014 message: TurboTasksExecutionErrorMessage::PIISafe(std::borrow::Cow::Borrowed(
2015 "task execution was canceled by shutdown",
2016 )),
2017 source: None,
2018 },
2019 )))));
2020
2021 let data_update = if self.should_track_dependencies() && !task_id.is_transient() {
2027 task.update_dirty_state(Some(Dirtyness::SessionDependent))
2028 } else {
2029 None
2030 };
2031
2032 let old = task.set_in_progress(InProgressState::Canceled);
2033 debug_assert!(old.is_none(), "InProgress already exists");
2034 drop(task);
2035
2036 if let Some(data_update) = data_update {
2037 AggregationUpdateQueue::run(data_update, &mut ctx);
2038 }
2039
2040 drop(in_progress_cells);
2041 }
2042
2043 fn try_start_task_execution(
2044 &self,
2045 task_id: TaskId,
2046 priority: TaskPriority,
2047 turbo_tasks: &TurboTasks<TurboTasksBackend>,
2048 ) -> Option<TaskExecutionSpec<'_>> {
2049 let execution_reason;
2050 let task_type;
2051 #[cfg(feature = "task_dirty_cause")]
2052 let cause;
2053 {
2054 let mut ctx = self.execute_context(turbo_tasks);
2055 let mut task = ctx.task(task_id, TaskDataCategory::All);
2056 task.assert_not_deleted("try_start_task_execution");
2057 task_type = task.get_task_type().to_owned();
2058 let once_task = matches!(task_type, TaskType::Transient(ref tt) if matches!(&**tt, TransientTask::Once(_)));
2059 if let Some(tasks) = task.prefetch() {
2060 drop(task);
2061 ctx.prepare_tasks(tasks, "prefetch");
2062 task = ctx.task(task_id, TaskDataCategory::All);
2063 }
2064 let in_progress = task.take_in_progress()?;
2065 let InProgressState::Scheduled { done_event, reason } = in_progress else {
2066 let old = task.set_in_progress(in_progress);
2067 debug_assert!(old.is_none(), "InProgress already exists");
2068 return None;
2069 };
2070 execution_reason = reason;
2071 #[cfg(feature = "task_dirty_cause")]
2072 {
2073 cause = match task.get_dirty() {
2074 Some(Dirtyness::Dirty { cause, .. }) => Some(cause.clone()),
2075 _ => None,
2076 };
2077 }
2078 let old = task.set_in_progress(InProgressState::InProgress(Box::new(
2079 InProgressStateInner {
2080 stale: false,
2081 once_task,
2082 done_event,
2083 marked_as_completed: false,
2084 new_children: Default::default(),
2085 },
2086 )));
2087 debug_assert!(old.is_none(), "InProgress already exists");
2088
2089 enum Collectible {
2091 Current(CollectibleRef, i32),
2092 Outdated(CollectibleRef),
2093 }
2094 let collectibles = task
2095 .iter_collectibles()
2096 .map(|(&collectible, &value)| Collectible::Current(collectible, value))
2097 .chain(
2098 task.iter_outdated_collectibles()
2099 .map(|(collectible, _count)| Collectible::Outdated(*collectible)),
2100 )
2101 .collect::<Vec<_>>();
2102 for collectible in collectibles {
2103 match collectible {
2104 Collectible::Current(collectible, value) => {
2105 let _ = task.insert_outdated_collectible(collectible, value);
2106 }
2107 Collectible::Outdated(collectible) => {
2108 if task
2109 .collectibles()
2110 .is_none_or(|m| m.get(&collectible).is_none())
2111 {
2112 task.remove_outdated_collectibles(&collectible);
2113 }
2114 }
2115 }
2116 }
2117
2118 if self.should_track_dependencies() {
2119 let cell_dependencies = task.iter_cell_dependencies().collect();
2124 task.set_outdated_cell_dependencies(cell_dependencies);
2125 let cell_dependencies_hashed = task.iter_cell_dependencies_hashed().collect();
2126 task.set_outdated_cell_dependencies_hashed(cell_dependencies_hashed);
2127
2128 let outdated_output_dependencies = task.iter_output_dependencies().collect();
2129 task.set_outdated_output_dependencies(outdated_output_dependencies);
2130
2131 let outdated_collectibles_dependencies =
2132 task.iter_collectibles_dependencies().collect();
2133 task.set_outdated_collectibles_dependencies(outdated_collectibles_dependencies);
2134 }
2135 }
2136
2137 let (span, future) = match task_type {
2138 TaskType::Cached(task_type) => {
2139 let CachedTaskType {
2140 native_fn,
2141 this,
2142 arg,
2143 } = &*task_type;
2144 (
2145 native_fn.span(
2146 task_id.persistence(),
2147 execution_reason,
2148 priority,
2149 #[cfg(feature = "task_dirty_cause")]
2150 cause.as_ref(),
2151 ),
2152 native_fn.execute(*this, &**arg),
2153 )
2154 }
2155 TaskType::Transient(task_type) => {
2156 let span = tracing::trace_span!(
2159 "turbo_tasks::root_task",
2160 inline_execution = tracing::field::Empty
2161 );
2162 let future = match &*task_type {
2163 TransientTask::Root(f) => f(),
2164 TransientTask::Once(future_mutex) => take(&mut *future_mutex.lock())?,
2165 };
2166 (span, future)
2167 }
2168 };
2169 Some(TaskExecutionSpec { future, span })
2170 }
2171
2172 fn task_execution_completed(
2176 &self,
2177 task_id: TaskId,
2178 result: Result<RawVc, TurboTasksExecutionError>,
2179 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2180 #[cfg(feature = "verify_determinism")] stateful: bool,
2181 has_invalidator: bool,
2182 turbo_tasks: &TurboTasks<TurboTasksBackend>,
2183 ) -> Option<TaskPriority> {
2184 #[cfg(not(feature = "trace_task_details"))]
2199 let span = tracing::trace_span!(
2200 "task execution completed",
2201 new_children = tracing::field::Empty
2202 )
2203 .entered();
2204 #[cfg(feature = "trace_task_details")]
2205 let span = tracing::trace_span!(
2206 "task execution completed",
2207 task_id = display(task_id),
2208 result = match result.as_ref() {
2209 Ok(value) => display(either::Either::Left(value)),
2210 Err(err) => display(either::Either::Right(err)),
2211 },
2212 new_children = tracing::field::Empty,
2213 immutable = tracing::field::Empty,
2214 new_output = tracing::field::Empty,
2215 output_dependents = tracing::field::Empty,
2216 aggregation_number = tracing::field::Empty,
2217 stale = tracing::field::Empty,
2218 )
2219 .entered();
2220
2221 let is_error = result.is_err();
2222
2223 let mut ctx = self.execute_context(turbo_tasks);
2224
2225 let TaskExecutionCompletePrepareResult {
2226 new_children,
2227 is_now_immutable,
2228 #[cfg(feature = "verify_determinism")]
2229 no_output_set,
2230 new_output,
2231 #[cfg(feature = "task_dirty_cause")]
2232 function_id,
2233 output_dependent_tasks,
2234 is_recomputation,
2235 is_session_dependent,
2236 } = match self.task_execution_completed_prepare(
2237 &mut ctx,
2238 #[cfg(feature = "trace_task_details")]
2239 &span,
2240 task_id,
2241 result,
2242 cell_counters,
2243 #[cfg(feature = "verify_determinism")]
2244 stateful,
2245 has_invalidator,
2246 ) {
2247 Ok(r) => r,
2248 Err(stale_priority) => {
2249 #[cfg(feature = "trace_task_details")]
2251 span.record("stale", "prepare");
2252 return Some(stale_priority);
2253 }
2254 };
2255
2256 #[cfg(feature = "trace_task_details")]
2257 span.record("new_output", new_output.is_some());
2258 #[cfg(feature = "trace_task_details")]
2259 span.record("output_dependents", output_dependent_tasks.len());
2260
2261 if !output_dependent_tasks.is_empty() {
2262 self.task_execution_completed_invalidate_output_dependent(
2263 &mut ctx,
2264 task_id,
2265 #[cfg(feature = "task_dirty_cause")]
2266 function_id,
2267 output_dependent_tasks,
2268 );
2269 }
2270
2271 let has_new_children = !new_children.is_empty();
2272 span.record("new_children", new_children.len());
2273
2274 if has_new_children
2275 && let Some(stale_priority) =
2276 self.task_execution_completed_connect(&mut ctx, task_id, new_children)
2277 {
2278 #[cfg(feature = "trace_task_details")]
2280 span.record("stale", "connect");
2281 return Some(stale_priority);
2282 }
2283
2284 let (stale_priority, in_progress_cells, removed_data) = self
2285 .task_execution_completed_finish(
2286 &mut ctx,
2287 task_id,
2288 #[cfg(feature = "verify_determinism")]
2289 no_output_set,
2290 new_output,
2291 is_now_immutable,
2292 is_session_dependent,
2293 cell_counters,
2294 is_error,
2295 is_recomputation,
2296 );
2297 if let Some(stale_priority) = stale_priority {
2298 #[cfg(feature = "trace_task_details")]
2300 span.record("stale", "finish");
2301 return Some(stale_priority);
2302 }
2303
2304 drop(removed_data);
2306 drop(in_progress_cells);
2307
2308 None
2309 }
2310
2311 fn task_execution_completed_prepare(
2312 &self,
2313 ctx: &mut ExecuteContext<'_>,
2314 #[cfg(feature = "trace_task_details")] span: &Span,
2315 task_id: TaskId,
2316 result: Result<RawVc, TurboTasksExecutionError>,
2317 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2318 #[cfg(feature = "verify_determinism")] stateful: bool,
2319 has_invalidator: bool,
2320 ) -> Result<TaskExecutionCompletePrepareResult, TaskPriority> {
2321 let mut task = ctx.task(task_id, TaskDataCategory::All);
2322 let is_recomputation = task.is_dirty().is_none();
2323 let is_session_dependent = self.should_track_dependencies()
2326 && matches!(task.get_task_type(), TaskTypeRef::Cached(tt) if tt.native_fn.is_session_dependent);
2327 let Some(in_progress) = task.get_in_progress_mut() else {
2328 panic!("Task execution completed, but task is not in progress: {task:#?}");
2329 };
2330 if matches!(in_progress, InProgressState::Canceled) {
2331 return Ok(TaskExecutionCompletePrepareResult {
2332 new_children: Default::default(),
2333 is_now_immutable: false,
2334 #[cfg(feature = "verify_determinism")]
2335 no_output_set: false,
2336 #[cfg(feature = "task_dirty_cause")]
2337 function_id: None,
2338 new_output: None,
2339 output_dependent_tasks: Default::default(),
2340 is_recomputation,
2341 is_session_dependent,
2342 });
2343 }
2344 let &mut InProgressState::InProgress(InProgressStateInner {
2345 stale,
2346 ref mut new_children,
2347 once_task: is_once_task,
2348 ..
2349 }) = in_progress
2350 else {
2351 panic!("Task execution completed, but task is not in progress: {task:#?}");
2352 };
2353
2354 #[cfg(not(feature = "no_fast_stale"))]
2356 if stale && !is_once_task {
2357 let stale_priority = compute_stale_priority(&task);
2358 let Some(InProgressState::InProgress(InProgressStateInner {
2359 done_event,
2360 mut new_children,
2361 ..
2362 })) = task.take_in_progress()
2363 else {
2364 unreachable!();
2365 };
2366 let old = task.set_in_progress(InProgressState::Scheduled {
2367 done_event,
2368 reason: TaskExecutionReason::Stale,
2369 });
2370 debug_assert!(old.is_none(), "InProgress already exists");
2371 for task in task.iter_children() {
2374 new_children.remove(&task);
2375 }
2376 drop(task);
2377
2378 AggregationUpdateQueue::run(
2381 AggregationUpdateJob::DecreaseActiveCounts {
2382 task_ids: new_children.into_iter().collect(),
2383 },
2384 ctx,
2385 );
2386 return Err(stale_priority);
2387 }
2388
2389 let mut new_children = take(new_children);
2391
2392 #[cfg(feature = "task_dirty_cause")]
2394 let function_id = match task.get_task_type() {
2395 TaskTypeRef::Cached(task_type) => {
2396 Some(turbo_tasks::registry::get_function_id(task_type.native_fn))
2397 }
2398 TaskTypeRef::Transient(_) => None,
2399 };
2400
2401 #[cfg(feature = "verify_determinism")]
2403 if stateful {
2404 task.set_stateful(true);
2405 }
2406
2407 if has_invalidator {
2409 task.set_invalidator(true);
2410 }
2411
2412 if result.is_ok() || is_recomputation {
2422 let old_counters: FxHashMap<_, _> = task
2423 .iter_cell_type_max_index()
2424 .map(|(&k, &v)| (k, v))
2425 .collect();
2426 let mut counters_to_remove = old_counters.clone();
2427
2428 for (&cell_type, &max_index) in cell_counters.iter() {
2429 if let Some(old_max_index) = counters_to_remove.remove(&cell_type) {
2430 if old_max_index != max_index {
2431 task.insert_cell_type_max_index(cell_type, max_index);
2432 }
2433 } else {
2434 task.insert_cell_type_max_index(cell_type, max_index);
2435 }
2436 }
2437 for (cell_type, _) in counters_to_remove {
2438 task.remove_cell_type_max_index(&cell_type);
2439 }
2440 }
2441
2442 let mut queue = AggregationUpdateQueue::new();
2443
2444 let mut old_edges = Vec::new();
2445
2446 let has_children = !new_children.is_empty();
2447 let is_immutable = task.immutable();
2448 let task_dependencies_for_immutable =
2449 if !is_immutable
2451 && !is_session_dependent
2453 && !task.invalidator()
2455 && task.is_collectibles_dependencies_empty()
2457 {
2458 Some(
2459 task.iter_output_dependencies()
2461 .chain(task.iter_cell_dependencies().map(|r| r.task))
2462 .chain(task.iter_cell_dependencies_hashed().map(|(r, _)| r.task))
2463 .collect::<FxHashSet<_>>(),
2464 )
2465 } else {
2466 None
2467 };
2468
2469 if has_children {
2470 let _aggregation_number =
2472 prepare_new_children(task_id, &mut task, &new_children, &mut queue);
2473
2474 #[cfg(feature = "trace_task_details")]
2475 span.record("aggregation_number", _aggregation_number);
2476
2477 old_edges.extend(
2479 task.iter_children()
2480 .filter(|task| !new_children.remove(task))
2481 .map(OutdatedEdge::Child),
2482 );
2483 } else {
2484 old_edges.extend(task.iter_children().map(OutdatedEdge::Child));
2485 }
2486
2487 old_edges.extend(
2488 task.iter_outdated_collectibles()
2489 .map(|(&collectible, &count)| OutdatedEdge::Collectible(collectible, count)),
2490 );
2491
2492 if self.should_track_dependencies() {
2493 old_edges.extend(task.iter_outdated_cell_dependencies().map(|cell| {
2500 OutdatedEdge::CellDependency {
2501 dependent: task_id,
2502 cell,
2503 }
2504 }));
2505 old_edges.extend(
2506 task.iter_outdated_cell_dependencies_hashed()
2507 .map(|(cell, key)| OutdatedEdge::HashedCellDependency {
2508 dependent: task_id,
2509 cell,
2510 key,
2511 }),
2512 );
2513 old_edges.extend(task.iter_outdated_output_dependencies().map(|output_task| {
2514 OutdatedEdge::OutputDependency {
2515 dependent: task_id,
2516 output_task,
2517 }
2518 }));
2519 old_edges.extend(
2520 task.iter_outdated_collectibles_dependencies()
2521 .map(OutdatedEdge::CollectiblesDependency),
2522 );
2523 }
2524
2525 let current_output = task.get_output();
2527 #[cfg(feature = "verify_determinism")]
2528 let no_output_set = current_output.is_none();
2529 let new_output = match result.map(RawVc::unpack) {
2530 Ok(RawVcUnpacked::TaskOutput(output_task_id)) => {
2531 if let Some(OutputValue::Output(current_task_id)) = current_output
2532 && *current_task_id == output_task_id
2533 {
2534 None
2535 } else {
2536 Some(OutputValue::Output(output_task_id))
2537 }
2538 }
2539 Ok(RawVcUnpacked::TaskCell(output_task_id, cell)) => {
2540 if let Some(OutputValue::Cell(CellRef {
2541 task: current_task_id,
2542 cell: current_cell,
2543 })) = current_output
2544 && *current_task_id == output_task_id
2545 && *current_cell == cell
2546 {
2547 None
2548 } else {
2549 Some(OutputValue::Cell(CellRef {
2550 task: output_task_id,
2551 cell,
2552 }))
2553 }
2554 }
2555 Ok(RawVcUnpacked::LocalOutput(..)) => {
2556 panic!("Non-local tasks must not return a local Vc");
2557 }
2558 Err(err) => {
2559 if let Some(OutputValue::Error(old_error)) = current_output
2560 && **old_error == err
2561 {
2562 None
2563 } else {
2564 Some(OutputValue::Error(Arc::new((&err).into())))
2565 }
2566 }
2567 };
2568 let mut output_dependent_tasks = SmallVec::<[_; 4]>::new();
2569 if new_output.is_some() && ctx.should_track_dependencies() {
2571 output_dependent_tasks = task.iter_output_dependent().collect();
2572 }
2573
2574 drop(task);
2575
2576 let mut is_now_immutable = false;
2578 if let Some(dependencies) = task_dependencies_for_immutable
2579 && dependencies
2580 .iter()
2581 .all(|&task_id| ctx.task(task_id, TaskDataCategory::Data).immutable())
2582 {
2583 is_now_immutable = true;
2584 }
2585 #[cfg(feature = "trace_task_details")]
2586 span.record("immutable", is_immutable || is_now_immutable);
2587
2588 if !queue.is_empty() || !old_edges.is_empty() {
2589 #[cfg(any(
2590 feature = "trace_task_completion",
2591 feature = "trace_aggregation_update_stats"
2592 ))]
2593 let _span = tracing::trace_span!(
2594 "remove old edges and prepare new children",
2595 stats = tracing::field::Empty
2596 )
2597 .entered();
2598 #[cfg(feature = "trace_aggregation_update_stats")]
2602 {
2603 let stats = cleanup_old_edges(task_id, old_edges, queue, ctx);
2604 _span.record("stats", tracing::field::debug(stats));
2605 }
2606 #[cfg(not(feature = "trace_aggregation_update_stats"))]
2607 cleanup_old_edges(task_id, old_edges, queue, ctx);
2608 }
2609
2610 Ok(TaskExecutionCompletePrepareResult {
2611 new_children,
2612 is_now_immutable,
2613 #[cfg(feature = "verify_determinism")]
2614 no_output_set,
2615 #[cfg(feature = "task_dirty_cause")]
2616 function_id,
2617 new_output,
2618 output_dependent_tasks,
2619 is_recomputation,
2620 is_session_dependent,
2621 })
2622 }
2623
2624 fn task_execution_completed_invalidate_output_dependent(
2625 &self,
2626 ctx: &mut ExecuteContext<'_>,
2627 task_id: TaskId,
2628 #[cfg(feature = "task_dirty_cause")] function_id: Option<FunctionId>,
2629 output_dependent_tasks: SmallVec<[TaskId; 4]>,
2630 ) {
2631 debug_assert!(!output_dependent_tasks.is_empty());
2632
2633 #[cfg(feature = "task_dirty_cause")]
2634 let cause = match function_id {
2635 Some(function) => TaskDirtyCause::OutputChange { function },
2636 None => TaskDirtyCause::RootOutputChange,
2637 };
2638
2639 if output_dependent_tasks.len() > 1 {
2640 ctx.prepare_tasks(
2641 output_dependent_tasks
2642 .iter()
2643 .map(|&id| (id, TaskDataCategory::All)),
2644 "invalidate output dependents",
2645 );
2646 }
2647
2648 fn process_output_dependents(
2649 ctx: &mut ExecuteContext<'_>,
2650 task_id: TaskId,
2651 #[cfg(feature = "task_dirty_cause")] cause: &TaskDirtyCause,
2652 dependent_task_id: TaskId,
2653 queue: &mut AggregationUpdateQueue,
2654 ) {
2655 #[cfg(feature = "trace_task_output_dependencies")]
2656 let span = tracing::trace_span!(
2657 "invalidate output dependency",
2658 task = %task_id,
2659 dependent_task = %dependent_task_id,
2660 result = tracing::field::Empty,
2661 )
2662 .entered();
2663 let mut make_stale = true;
2664 let mut dependent = ctx.task(dependent_task_id, TaskDataCategory::All);
2665 let transient_task_type = dependent.get_transient_task_type();
2666 if transient_task_type.is_some_and(|tt| matches!(&**tt, TransientTask::Once(_))) {
2667 #[cfg(feature = "trace_task_output_dependencies")]
2669 span.record("result", "once task");
2670 return;
2671 }
2672 if dependent.outdated_output_dependencies_contains(&task_id) {
2673 #[cfg(feature = "trace_task_output_dependencies")]
2674 span.record("result", "outdated dependency");
2675 make_stale = false;
2680 } else if !dependent.output_dependencies_contains(&task_id) {
2681 #[cfg(feature = "trace_task_output_dependencies")]
2684 span.record("result", "no backward dependency");
2685 return;
2686 }
2687 make_task_dirty_internal(
2688 &mut dependent,
2689 make_stale,
2690 #[cfg(feature = "task_dirty_cause")]
2691 cause.clone(),
2692 queue,
2693 ctx,
2694 );
2695 #[cfg(feature = "trace_task_output_dependencies")]
2696 span.record("result", "marked dirty");
2697 }
2698
2699 if output_dependent_tasks.len() > DEPENDENT_TASKS_DIRTY_PARALLELIZATION_THRESHOLD {
2700 let chunk_size = good_chunk_size(output_dependent_tasks.len());
2701 let chunks = into_chunks(output_dependent_tasks.to_vec(), chunk_size);
2702 let _ = scope_bounded(chunks.len(), |scope| {
2703 for chunk in chunks {
2704 let child_ctx = ctx.child_context();
2705 #[cfg(feature = "task_dirty_cause")]
2706 let cause = &cause;
2707 scope.spawn(move || {
2708 let mut ctx = child_ctx.create();
2709 let mut queue = AggregationUpdateQueue::new();
2710 for dependent_task_id in chunk {
2711 process_output_dependents(
2712 &mut ctx,
2713 task_id,
2714 #[cfg(feature = "task_dirty_cause")]
2715 cause,
2716 dependent_task_id,
2717 &mut queue,
2718 )
2719 }
2720 queue.execute(&mut ctx);
2721 });
2722 }
2723 });
2724 } else {
2725 let mut queue = AggregationUpdateQueue::new();
2726 for dependent_task_id in output_dependent_tasks {
2727 process_output_dependents(
2728 ctx,
2729 task_id,
2730 #[cfg(feature = "task_dirty_cause")]
2731 &cause,
2732 dependent_task_id,
2733 &mut queue,
2734 );
2735 }
2736 queue.execute(ctx);
2737 }
2738 }
2739
2740 fn task_execution_completed_connect(
2741 &self,
2742 ctx: &mut ExecuteContext<'_>,
2743 task_id: TaskId,
2744 new_children: FxHashSet<TaskId>,
2745 ) -> Option<TaskPriority> {
2746 debug_assert!(!new_children.is_empty());
2747
2748 let mut task = ctx.task(task_id, TaskDataCategory::All);
2749 let Some(in_progress) = task.get_in_progress() else {
2750 panic!("Task execution completed, but task is not in progress: {task:#?}");
2751 };
2752 if matches!(in_progress, InProgressState::Canceled) {
2753 return None;
2755 }
2756 let InProgressState::InProgress(InProgressStateInner {
2757 #[cfg(not(feature = "no_fast_stale"))]
2758 stale,
2759 once_task: is_once_task,
2760 ..
2761 }) = in_progress
2762 else {
2763 panic!("Task execution completed, but task is not in progress: {task:#?}");
2764 };
2765
2766 #[cfg(not(feature = "no_fast_stale"))]
2768 if *stale && !is_once_task {
2769 let stale_priority = compute_stale_priority(&task);
2770 let Some(InProgressState::InProgress(InProgressStateInner { done_event, .. })) =
2771 task.take_in_progress()
2772 else {
2773 unreachable!();
2774 };
2775 let old = task.set_in_progress(InProgressState::Scheduled {
2776 done_event,
2777 reason: TaskExecutionReason::Stale,
2778 });
2779 debug_assert!(old.is_none(), "InProgress already exists");
2780 drop(task);
2781
2782 AggregationUpdateQueue::run(
2785 AggregationUpdateJob::DecreaseActiveCounts {
2786 task_ids: new_children.into_iter().collect(),
2787 },
2788 ctx,
2789 );
2790 return Some(stale_priority);
2791 }
2792
2793 let has_active_count = ctx.should_track_activeness()
2794 && task
2795 .get_activeness()
2796 .is_some_and(|activeness| activeness.active_counter > 0);
2797 connect_children(
2798 ctx,
2799 task_id,
2800 task,
2801 new_children,
2802 has_active_count,
2803 ctx.should_track_activeness(),
2804 );
2805
2806 None
2807 }
2808
2809 #[allow(clippy::type_complexity)]
2810 fn task_execution_completed_finish(
2811 &self,
2812 ctx: &mut ExecuteContext<'_>,
2813 task_id: TaskId,
2814 #[cfg(feature = "verify_determinism")] no_output_set: bool,
2815 new_output: Option<OutputValue>,
2816 is_now_immutable: bool,
2817 is_session_dependent: bool,
2818 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2819 is_error: bool,
2820 is_recomputation: bool,
2821 ) -> (
2822 Option<TaskPriority>,
2823 Option<
2824 auto_hash_map::AutoMap<CellId, InProgressCellState, BuildHasherDefault<FxHasher>, 1>,
2825 >,
2826 Vec<SharedReference>,
2827 ) {
2828 let mut task = ctx.task(task_id, TaskDataCategory::All);
2829 let Some(in_progress) = task.take_in_progress() else {
2830 panic!("Task execution completed, but task is not in progress: {task:#?}");
2831 };
2832 if matches!(in_progress, InProgressState::Canceled) {
2833 let removed_data = Self::task_execution_completed_cleanup(
2835 &mut task,
2836 cell_counters,
2837 is_error,
2838 is_recomputation,
2839 );
2840 return (None, None, removed_data);
2841 }
2842 let InProgressState::InProgress(InProgressStateInner {
2843 done_event,
2844 once_task: is_once_task,
2845 stale,
2846 marked_as_completed: _,
2847 new_children,
2848 }) = in_progress
2849 else {
2850 panic!("Task execution completed, but task is not in progress: {task:#?}");
2851 };
2852 debug_assert!(new_children.is_empty());
2853
2854 if stale && !is_once_task {
2856 let stale_priority = compute_stale_priority(&task);
2857 let old = task.set_in_progress(InProgressState::Scheduled {
2858 done_event,
2859 reason: TaskExecutionReason::Stale,
2860 });
2861 debug_assert!(old.is_none(), "InProgress already exists");
2862 return (Some(stale_priority), None, Vec::new());
2863 }
2864
2865 let mut old_content = None;
2867 if let Some(value) = new_output {
2868 old_content = task.set_output(value);
2869 }
2870
2871 if is_now_immutable {
2874 task.set_immutable(true);
2875 }
2876
2877 let in_progress_cells = task.take_in_progress_cells();
2879 if let Some(ref cells) = in_progress_cells {
2880 for state in cells.values() {
2881 state.event.notify(usize::MAX);
2882 }
2883 }
2884
2885 let new_dirtyness = if is_session_dependent {
2887 Some(Dirtyness::SessionDependent)
2888 } else {
2889 None
2890 };
2891 #[cfg(feature = "verify_determinism")]
2892 let dirty_changed = task.get_dirty().cloned() != new_dirtyness;
2893 let data_update = task.update_dirty_state(new_dirtyness);
2894
2895 #[cfg(feature = "verify_determinism")]
2899 let stale_priority: Option<TaskPriority> =
2900 ((dirty_changed || no_output_set) && !task_id.is_transient() && !is_once_task)
2901 .then(TaskPriority::leaf);
2902 #[cfg(not(feature = "verify_determinism"))]
2903 let stale_priority: Option<TaskPriority> = None;
2904 let removed_data = if stale_priority.is_some() {
2905 let old = task.set_in_progress(InProgressState::Scheduled {
2906 done_event,
2907 reason: TaskExecutionReason::Stale,
2908 });
2909 debug_assert!(old.is_none(), "InProgress already exists");
2910 drop(task);
2911 Vec::new()
2912 } else {
2913 let removed_data = Self::task_execution_completed_cleanup(
2916 &mut task,
2917 cell_counters,
2918 is_error,
2919 is_recomputation,
2920 );
2921 drop(task);
2922
2923 done_event.notify(usize::MAX);
2925 removed_data
2926 };
2927
2928 drop(old_content);
2929
2930 if let Some(data_update) = data_update {
2931 AggregationUpdateQueue::run(data_update, ctx);
2932 }
2933
2934 (stale_priority, in_progress_cells, removed_data)
2936 }
2937
2938 fn task_execution_completed_cleanup(
2948 task: &mut TaskGuard<'_>,
2949 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2950 is_error: bool,
2951 is_recomputation: bool,
2952 ) -> Vec<SharedReference> {
2953 let mut removed_cell_data = Vec::new();
2954 if !is_error || is_recomputation {
2960 let to_remove: Vec<_> = task
2966 .iter_cell_data()
2967 .filter_map(|(cell, _)| {
2968 cell_counters
2969 .get(&cell.type_id())
2970 .is_none_or(|start_index| cell.index() >= *start_index)
2971 .then_some(*cell)
2972 })
2973 .collect();
2974 removed_cell_data.reserve_exact(to_remove.len());
2975 for cell in to_remove {
2976 if let Some(data) =
2977 task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence)
2978 {
2979 removed_cell_data.push(data);
2980 }
2981 }
2982 let to_remove_hash: Vec<_> = task
2984 .iter_cell_data_hash()
2985 .filter_map(|(cell, _)| {
2986 cell_counters
2987 .get(&cell.type_id())
2988 .is_none_or(|start_index| cell.index() >= *start_index)
2989 .then_some(*cell)
2990 })
2991 .collect();
2992 for cell in to_remove_hash {
2993 task.remove_cell_data_hash(&cell);
2994 }
2995 }
2996
2997 task.cleanup_after_execution();
2999
3000 removed_cell_data
3002 }
3003
3004 fn report_slow_settle_for_snapshot(
3007 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3008 slow: SlowSettle,
3009 ) {
3010 let SlowSettle {
3011 waited,
3012 active_at_start,
3013 active_now,
3014 } = slow;
3015 turbo_tasks.send_compilation_event(Arc::new(DiagnosticEvent::new(
3016 Severity::Warning,
3017 format!(
3018 "Writing to the filesystem cache has been waiting {}s for active operations to \
3019 finish ({active_at_start} active when it started, {active_now} active now)",
3020 waited.as_secs(),
3021 ),
3022 )));
3023 }
3024
3025 fn log_unrecoverable_persist_error() {
3028 eprintln!(
3029 "Persisting is disabled for this session due to an unrecoverable error. Stopping the \
3030 background persisting process."
3031 );
3032 }
3033
3034 fn run_backend_job<'a>(
3035 &'a self,
3036 job: TurboTasksBackendJob,
3037 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
3038 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
3039 Box::pin(async move {
3040 match job {
3041 TurboTasksBackendJob::Snapshot => {
3042 debug_assert!(self.should_persist());
3043
3044 static IDLE_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
3047 std::env::var("TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS")
3048 .ok()
3049 .and_then(|v| v.parse::<u64>().ok())
3050 .map(Duration::from_millis)
3051 .unwrap_or(Duration::from_secs(2))
3052 });
3053
3054 static MIN_SNAPSHOT_ACTIVE_TIME: LazyLock<Duration> = LazyLock::new(|| {
3062 std::env::var("TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS")
3063 .ok()
3064 .and_then(|v| v.parse::<u64>().ok())
3065 .map(Duration::from_millis)
3066 .unwrap_or(Duration::from_secs(1))
3067 });
3068
3069 let mut last_snapshot = self.start_time;
3070 let mut idle_start_listener = self.idle_start_event.listen();
3071 let mut idle_end_listener = self.idle_end_event.listen();
3072 let mut fresh_idle = true;
3075 let mut is_first = true;
3076 let mut eviction_control = EvictionControl::new(self.options.eviction_mode);
3079 let mut active_time = Stopwatch::new();
3084 'outer: loop {
3085 const FIRST_SNAPSHOT_WAIT: Duration = Duration::from_secs(300);
3086 const SNAPSHOT_INTERVAL: Duration = Duration::from_secs(120);
3087 let idle_timeout = *IDLE_TIMEOUT;
3088 let (time, mut reason) = if is_first {
3089 (FIRST_SNAPSHOT_WAIT, SnapshotReason::InitialSnapshotTimeout)
3090 } else {
3091 (SNAPSHOT_INTERVAL, SnapshotReason::RegularSnapshotInterval)
3092 };
3093
3094 if !turbo_tasks.is_idle() {
3097 active_time.start();
3098 }
3099
3100 let until = last_snapshot + time;
3101 if until > Instant::now() {
3102 let mut stop_listener = self.stopping_event.listen();
3103 if *self.stopping.read() {
3104 return;
3105 }
3106 let mut idle_time = if turbo_tasks.is_idle() && fresh_idle {
3107 Instant::now() + idle_timeout
3108 } else {
3109 far_future()
3110 };
3111 loop {
3112 tokio::select! {
3113 _ = &mut stop_listener => {
3114 return;
3115 },
3116 _ = &mut idle_start_listener => {
3117 active_time.stop();
3119 idle_time = Instant::now() + idle_timeout;
3120 idle_start_listener = self.idle_start_event.listen()
3121 },
3122 _ = &mut idle_end_listener => {
3123 active_time.start();
3125 idle_time = far_future();
3126 idle_end_listener = self.idle_end_event.listen()
3127 },
3128 _ = tokio::time::sleep_until(until) => {
3129 break;
3130 },
3131 _ = tokio::time::sleep_until(idle_time) => {
3132 if turbo_tasks.is_idle() {
3133 reason = SnapshotReason::IdleTimeout;
3134 break;
3135 }
3136 },
3137 }
3138 }
3139 }
3140
3141 if active_time.elapsed() < *MIN_SNAPSHOT_ACTIVE_TIME {
3148 fresh_idle = false;
3155 is_first = false;
3156 last_snapshot = Instant::now();
3157 continue 'outer;
3158 }
3159
3160 let background_span =
3164 tracing::info_span!(parent: None, "background snapshot");
3165 match self.snapshot_and_persist(background_span.id(), reason, turbo_tasks) {
3166 Err(err) => {
3167 eprintln!("Persisting failed: {err:?}");
3170 Self::log_unrecoverable_persist_error();
3171 return;
3172 }
3173 Ok(None) => {
3174 fresh_idle = true;
3179 continue 'outer;
3180 }
3181 Ok(Some((snapshot_start, new_data, _gc_outcome))) => {
3182 fresh_idle = new_data;
3184 is_first = false;
3185 last_snapshot = snapshot_start;
3186 active_time.reset();
3194
3195 macro_rules! check_idle_ended {
3199 () => {{
3200 tokio::select! {
3201 biased;
3202 _ = &mut idle_end_listener => {
3203 idle_end_listener = self.idle_end_event.listen();
3204 true
3205 },
3206 _ = std::future::ready(()) => false,
3207 }
3208 }};
3209 }
3210 let ran_eviction = if eviction_control.should_evict(new_data) {
3224 self.storage.evict_after_snapshot(background_span.id());
3229 true
3230 } else {
3231 false
3232 };
3233
3234 let mut ran_compaction = false;
3241 const MAX_IDLE_COMPACTION_PASSES: usize = 10;
3242 for _ in 0..MAX_IDLE_COMPACTION_PASSES {
3243 if check_idle_ended!() {
3244 continue 'outer;
3245 }
3246 let compact_span = tracing::info_span!(
3250 parent: background_span.id(),
3251 "compact database",
3252 stats = tracing::field::Empty,
3253 )
3254 .entered();
3255 match self.backing_storage.compact() {
3256 Ok(Some(stats)) => {
3257 compact_span.record("stats", display(stats));
3258 ran_compaction = true;
3259 }
3260 Ok(None) => break,
3261 Err(err) => {
3262 eprintln!("Compaction failed: {err:?}");
3263 if self.backing_storage.has_unrecoverable_write_error()
3264 {
3265 Self::log_unrecoverable_persist_error();
3266 return;
3267 }
3268 break;
3269 }
3270 }
3271 }
3272 if !check_idle_ended!()
3277 && (new_data || ran_compaction || ran_eviction)
3278 {
3279 TurboMalloc::collect(true);
3280 }
3281
3282 if ran_eviction {
3285 eviction_control.record_eviction();
3286 }
3287 }
3288 }
3289 }
3290 }
3291 }
3292 })
3293 }
3294
3295 fn try_read_own_task_cell(
3296 &self,
3297 task_id: TaskId,
3298 cell: CellId,
3299 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3300 ) -> Result<TypedCellContent> {
3301 let mut ctx = self.execute_context(turbo_tasks);
3302 let task = ctx.task(task_id, TaskDataCategory::Data);
3303 task.assert_not_deleted("try_read_own_task_cell");
3304 if let Some(content) = task.get_cell_data(&cell).cloned() {
3305 Ok(CellContent(Some(content)).into_typed(cell.type_id()))
3306 } else {
3307 Ok(CellContent(None).into_typed(cell.type_id()))
3308 }
3309 }
3310
3311 fn read_task_collectibles(
3312 &self,
3313 task_id: TaskId,
3314 collectible_type: TraitTypeId,
3315 reader_id: Option<TaskId>,
3316 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3317 ) -> AutoMap<RawVc, i32, BuildHasherDefault<FxHasher>, 1> {
3318 let mut ctx = self.execute_context(turbo_tasks);
3319 let mut collectibles = AutoMap::default();
3320 {
3321 let mut task = ctx.task(task_id, TaskDataCategory::All);
3322 task.assert_not_deleted("read_task_collectibles");
3323 if task
3324 .get_persistent_task_type()
3325 .is_some_and(|t| !t.native_fn.is_root)
3326 {
3327 drop(task);
3328 panic!(
3329 "Reading collectibles of non-root task {} (reader: {}). The `root` attribute \
3330 is missing on the task.",
3331 self.debug_get_task_description(task_id),
3332 reader_id.map_or_else(
3333 || "unknown".to_string(),
3334 |r| self.debug_get_task_description(r)
3335 )
3336 );
3337 }
3338 for (collectible, count) in task.iter_aggregated_collectibles() {
3339 if *count > 0 && collectible.collectible_type == collectible_type {
3340 *collectibles
3341 .entry(RawVc::task_cell(
3342 collectible.cell.task,
3343 collectible.cell.cell,
3344 ))
3345 .or_insert(0) += 1;
3346 }
3347 }
3348 for (&collectible, &count) in task.iter_collectibles() {
3349 if collectible.collectible_type == collectible_type {
3350 *collectibles
3351 .entry(RawVc::task_cell(
3352 collectible.cell.task,
3353 collectible.cell.cell,
3354 ))
3355 .or_insert(0) += count;
3356 }
3357 }
3358 if let Some(reader_id) = reader_id {
3359 let _ = task.add_collectibles_dependents((collectible_type, reader_id));
3360 }
3361 }
3362 if let Some(reader_id) = reader_id {
3363 let mut reader = ctx.task(reader_id, TaskDataCategory::Data);
3364 let target = CollectiblesRef {
3365 task: task_id,
3366 collectible_type,
3367 };
3368 if !reader.remove_outdated_collectibles_dependencies(&target) {
3369 let _ = reader.add_collectibles_dependencies(target);
3370 }
3371 }
3372 collectibles
3373 }
3374
3375 fn emit_collectible(
3376 &self,
3377 collectible_type: TraitTypeId,
3378 collectible: RawVc,
3379 task_id: TaskId,
3380 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3381 ) {
3382 self.assert_valid_collectible(task_id, collectible);
3383
3384 let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3385 panic!("Collectibles need to be resolved");
3386 };
3387 let cell = CellRef {
3388 task: collectible_task,
3389 cell,
3390 };
3391 operation::UpdateCollectibleOperation::run(
3392 task_id,
3393 CollectibleRef {
3394 collectible_type,
3395 cell,
3396 },
3397 1,
3398 self.execute_context(turbo_tasks),
3399 );
3400 }
3401
3402 fn unemit_collectible(
3403 &self,
3404 collectible_type: TraitTypeId,
3405 collectible: RawVc,
3406 count: u32,
3407 task_id: TaskId,
3408 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3409 ) {
3410 self.assert_valid_collectible(task_id, collectible);
3411
3412 let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3413 panic!("Collectibles need to be resolved");
3414 };
3415 let cell = CellRef {
3416 task: collectible_task,
3417 cell,
3418 };
3419 operation::UpdateCollectibleOperation::run(
3420 task_id,
3421 CollectibleRef {
3422 collectible_type,
3423 cell,
3424 },
3425 -(i32::try_from(count).unwrap()),
3426 self.execute_context(turbo_tasks),
3427 );
3428 }
3429
3430 fn unemit_collectibles(
3431 &self,
3432 collectible_type: TraitTypeId,
3433 collectibles: &TaskCollectiblesMap,
3434 task_id: TaskId,
3435 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3436 ) {
3437 let updates: SmallVec<[_; 1]> = collectibles
3438 .iter()
3439 .filter(|&(_, &count)| count > 0)
3440 .map(|(&collectible, &count)| {
3441 self.assert_valid_collectible(task_id, collectible);
3442 let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3443 panic!("Collectibles need to be resolved");
3444 };
3445 (
3446 CollectibleRef {
3447 collectible_type,
3448 cell: CellRef {
3449 task: collectible_task,
3450 cell,
3451 },
3452 },
3453 -count,
3454 )
3455 })
3456 .collect();
3457 operation::UpdateCollectibleOperation::run_batch(
3458 task_id,
3459 updates,
3460 self.execute_context(turbo_tasks),
3461 );
3462 }
3463
3464 fn update_task_cell(
3465 &self,
3466 task_id: TaskId,
3467 cell: CellId,
3468 content: CellContent,
3469 updated_key_hashes: Option<SmallVec<[u64; 2]>>,
3470 content_hash: Option<CellHash>,
3471 verification_mode: VerificationMode,
3472 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3473 ) {
3474 update_cell(
3475 task_id,
3476 cell,
3477 content,
3478 updated_key_hashes,
3479 content_hash,
3480 verification_mode,
3481 self.execute_context(turbo_tasks),
3482 );
3483 }
3484
3485 fn mark_own_task_as_finished(&self, task: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
3486 let mut ctx = self.execute_context(turbo_tasks);
3487 let mut task = ctx.task(task, TaskDataCategory::Data);
3488 if let Some(InProgressState::InProgress(InProgressStateInner {
3489 marked_as_completed,
3490 ..
3491 })) = task.get_in_progress_mut()
3492 {
3493 *marked_as_completed = true;
3494 }
3499 }
3500
3501 fn connect_task(
3502 &self,
3503 task: TaskId,
3504 parent_task: Option<TaskId>,
3505 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3506 ) {
3507 self.assert_not_persistent_calling_transient(parent_task, task);
3508 let mut ctx = self.execute_context(turbo_tasks);
3509 if ctx.try_task(task, TaskDataCategory::Meta).is_none() {
3515 panic!(
3516 "connect_task: task {task} no longer exists (it was garbage collected). An \
3517 `OperationVc` to it was held without a pin; hold it in a `GcRoot` or connect it \
3518 from its parent task."
3519 );
3520 }
3521 connect_child(
3522 parent_task,
3523 task,
3524 false,
3525 ctx,
3526 );
3527 }
3528
3529 fn create_transient_task(&self, task_type: TransientTaskType) -> TaskId {
3530 let task_id = self.transient_task_id_factory.get();
3531 {
3532 let mut task = self.storage.access_mut(task_id);
3533 task.init_transient_task(task_id, task_type, self.should_track_activeness());
3534 }
3535 #[cfg(feature = "verify_aggregation_graph")]
3536 self.root_tasks.lock().insert(task_id);
3537 task_id
3538 }
3539
3540 fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
3541 let Some(mut ctx) = self.try_execute_context(turbo_tasks) else {
3546 return;
3547 };
3548
3549 #[cfg(feature = "verify_aggregation_graph")]
3550 self.root_tasks.lock().remove(&task_id);
3551
3552 let mut task = ctx.task(task_id, TaskDataCategory::All);
3553 let is_dirty = task.is_dirty();
3554 let has_dirty_containers = task.has_dirty_containers();
3555 if is_dirty.is_some() || has_dirty_containers {
3556 if let Some(activeness_state) = task.get_activeness_mut() {
3557 activeness_state.unset_root_type();
3559 activeness_state.set_active_until_clean();
3560 };
3561 } else {
3562 if let Some(activeness_state) = task.take_activeness() {
3563 activeness_state.all_clean_event.notify(usize::MAX);
3566 }
3567 let old_edges = capture_all_edges(&task);
3569 drop(task);
3570
3571 if !old_edges.is_empty() {
3572 cleanup_old_edges(task_id, old_edges, AggregationUpdateQueue::new(), &mut ctx);
3573 }
3574 }
3575 }
3576
3577 #[cfg(feature = "verify_aggregation_graph")]
3578 fn verify_aggregation_graph(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>, idle: bool) {
3579 if env::var("TURBO_ENGINE_VERIFY_GRAPH").ok().as_deref() == Some("0") {
3580 return;
3581 }
3582 use std::{collections::VecDeque, env, io::stdout};
3583
3584 use crate::backend::operation::{get_uppers, is_aggregating_node};
3585
3586 let mut ctx = self.execute_context(turbo_tasks);
3587 let root_tasks = self.root_tasks.lock().clone();
3588
3589 for task_id in root_tasks.into_iter() {
3590 let mut queue = VecDeque::new();
3591 let mut visited = FxHashSet::default();
3592 let mut aggregated_nodes = FxHashSet::default();
3593 let mut collectibles = FxHashMap::default();
3594 let root_task_id = task_id;
3595 visited.insert(task_id);
3596 aggregated_nodes.insert(task_id);
3597 queue.push_back(task_id);
3598 let mut counter = 0;
3599 while let Some(task_id) = queue.pop_front() {
3600 counter += 1;
3601 if counter % 100000 == 0 {
3602 println!(
3603 "queue={}, visited={}, aggregated_nodes={}",
3604 queue.len(),
3605 visited.len(),
3606 aggregated_nodes.len()
3607 );
3608 }
3609 let task = ctx.task(task_id, TaskDataCategory::All);
3610 if idle && !self.is_idle.load(Ordering::Relaxed) {
3611 return;
3612 }
3613
3614 let uppers = get_uppers(&task);
3615 if task_id != root_task_id
3616 && !uppers.iter().any(|upper| aggregated_nodes.contains(upper))
3617 {
3618 panic!(
3619 "Task {} {} doesn't report to any root but is reachable from one (uppers: \
3620 {:?})",
3621 task_id,
3622 task.get_task_description(),
3623 uppers
3624 );
3625 }
3626
3627 for (collectible, _) in task.iter_aggregated_collectibles() {
3628 collectibles
3629 .entry(*collectible)
3630 .or_insert_with(|| (false, Vec::new()))
3631 .1
3632 .push(task_id);
3633 }
3634
3635 for (&collectible, &value) in task.iter_collectibles() {
3636 if value > 0 {
3637 if let Some((flag, _)) = collectibles.get_mut(&collectible) {
3638 *flag = true
3639 } else {
3640 panic!(
3641 "Task {} has a collectible {:?} that is not in any upper task",
3642 task_id, collectible
3643 );
3644 }
3645 }
3646 }
3647
3648 let is_dirty = task.has_dirty();
3649 let has_dirty_container = task.has_dirty_containers();
3650 let should_be_in_upper = is_dirty || has_dirty_container;
3651
3652 let aggregation_number = get_aggregation_number(&task);
3653 if is_aggregating_node(aggregation_number) {
3654 aggregated_nodes.insert(task_id);
3655 }
3656 for child_id in task.iter_children() {
3663 if visited.insert(child_id) {
3665 queue.push_back(child_id);
3666 }
3667 }
3668 drop(task);
3669
3670 if should_be_in_upper {
3671 for upper_id in uppers {
3672 let upper = ctx.task(upper_id, TaskDataCategory::All);
3673 let in_upper = upper
3674 .get_aggregated_dirty_containers(&task_id)
3675 .is_some_and(|&dirty| dirty > 0);
3676 if !in_upper {
3677 let containers: Vec<_> = upper
3678 .iter_aggregated_dirty_containers()
3679 .map(|(&k, &v)| (k, v))
3680 .collect();
3681 let upper_task_desc = upper.get_task_description();
3682 drop(upper);
3683 panic!(
3684 "Task {} ({}) is dirty, but is not listed in the upper task {} \
3685 ({})\nThese dirty containers are present:\n{:#?}",
3686 task_id,
3687 ctx.task(task_id, TaskDataCategory::Data)
3688 .get_task_description(),
3689 upper_id,
3690 upper_task_desc,
3691 containers,
3692 );
3693 }
3694 }
3695 }
3696 }
3697
3698 for (collectible, (flag, task_ids)) in collectibles {
3699 if !flag {
3700 use std::io::Write;
3701 let mut stdout = stdout().lock();
3702 writeln!(
3703 stdout,
3704 "{:?} that is not emitted in any child task but in these aggregated \
3705 tasks: {:#?}",
3706 collectible,
3707 task_ids
3708 .iter()
3709 .map(|t| format!(
3710 "{t} {}",
3711 ctx.task(*t, TaskDataCategory::Data).get_task_description()
3712 ))
3713 .collect::<Vec<_>>()
3714 )
3715 .unwrap();
3716
3717 let task_id = collectible.cell.task;
3718 let mut queue = {
3719 let task = ctx.task(task_id, TaskDataCategory::All);
3720 get_uppers(&task)
3721 };
3722 let mut visited = FxHashSet::default();
3723 for &upper_id in queue.iter() {
3724 visited.insert(upper_id);
3725 writeln!(stdout, "{task_id:?} -> {upper_id:?}").unwrap();
3726 }
3727 while let Some(task_id) = queue.pop() {
3728 let task = ctx.task(task_id, TaskDataCategory::All);
3729 let desc = task.get_task_description();
3730 let aggregated_collectible = task
3731 .get_aggregated_collectibles(&collectible)
3732 .copied()
3733 .unwrap_or_default();
3734 let uppers = get_uppers(&task);
3735 drop(task);
3736 writeln!(
3737 stdout,
3738 "upper {task_id} {desc} collectible={aggregated_collectible}"
3739 )
3740 .unwrap();
3741 if task_ids.contains(&task_id) {
3742 writeln!(
3743 stdout,
3744 "Task has an upper connection to an aggregated task that doesn't \
3745 reference it. Upper connection is invalid!"
3746 )
3747 .unwrap();
3748 }
3749 for upper_id in uppers {
3750 writeln!(stdout, "{task_id:?} -> {upper_id:?}").unwrap();
3751 if !visited.contains(&upper_id) {
3752 queue.push(upper_id);
3753 }
3754 }
3755 }
3756 panic!("See stdout for more details");
3757 }
3758 }
3759 }
3760 }
3761
3762 fn assert_not_persistent_calling_transient(&self, parent_id: Option<TaskId>, child_id: TaskId) {
3763 if let Some(parent_id) = parent_id
3764 && !parent_id.is_transient()
3765 && child_id.is_transient()
3766 {
3767 self.panic_persistent_calling_transient(
3768 self.debug_get_task_description(parent_id),
3769 self.debug_get_cached_task_type(child_id).as_deref(),
3770 );
3771 }
3772 }
3773
3774 fn panic_persistent_calling_transient(
3775 &self,
3776 parent: String,
3777 child: Option<&CachedTaskType>,
3778 ) -> ! {
3779 panic!(
3780 "Persistent task {} is not allowed to call, read, or connect to transient task {}.",
3781 parent,
3782 child.map_or("unknown", |t| t.get_name()),
3783 );
3784 }
3785
3786 fn assert_valid_collectible(&self, task_id: TaskId, collectible: RawVc) {
3787 let Some((col_task_id, _)) = collectible.as_task_cell() else {
3789 let task_info = if let Some(col_task_ty) = collectible
3791 .try_get_task_id()
3792 .map(|t| self.debug_get_task_description(t))
3793 {
3794 Cow::Owned(format!(" (return type of {col_task_ty})"))
3795 } else {
3796 Cow::Borrowed("")
3797 };
3798 panic!("Collectible{task_info} must be a ResolvedVc")
3799 };
3800 if col_task_id.is_transient() && !task_id.is_transient() {
3801 panic!(
3803 "Collectible is transient; transient collectibles cannot be emitted from \
3804 persistent tasks"
3805 )
3806 }
3807 }
3808}
3809
3810impl Backend for TurboTasksBackend {
3811 fn startup(&self, turbo_tasks: &TurboTasks<Self>) {
3812 self.startup(turbo_tasks);
3813 }
3814
3815 fn stopping(&self, _turbo_tasks: &TurboTasks<Self>) {
3816 self.stopping();
3817 }
3818
3819 fn stop(&self, turbo_tasks: &TurboTasks<Self>) {
3820 self.stop(turbo_tasks);
3821 }
3822
3823 fn idle_start(&self, turbo_tasks: &TurboTasks<Self>) {
3824 self.idle_start(turbo_tasks);
3825 }
3826
3827 fn idle_end(&self, _turbo_tasks: &TurboTasks<Self>) {
3828 self.idle_end();
3829 }
3830
3831 fn get_or_create_task(
3832 &self,
3833 native_fn: &'static NativeFunction,
3834 this: Option<RawVc>,
3835 arg: &mut dyn DynTaskInputsStorage,
3836 parent_task: Option<TaskId>,
3837 persistence: TaskPersistence,
3838 turbo_tasks: &TurboTasks<Self>,
3839 ) -> TaskId {
3840 self.get_or_create_task(native_fn, this, arg, parent_task, persistence, turbo_tasks)
3841 }
3842
3843 fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3844 self.invalidate_task(task_id, turbo_tasks);
3845 }
3846
3847 fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks<Self>) {
3848 self.invalidate_tasks(tasks, turbo_tasks);
3849 }
3850
3851 fn invalidate_tasks_set(
3852 &self,
3853 tasks: &AutoSet<TaskId, BuildHasherDefault<FxHasher>, 2>,
3854 turbo_tasks: &TurboTasks<Self>,
3855 ) {
3856 self.invalidate_tasks_set(tasks, turbo_tasks);
3857 }
3858
3859 fn invalidate_serialization(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3860 self.invalidate_serialization(task_id, turbo_tasks);
3861 }
3862
3863 fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
3864 self.task_execution_canceled(task, turbo_tasks)
3865 }
3866
3867 fn try_start_task_execution(
3868 &self,
3869 task_id: TaskId,
3870 priority: TaskPriority,
3871 turbo_tasks: &TurboTasks<Self>,
3872 ) -> Option<TaskExecutionSpec<'_>> {
3873 self.try_start_task_execution(task_id, priority, turbo_tasks)
3874 }
3875
3876 fn task_execution_completed(
3877 &self,
3878 task_id: TaskId,
3879 result: Result<RawVc, TurboTasksExecutionError>,
3880 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
3881 #[cfg(feature = "verify_determinism")] stateful: bool,
3882 has_invalidator: bool,
3883 turbo_tasks: &TurboTasks<Self>,
3884 ) -> Option<TaskPriority> {
3885 self.task_execution_completed(
3886 task_id,
3887 result,
3888 cell_counters,
3889 #[cfg(feature = "verify_determinism")]
3890 stateful,
3891 has_invalidator,
3892 turbo_tasks,
3893 )
3894 }
3895
3896 type BackendJob = TurboTasksBackendJob;
3897
3898 fn run_backend_job<'a>(
3899 &'a self,
3900 job: Self::BackendJob,
3901 turbo_tasks: &'a TurboTasks<Self>,
3902 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
3903 self.run_backend_job(job, turbo_tasks)
3904 }
3905
3906 fn try_read_task_output(
3907 &self,
3908 task_id: TaskId,
3909 reader: Option<TaskId>,
3910 options: ReadOutputOptions,
3911 turbo_tasks: &TurboTasks<Self>,
3912 ) -> Result<ReadOutcome<RawVc>> {
3913 self.try_read_task_output(task_id, reader, options, turbo_tasks)
3914 }
3915
3916 fn try_read_task_cell(
3917 &self,
3918 task_id: TaskId,
3919 cell: CellId,
3920 reader: Option<TaskId>,
3921 options: ReadCellOptions,
3922 turbo_tasks: &TurboTasks<Self>,
3923 ) -> Result<ReadOutcome<TypedCellContent>> {
3924 self.try_read_task_cell(task_id, reader, cell, options, turbo_tasks)
3925 }
3926
3927 fn try_read_own_task_cell(
3928 &self,
3929 task_id: TaskId,
3930 cell: CellId,
3931 turbo_tasks: &TurboTasks<Self>,
3932 ) -> Result<TypedCellContent> {
3933 self.try_read_own_task_cell(task_id, cell, turbo_tasks)
3934 }
3935
3936 fn read_task_collectibles(
3937 &self,
3938 task_id: TaskId,
3939 collectible_type: TraitTypeId,
3940 reader: Option<TaskId>,
3941 turbo_tasks: &TurboTasks<Self>,
3942 ) -> AutoMap<RawVc, i32, BuildHasherDefault<FxHasher>, 1> {
3943 self.read_task_collectibles(task_id, collectible_type, reader, turbo_tasks)
3944 }
3945
3946 fn emit_collectible(
3947 &self,
3948 collectible_type: TraitTypeId,
3949 collectible: RawVc,
3950 task_id: TaskId,
3951 turbo_tasks: &TurboTasks<Self>,
3952 ) {
3953 self.emit_collectible(collectible_type, collectible, task_id, turbo_tasks)
3954 }
3955
3956 fn unemit_collectible(
3957 &self,
3958 collectible_type: TraitTypeId,
3959 collectible: RawVc,
3960 count: u32,
3961 task_id: TaskId,
3962 turbo_tasks: &TurboTasks<Self>,
3963 ) {
3964 self.unemit_collectible(collectible_type, collectible, count, task_id, turbo_tasks)
3965 }
3966
3967 fn unemit_collectibles(
3968 &self,
3969 collectible_type: TraitTypeId,
3970 collectibles: &TaskCollectiblesMap,
3971 task_id: TaskId,
3972 turbo_tasks: &TurboTasks<Self>,
3973 ) {
3974 self.unemit_collectibles(collectible_type, collectibles, task_id, turbo_tasks)
3975 }
3976
3977 fn update_task_cell(
3978 &self,
3979 task_id: TaskId,
3980 cell: CellId,
3981 content: CellContent,
3982 updated_key_hashes: Option<SmallVec<[u64; 2]>>,
3983 content_hash: Option<CellHash>,
3984 verification_mode: VerificationMode,
3985 turbo_tasks: &TurboTasks<Self>,
3986 ) {
3987 self.update_task_cell(
3988 task_id,
3989 cell,
3990 content,
3991 updated_key_hashes,
3992 content_hash,
3993 verification_mode,
3994 turbo_tasks,
3995 );
3996 }
3997
3998 fn mark_own_task_as_finished(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3999 self.mark_own_task_as_finished(task_id, turbo_tasks);
4000 }
4001
4002 fn pin_task_for_gc(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
4003 self.pin_task_for_gc(task, turbo_tasks);
4004 }
4005
4006 fn unpin_task_for_gc(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
4007 self.unpin_task_for_gc(task, turbo_tasks);
4008 }
4009
4010 fn connect_task(
4011 &self,
4012 task: TaskId,
4013 parent_task: Option<TaskId>,
4014 turbo_tasks: &TurboTasks<Self>,
4015 ) {
4016 self.connect_task(task, parent_task, turbo_tasks);
4017 }
4018
4019 fn create_transient_task(
4020 &self,
4021 task_type: TransientTaskType,
4022 _turbo_tasks: &TurboTasks<Self>,
4023 ) -> TaskId {
4024 self.create_transient_task(task_type)
4025 }
4026
4027 fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
4028 self.dispose_root_task(task_id, turbo_tasks);
4029 }
4030
4031 fn task_statistics(&self) -> &TaskStatisticsApi {
4032 &self.task_statistics
4033 }
4034
4035 fn is_tracking_dependencies(&self) -> bool {
4036 self.options.dependency_tracking
4037 }
4038
4039 fn get_task_name(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) -> String {
4040 self.get_task_name(task, turbo_tasks)
4041 }
4042}
4043
4044fn far_future() -> Instant {
4046 Instant::now() + Duration::from_secs(86400 * 365 * 30)
4051}
4052
4053#[cfg(test)]
4054mod persist_failure_tests {
4055 use turbo_tasks::{TaskId, TurboTasks};
4056
4057 use crate::{
4058 backend::{
4059 BackendOptions, LastPersistenceStatus, SnapshotReason, StorageMode, TurboTasksBackend,
4060 storage::SpecificTaskDataCategory,
4061 },
4062 noop_backing_storage,
4063 };
4064
4065 #[tokio::test(flavor = "multi_thread")]
4068 async fn failed_persist_disables_persisting() {
4069 let tt = TurboTasks::new(TurboTasksBackend::new(
4070 BackendOptions {
4071 storage_mode: Some(StorageMode::ReadWrite),
4072 num_workers: Some(1),
4073 small_preallocation: true,
4074 ..Default::default()
4075 },
4076 noop_backing_storage(),
4077 ));
4078 let backend = tt.backend();
4079 let modify = |id: u32| {
4080 let task_id = TaskId::new(id).unwrap();
4081 let mut task = backend.storage.access_mut(task_id);
4082 let _ = task.track_modification(SpecificTaskDataCategory::Data, "test");
4083 task_id
4084 };
4085
4086 let status = || *backend.snapshot_in_progress.lock();
4087
4088 modify(1);
4089 assert_eq!(status(), LastPersistenceStatus::Ok);
4090 let _ = backend.snapshot_and_evict_for_testing(&tt);
4091 assert_eq!(status(), LastPersistenceStatus::Failed);
4092
4093 let task_id = modify(2);
4094 let result = backend
4095 .snapshot_and_persist(None, SnapshotReason::Test, &tt)
4096 .expect("a disabled persist is skipped, not an error");
4097 assert!(result.is_none(), "persisting must stay disabled");
4098 assert_eq!(status(), LastPersistenceStatus::Failed);
4099 let task = backend.storage.access_mut(task_id);
4100 assert!(
4101 task.flags.data_modified(),
4102 "the second snapshot must not start"
4103 );
4104 assert!(!task.flags.any_snapshot_pending());
4105 }
4106}