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::{Context, Result, bail};
26use auto_hash_map::{AutoMap, AutoSet};
27use gc::DEFAULT_GC_ROOT_TTL;
28pub use gc::{GcPassResult, GcStats, TtlCounter};
29use hashbrown::hash_table::Entry;
30use parking_lot::{Mutex, RwLock};
31use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
32use smallvec::{SmallVec, smallvec};
33use tokio::time::{Duration, Instant};
34use tracing::{Span, field::display, trace_span};
35use turbo_bincode::{TurboBincodeBuffer, new_turbo_bincode_decoder, new_turbo_bincode_encoder};
36use turbo_tasks::{
37 CellId, DynTaskInputsStorage, RawVc, RawVcUnpacked, ReadCellOptions, ReadCellTracking,
38 ReadConsistency, ReadOutcome, ReadOutputOptions, ReadTracking, SharedReference,
39 TRANSIENT_TASK_BIT, TaskExecutionReason, TaskId, TaskPersistence, TaskPriority, TraitTypeId,
40 TurboTasks, TurboTasksCallApi, TurboTasksPanic, ValueTypeId,
41 backend::{
42 Backend, CachedTaskType, CachedTaskTypeArc, CellContent, CellHash, TaskExecutionSpec,
43 TransientTaskType, TurboTaskContextError, TurboTaskLocalContextError, TurboTasksError,
44 TurboTasksExecutionError, TurboTasksExecutionErrorMessage, TypedCellContent,
45 VerificationMode,
46 },
47 event::{Event, EventDescription, EventListener},
48 macro_helpers::NativeFunction,
49 message_queue::{TimingEvent, TraceEvent},
50 registry::get_value_type,
51 scope_bounded::scope_bounded,
52 task_statistics::TaskStatisticsApi,
53 util::{IdFactoryWithReuse, good_chunk_size, into_chunks},
54};
55#[cfg(feature = "task_dirty_cause")]
56use turbo_tasks::{FunctionId, TaskDirtyCause};
57use turbo_tasks_malloc::TurboMalloc;
58
59use self::eviction::EvictionControl;
60pub use self::{
61 eviction::EvictionMode,
62 operation::AnyOperation,
63 storage::{EvictionCounts, SpecificTaskDataCategory, TaskDataCategory},
64};
65use crate::{
66 backend::{
67 operation::{
68 AggregationUpdateJob, AggregationUpdateQueue, ChildExecuteContext,
69 CleanupOldEdgesOperation, ConnectChildOperation, ExecuteContext, ExecuteContextImpl,
70 LeafDistanceUpdateQueue, Operation, OutdatedEdge, TaskGuard, TaskType, TaskTypeRef,
71 capture_all_edges, connect_children, get_aggregation_number, get_uppers,
72 make_task_dirty_internal, prepare_new_children,
73 },
74 snapshot_coordinator::{OperationGuard, SnapshotCoordinator},
75 storage::Storage,
76 storage_schema::{TaskStorage, TaskStorageAccessors},
77 },
78 backing_storage::{SnapshotItem, compute_task_type_hash},
79 data::{
80 ActivenessState, CellRef, CollectibleRef, CollectiblesRef, Dirtyness, InProgressCellState,
81 InProgressState, InProgressStateInner, OutputValue, TransientTask,
82 },
83 error::{TaskError, TaskErrorItem},
84 kv_backing_storage::TurboBackingStorage,
85 utils::{
86 dash_map_entry::{get_in_shard, get_shard, with_entry_in_shard},
87 shard_amount::compute_shard_amount,
88 stopwatch::Stopwatch,
89 },
90};
91
92const DEPENDENT_TASKS_DIRTY_PARALLELIZATION_THRESHOLD: usize = 10000;
96
97const GC_MIN_PROGRESS: Duration = Duration::from_millis(100);
101
102fn compute_stale_priority(task: &impl TaskGuard) -> TaskPriority {
109 TaskPriority::invalidation(
110 task.get_leaf_distance()
111 .copied()
112 .unwrap_or_default()
113 .distance,
114 )
115 .in_parent(task.is_dirty().unwrap_or(TaskPriority::leaf()))
116}
117
118#[derive(PartialEq, Eq)]
119pub enum StorageMode {
120 ReadOnly,
122 ReadWrite,
125 ReadWriteOnShutdown,
128}
129
130pub struct BackendOptions {
131 pub dependency_tracking: bool,
136
137 pub active_tracking: bool,
143
144 pub storage_mode: Option<StorageMode>,
146
147 pub num_workers: Option<usize>,
150
151 pub small_preallocation: bool,
153
154 pub eviction_mode: EvictionMode,
157
158 pub gc: Option<bool>,
162
163 pub gc_root_ttl: Option<Duration>,
166
167 pub gc_min_progress: Option<Duration>,
170}
171
172impl Default for BackendOptions {
173 fn default() -> Self {
174 Self {
175 dependency_tracking: true,
176 active_tracking: true,
177 storage_mode: Some(StorageMode::ReadWrite),
178 num_workers: None,
179 small_preallocation: false,
180 eviction_mode: EvictionMode::Off,
181 gc: None,
182 gc_root_ttl: None,
183 gc_min_progress: None,
184 }
185 }
186}
187
188pub enum TurboTasksBackendJob {
189 Snapshot,
190}
191
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194enum SnapshotReason {
195 Test,
196 Stop,
197 InitialSnapshotTimeout,
198 RegularSnapshotInterval,
199 IdleTimeout,
200}
201
202impl SnapshotReason {
203 fn as_str(self) -> &'static str {
204 match self {
205 SnapshotReason::Test => "test",
206 SnapshotReason::Stop => "stop",
207 SnapshotReason::InitialSnapshotTimeout => "initial snapshot timeout",
208 SnapshotReason::RegularSnapshotInterval => "regular snapshot interval",
209 SnapshotReason::IdleTimeout => "idle timeout",
210 }
211 }
212
213 fn gc_is_interruptible(self) -> bool {
215 matches!(self, SnapshotReason::IdleTimeout | SnapshotReason::Test)
216 }
217
218 fn drain_entries(self) -> bool {
222 matches!(self, SnapshotReason::Stop)
223 }
224}
225
226pub struct TurboTasksBackend {
227 options: BackendOptions,
228
229 start_time: Instant,
230
231 persisted_task_id_factory: IdFactoryWithReuse<TaskId>,
232 transient_task_id_factory: IdFactoryWithReuse<TaskId>,
233
234 storage: Storage,
235
236 snapshot_coord: SnapshotCoordinator,
239 snapshot_in_progress: Mutex<()>,
244
245 gc_enabled: bool,
247
248 stopping: RwLock<bool>,
249 stopping_event: Event,
250 idle_start_event: Event,
251 idle_end_event: Event,
252 #[cfg(feature = "verify_aggregation_graph")]
253 is_idle: AtomicBool,
254
255 task_statistics: TaskStatisticsApi,
256
257 backing_storage: TurboBackingStorage,
258 gc_root_ttl: Duration,
260
261 gc_min_progress: Duration,
263
264 #[cfg(feature = "verify_aggregation_graph")]
265 root_tasks: Mutex<FxHashSet<TaskId>>,
266}
267
268#[doc(hidden)]
270#[derive(Default)]
271pub struct TestSnapshotOutcome {
272 pub had_new_data: bool,
274 pub eviction_counts: EvictionCounts,
276 pub gc: Option<(GcStats, GcPassResult)>,
279}
280
281impl TestSnapshotOutcome {
282 pub fn gc_stats(&self) -> &GcStats {
284 &self
285 .gc
286 .as_ref()
287 .expect("no GC pass ran: the backend needs `BackendOptions::gc = Some(true)`")
288 .0
289 }
290
291 pub fn gc_interrupted(&self) -> bool {
293 self.gc
294 .as_ref()
295 .is_some_and(|(_, result)| result.interrupted)
296 }
297}
298
299impl TurboTasksBackend {
300 pub fn invalidate_storage(&self, reason_code: &str) -> Result<()> {
306 self.backing_storage.invalidate(reason_code)
307 }
308
309 pub fn new(mut options: BackendOptions, backing_storage: TurboBackingStorage) -> Self {
310 let shard_amount = compute_shard_amount(options.num_workers, options.small_preallocation);
311 if !options.dependency_tracking {
312 options.active_tracking = false;
313 }
314 let small_preallocation = options.small_preallocation;
315 let gc_root_ttl = options.gc_root_ttl.unwrap_or(DEFAULT_GC_ROOT_TTL);
316 let gc_min_progress = options.gc_min_progress.unwrap_or(GC_MIN_PROGRESS);
317 let next_task_id = backing_storage
318 .next_free_task_id()
319 .expect("Failed to get task id");
320
321 let mut gc_enabled = options.gc.unwrap_or(false);
322 if gc_enabled
323 && options.storage_mode == Some(StorageMode::ReadWrite)
324 && options.eviction_mode == EvictionMode::Off
325 {
326 eprintln!(
327 "warning: GC is enabled but eviction is disabled; GC would leave collected tasks \
328 resident forever. Forcing GC off. Enable eviction ('auto'/'full') to use GC in \
329 this mode."
330 );
331 gc_enabled = false;
332 }
333
334 Self {
335 options,
336 gc_enabled,
337 start_time: Instant::now(),
338 persisted_task_id_factory: IdFactoryWithReuse::new(
339 next_task_id,
340 TaskId::try_from(TRANSIENT_TASK_BIT - 1).unwrap(),
341 ),
342 transient_task_id_factory: IdFactoryWithReuse::new(
343 TaskId::try_from(TRANSIENT_TASK_BIT).unwrap(),
344 TaskId::MAX,
345 ),
346 storage: Storage::new(shard_amount, small_preallocation),
347 snapshot_coord: SnapshotCoordinator::new(),
348 snapshot_in_progress: Mutex::new(()),
349 stopping: RwLock::new(false),
350 stopping_event: Event::new(|| || "TurboTasksBackend::stopping_event".to_string()),
351 idle_start_event: Event::new(|| || "TurboTasksBackend::idle_start_event".to_string()),
352 idle_end_event: Event::new(|| || "TurboTasksBackend::idle_end_event".to_string()),
353 #[cfg(feature = "verify_aggregation_graph")]
354 is_idle: AtomicBool::new(false),
355 task_statistics: TaskStatisticsApi::default(),
356 backing_storage,
357 gc_root_ttl,
358 gc_min_progress,
359 #[cfg(feature = "verify_aggregation_graph")]
360 root_tasks: Default::default(),
361 }
362 }
363
364 fn execute_context<'a>(
365 &'a self,
366 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
367 ) -> impl ExecuteContext<'a> {
368 ExecuteContextImpl::new(self, turbo_tasks)
369 }
370
371 fn try_execute_context<'a>(
379 &'a self,
380 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
381 ) -> Option<impl ExecuteContext<'a>> {
382 let stopping = self.stopping.read();
383 if *stopping {
384 return None;
385 }
386 Some(ExecuteContextImpl::new_with_shutdown_guard(
387 self,
388 turbo_tasks,
389 stopping,
390 ))
391 }
392
393 pub(crate) fn start_operation(&self) -> Option<OperationGuard<'_, AnyOperation>> {
394 if !self.should_persist() {
395 return None;
396 }
397 Some(self.snapshot_coord.begin_operation())
398 }
399
400 fn should_persist(&self) -> bool {
401 matches!(
402 self.options.storage_mode,
403 Some(StorageMode::ReadWrite) | Some(StorageMode::ReadWriteOnShutdown)
404 )
405 }
406
407 #[doc(hidden)]
412 pub fn snapshot_and_evict_for_testing(
413 &self,
414 turbo_tasks: &TurboTasks<TurboTasksBackend>,
415 ) -> TestSnapshotOutcome {
416 assert!(
417 self.should_persist(),
418 "snapshot_and_evict requires persistence"
419 );
420 let snapshot_result = self.snapshot_and_persist(None, SnapshotReason::Test, turbo_tasks);
421 let (had_new_data, gc_outcome) = match snapshot_result {
422 Ok((_, new_data, gc_outcome)) => (new_data, gc_outcome),
423 Err(_) => {
424 return TestSnapshotOutcome::default();
428 }
429 };
430 let eviction_counts = self.storage.evict_after_snapshot(None);
431 TestSnapshotOutcome {
432 had_new_data,
433 eviction_counts,
434 gc: gc_outcome,
435 }
436 }
437
438 #[doc(hidden)]
440 pub fn resident_task_count_for_testing(&self) -> usize {
441 self.storage.resident_task_count_for_testing()
442 }
443
444 #[doc(hidden)]
447 pub fn parent_count_for_testing(&self, task: TaskId) -> u32 {
448 self.storage
449 .with_task(task, |t| t.gc_parent_count())
450 .unwrap_or(0)
451 }
452
453 #[doc(hidden)]
455 pub fn transient_ref_count_for_testing(&self, task: TaskId) -> u32 {
456 self.storage
457 .with_task(task, |t| t.gc_transient_ref_count())
458 .unwrap_or(0)
459 }
460
461 #[doc(hidden)]
463 pub fn persisted_gc_roots_for_testing(&self) -> Vec<(TaskId, TtlCounter)> {
464 self.backing_storage.roots().unwrap_or_default()
465 }
466 #[doc(hidden)]
470 pub fn assert_task_exists_for_testing(
471 &self,
472 task: TaskId,
473 turbo_tasks: &TurboTasks<TurboTasksBackend>,
474 ) {
475 let mut ctx = self.execute_context(turbo_tasks);
476 let _ = ctx.task(task, TaskDataCategory::All);
477 }
478
479 fn should_restore(&self) -> bool {
480 self.options.storage_mode.is_some()
481 }
482
483 fn should_track_dependencies(&self) -> bool {
484 self.options.dependency_tracking
485 }
486
487 fn should_track_activeness(&self) -> bool {
488 self.options.active_tracking
489 }
490
491 fn track_cache_hit_by_fn(&self, native_fn: &'static NativeFunction) {
492 self.task_statistics
493 .map(|stats| stats.increment_cache_hit(native_fn));
494 }
495
496 fn track_cache_miss_by_fn(&self, native_fn: &'static NativeFunction) {
497 self.task_statistics
498 .map(|stats| stats.increment_cache_miss(native_fn));
499 }
500
501 fn task_error_to_turbo_tasks_execution_error(
506 &self,
507 error: &TaskError,
508 ctx: &mut impl ExecuteContext<'_>,
509 ) -> TurboTasksExecutionError {
510 match error {
511 TaskError::Panic(panic) => TurboTasksExecutionError::Panic(panic.clone()),
512 TaskError::Error(item) => TurboTasksExecutionError::Error(Arc::new(TurboTasksError {
513 message: item.message.clone(),
514 source: item
515 .source
516 .as_ref()
517 .map(|e| self.task_error_to_turbo_tasks_execution_error(e, ctx)),
518 })),
519 TaskError::LocalTaskContext(local_task_context) => {
520 TurboTasksExecutionError::LocalTaskContext(Arc::new(TurboTaskLocalContextError {
521 name: local_task_context.name.clone(),
522 source: local_task_context
523 .source
524 .as_ref()
525 .map(|e| self.task_error_to_turbo_tasks_execution_error(e, ctx)),
526 }))
527 }
528 TaskError::TaskChain(chain) => {
529 let task_id = chain.last().unwrap();
530 let error = {
531 let task = ctx.task(*task_id, TaskDataCategory::Meta);
532 if let Some(OutputValue::Error(error)) = task.get_output() {
533 Some(error.clone())
534 } else {
535 None
536 }
537 };
538 let error = error.map_or_else(
539 || {
540 TurboTasksExecutionError::Panic(Arc::new(TurboTasksPanic {
542 message: TurboTasksExecutionErrorMessage::PIISafe(Cow::Borrowed(
543 "Error no longer available",
544 )),
545 location: None,
546 }))
547 },
548 |e| self.task_error_to_turbo_tasks_execution_error(&e, ctx),
549 );
550 let mut current_error = error;
551 for &task_id in chain.iter().rev() {
552 current_error =
553 TurboTasksExecutionError::TaskContext(Arc::new(TurboTaskContextError {
554 task_id,
555 source: Some(current_error),
556 turbo_tasks: ctx.turbo_tasks(),
557 }));
558 }
559 current_error
560 }
561 }
562 }
563}
564
565struct TaskExecutionCompletePrepareResult {
567 pub new_children: FxHashSet<TaskId>,
568 pub is_now_immutable: bool,
569 #[cfg(feature = "verify_determinism")]
570 pub no_output_set: bool,
571 #[cfg(feature = "task_dirty_cause")]
572 pub function_id: Option<FunctionId>,
573 pub new_output: Option<OutputValue>,
574 pub output_dependent_tasks: SmallVec<[TaskId; 4]>,
575 pub is_recomputation: bool,
576 pub is_session_dependent: bool,
577}
578
579fn lock_task_and_optional_reader<'e, C: ExecuteContext<'e>>(
580 ctx: &mut C,
581 task_id: TaskId,
582 reader_id: Option<TaskId>,
583) -> (C::TaskGuardImpl, Option<C::TaskGuardImpl>) {
584 let Some(reader_id) = reader_id else {
585 return (ctx.task(task_id, TaskDataCategory::All), None);
586 };
587
588 let task = ctx.task(task_id, TaskDataCategory::All);
592 if task.immutable() && !cfg!(feature = "verify_immutable") {
593 (task, None)
594 } else {
595 drop(task);
596
597 let (task, reader) = ctx.task_pair(task_id, reader_id, TaskDataCategory::All);
601 if task.immutable() && !cfg!(feature = "verify_immutable") {
605 drop(reader);
606 (task, None)
607 } else {
608 (task, Some(reader))
609 }
610 }
611}
612
613impl TurboTasksBackend {
615 fn try_read_task_output(
616 &self,
617 task_id: TaskId,
618 reader: Option<TaskId>,
619 options: ReadOutputOptions,
620 turbo_tasks: &TurboTasks<TurboTasksBackend>,
621 ) -> Result<ReadOutcome<RawVc>> {
622 self.assert_not_persistent_calling_transient(reader, task_id);
623
624 let mut ctx = self.execute_context(turbo_tasks);
625 let need_reader_task = reader.and_then(|reader_id| {
626 (self.should_track_dependencies()
627 && !matches!(options.tracking, ReadTracking::Untracked)
628 && reader_id != task_id)
629 .then_some(reader_id)
630 });
631 let (mut task, mut reader_task) =
632 lock_task_and_optional_reader(&mut ctx, task_id, need_reader_task);
633 task.assert_not_deleted("read_task_output");
634
635 fn listen_to_done_event(
636 reader_description: Option<EventDescription>,
637 tracking: ReadTracking,
638 done_event: &Event,
639 ) -> EventListener {
640 done_event.listen_with_note(move || {
641 move || {
642 if let Some(reader_description) = reader_description.as_ref() {
643 format!(
644 "try_read_task_output from {} ({})",
645 reader_description, tracking
646 )
647 } else {
648 format!("try_read_task_output ({})", tracking)
649 }
650 }
651 })
652 }
653
654 fn check_in_progress<T>(
658 task: &impl TaskGuard,
659 reader_description: Option<EventDescription>,
660 tracking: ReadTracking,
661 ) -> Option<Result<ReadOutcome<T>>> {
662 match task.get_in_progress() {
663 Some(InProgressState::Scheduled { done_event, .. }) => {
664 Some(Ok(ReadOutcome::Scheduled(listen_to_done_event(
665 reader_description,
666 tracking,
667 done_event,
668 ))))
669 }
670 Some(InProgressState::InProgress(InProgressStateInner { done_event, .. })) => {
671 Some(Ok(ReadOutcome::InProgress(listen_to_done_event(
672 reader_description,
673 tracking,
674 done_event,
675 ))))
676 }
677 Some(InProgressState::Canceled) => Some(Err(anyhow::anyhow!(
678 "{} was canceled",
679 task.get_task_description()
680 ))),
681 None => None,
682 }
683 }
684
685 if matches!(options.consistency, ReadConsistency::Strong) {
686 if task
687 .get_persistent_task_type()
688 .is_some_and(|t| !t.native_fn.is_root)
689 {
690 drop(task);
691 drop(reader_task);
692 panic!(
693 "Strongly consistent read of non-root task {} (reader: {}). The `root` \
694 attribute is missing on the task.",
695 self.debug_get_task_description(task_id),
696 reader.map_or_else(
697 || "unknown".to_string(),
698 |r| self.debug_get_task_description(r)
699 )
700 );
701 }
702
703 let is_dirty = task.is_dirty();
704
705 let has_dirty_containers = task.has_dirty_containers();
707 if has_dirty_containers || is_dirty.is_some() {
708 let activeness = task.get_activeness_mut();
709 let mut task_ids_to_schedule: Vec<_> = Vec::new();
710 let activeness = if let Some(activeness) = activeness {
712 activeness.set_active_until_clean();
716 activeness
717 } else {
718 if ctx.should_track_activeness() {
722 task_ids_to_schedule = task.dirty_containers().collect();
724 task_ids_to_schedule.push(task_id);
725 }
726 let activeness =
727 task.get_activeness_mut_or_insert_with(|| ActivenessState::new(task_id));
728 activeness.set_active_until_clean();
729 activeness
730 };
731 let listener = activeness.all_clean_event.listen_with_note(move || {
732 let tt = turbo_tasks.pin();
735 move || {
736 let mut ctx = tt.backend().execute_context(&tt);
737 let mut visited = FxHashSet::default();
738 fn indent(s: &str) -> String {
739 s.split_inclusive('\n')
740 .flat_map(|line: &str| [" ", line].into_iter())
741 .collect::<String>()
742 }
743 fn get_info(
744 ctx: &mut impl ExecuteContext<'_>,
745 task_id: TaskId,
746 parent_and_count: Option<(TaskId, i32)>,
747 visited: &mut FxHashSet<TaskId>,
748 ) -> String {
749 let task = ctx.task(task_id, TaskDataCategory::All);
750 let is_dirty = task.is_dirty();
751 let in_progress =
752 task.get_in_progress()
753 .map_or("not in progress", |p| match p {
754 InProgressState::InProgress(_) => "in progress",
755 InProgressState::Scheduled { .. } => "scheduled",
756 InProgressState::Canceled => "canceled",
757 });
758 let activeness = task.get_activeness().map_or_else(
759 || "not active".to_string(),
760 |activeness| format!("{activeness:?}"),
761 );
762 let aggregation_number = get_aggregation_number(&task);
763 let missing_upper = if let Some((parent_task_id, _)) = parent_and_count
764 {
765 let uppers = get_uppers(&task);
766 !uppers.contains(&parent_task_id)
767 } else {
768 false
769 };
770
771 let has_dirty_containers = task.has_dirty_containers();
773
774 let task_description = task.get_task_description();
775 let is_dirty_label = if let Some(parent_priority) = is_dirty {
776 format!(", dirty({parent_priority})")
777 } else {
778 String::new()
779 };
780 let has_dirty_containers_label = if has_dirty_containers {
781 ", dirty containers"
782 } else {
783 ""
784 };
785 let count = if let Some((_, count)) = parent_and_count {
786 format!(" {count}")
787 } else {
788 String::new()
789 };
790 let mut info = format!(
791 "{task_id} {task_description}{count} (aggr={aggregation_number}, \
792 {in_progress}, \
793 {activeness}{is_dirty_label}{has_dirty_containers_label})",
794 );
795 let children: Vec<_> = task.dirty_containers_with_count().collect();
796 drop(task);
797
798 if missing_upper {
799 info.push_str("\n ERROR: missing upper connection");
800 }
801
802 if has_dirty_containers || !children.is_empty() {
803 writeln!(info, "\n dirty tasks:").unwrap();
804
805 for (child_task_id, count) in children {
806 let task_description = ctx
807 .task(child_task_id, TaskDataCategory::Data)
808 .get_task_description();
809 if visited.insert(child_task_id) {
810 let child_info = get_info(
811 ctx,
812 child_task_id,
813 Some((task_id, count)),
814 visited,
815 );
816 info.push_str(&indent(&child_info));
817 if !info.ends_with('\n') {
818 info.push('\n');
819 }
820 } else {
821 writeln!(
822 info,
823 " {child_task_id} {task_description} {count} \
824 (already visited)"
825 )
826 .unwrap();
827 }
828 }
829 }
830 info
831 }
832 let info = get_info(&mut ctx, task_id, None, &mut visited);
833 format!(
834 "try_read_task_output (strongly consistent) from {reader:?}\n{info}"
835 )
836 }
837 });
838 drop(reader_task);
839 drop(task);
840 if !task_ids_to_schedule.is_empty() {
841 let mut queue = AggregationUpdateQueue::new();
842 queue.extend_find_and_schedule_dirty(task_ids_to_schedule);
843 queue.execute(&mut ctx);
844 }
845
846 return Ok(ReadOutcome::InProgress(listener));
847 }
848 }
849
850 let reader_description = reader_task
851 .as_ref()
852 .map(|r| EventDescription::new(|| r.get_task_desc_fn()))
853 .or_else(|| {
854 need_reader_task.map(|reader_id| {
855 EventDescription::new(move || move || format!("{reader_id:?}"))
856 })
857 });
858 if let Some(value) = check_in_progress(&task, reader_description.clone(), options.tracking)
859 {
860 return value;
861 }
862
863 if let Some(output) = task.get_output() {
864 let result = match output {
865 OutputValue::Cell(cell) => Ok(RawVc::task_cell(cell.task, cell.cell)),
866 OutputValue::Output(task) => Ok(RawVc::task_output(*task)),
867 OutputValue::Error(error) => Err(error.clone()),
868 };
869 if let Some(mut reader_task) = reader_task.take()
870 && options.tracking.should_track(result.is_err())
871 {
872 #[cfg(feature = "trace_task_output_dependencies")]
873 let _span = tracing::trace_span!(
874 "add output dependency",
875 task = %task_id,
876 dependent_task = ?reader
877 )
878 .entered();
879 let mut queue = LeafDistanceUpdateQueue::new();
880 let reader = reader.unwrap();
881 if task.add_output_dependent(reader) {
882 let leaf_distance = task.get_leaf_distance().copied().unwrap_or_default();
884 let reader_leaf_distance =
885 reader_task.get_leaf_distance().copied().unwrap_or_default();
886 if reader_leaf_distance.distance <= leaf_distance.distance {
887 queue.push(
888 reader,
889 leaf_distance.distance,
890 leaf_distance.max_distance_in_buffer,
891 );
892 }
893 }
894
895 drop(task);
896
897 if !reader_task.remove_outdated_output_dependencies(&task_id) {
903 let _ = reader_task.add_output_dependencies(task_id);
904 }
905 drop(reader_task);
906
907 queue.execute(&mut ctx);
908 } else {
909 drop(task);
910 }
911
912 return result.map(ReadOutcome::Value).map_err(|error| {
913 self.task_error_to_turbo_tasks_execution_error(&error, &mut ctx)
914 .with_task_context(task_id, turbo_tasks.pin())
915 .into()
916 });
917 }
918 drop(reader_task);
919
920 let note = EventDescription::new(|| {
921 move || {
922 if let Some(reader) = reader_description.as_ref() {
923 format!("try_read_task_output (recompute) from {reader}",)
924 } else {
925 "try_read_task_output (recompute, untracked)".to_string()
926 }
927 }
928 });
929
930 let (in_progress_state, listener) = InProgressState::new_scheduled_with_listener(
932 TaskExecutionReason::OutputNotAvailable,
933 EventDescription::new(|| task.get_task_desc_fn()),
934 note,
935 );
936
937 let old = task.set_in_progress(in_progress_state);
940 debug_assert!(old.is_none(), "InProgress already exists");
941 ctx.schedule_task(&task, TaskPriority::Recomputation);
942
943 Ok(ReadOutcome::Scheduled(listener))
944 }
945
946 fn try_read_task_cell(
947 &self,
948 task_id: TaskId,
949 reader: Option<TaskId>,
950 cell: CellId,
951 options: ReadCellOptions,
952 turbo_tasks: &TurboTasks<TurboTasksBackend>,
953 ) -> Result<ReadOutcome<TypedCellContent>> {
954 self.assert_not_persistent_calling_transient(reader, task_id);
955
956 fn add_cell_dependency(
957 task_id: TaskId,
958 mut task: impl TaskGuard,
959 reader: Option<TaskId>,
960 reader_task: Option<impl TaskGuard>,
961 cell: CellId,
962 key: Option<u64>,
963 ) {
964 if let Some(mut reader_task) = reader_task {
965 let reader = reader.unwrap();
966 let reverse = CellRef { task: reader, cell };
967 if let Some(k) = key {
968 let _ = task.add_cell_dependents_hashed((reverse, k));
969 } else {
970 let _ = task.add_cell_dependents(reverse);
971 }
972 drop(task);
973
974 let target = CellRef {
980 task: task_id,
981 cell,
982 };
983 if let Some(k) = key {
984 if !reader_task.remove_outdated_cell_dependencies_hashed(&(target, k)) {
985 let _ = reader_task.add_cell_dependencies_hashed((target, k));
986 }
987 } else if !reader_task.remove_outdated_cell_dependencies(&target) {
988 let _ = reader_task.add_cell_dependencies(target);
989 }
990 drop(reader_task);
991 }
992 }
993
994 let ReadCellOptions {
995 tracking,
996 final_read_hint,
997 } = options;
998
999 let mut ctx = self.execute_context(turbo_tasks);
1000 let need_reader_task = reader.and_then(|reader_id| {
1001 (self.should_track_dependencies()
1002 && !matches!(tracking, ReadCellTracking::Untracked)
1003 && reader_id != task_id)
1004 .then_some(reader_id)
1005 });
1006 let (mut task, reader_task) =
1007 lock_task_and_optional_reader(&mut ctx, task_id, need_reader_task);
1008 task.assert_not_deleted("read_task_cell");
1009
1010 let content = if final_read_hint {
1011 task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence)
1012 } else {
1013 task.get_cell_data(&cell).cloned()
1014 };
1015 if let Some(content) = content {
1016 if tracking.should_track(false) {
1017 add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1018 }
1019 return Ok(ReadOutcome::Value(TypedCellContent(
1020 cell.type_id(),
1021 CellContent(Some(content)),
1022 )));
1023 }
1024
1025 let in_progress = task.get_in_progress();
1026 if matches!(
1027 in_progress,
1028 Some(InProgressState::InProgress(..) | InProgressState::Scheduled { .. })
1029 ) {
1030 let started = matches!(in_progress, Some(InProgressState::InProgress(..)));
1033 let listener = self
1034 .listen_to_cell(&mut task, task_id, need_reader_task, &reader_task, cell)
1035 .0;
1036 return Ok(if started {
1037 ReadOutcome::InProgress(listener)
1038 } else {
1039 ReadOutcome::Scheduled(listener)
1040 });
1041 }
1042 let is_cancelled = matches!(in_progress, Some(InProgressState::Canceled));
1043
1044 let max_id = task.get_cell_type_max_index(&cell.type_id()).copied();
1046 let Some(max_id) = max_id else {
1047 let task_desc = task.get_task_description();
1048 if tracking.should_track(true) {
1049 add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1050 }
1051 bail!(
1052 "Cell {cell:?} no longer exists in task {task_desc} (no cell of this type exists)",
1053 );
1054 };
1055 if cell.index() >= max_id {
1056 let task_desc = task.get_task_description();
1057 if tracking.should_track(true) {
1058 add_cell_dependency(task_id, task, reader, reader_task, cell, tracking.key());
1059 }
1060 bail!("Cell {cell:?} no longer exists in task {task_desc} (index out of bounds)");
1061 }
1062
1063 if is_cancelled {
1069 bail!("{} was canceled", task.get_task_description());
1070 }
1071
1072 let (listener, new_listener) =
1074 self.listen_to_cell(&mut task, task_id, need_reader_task, &reader_task, cell);
1075 drop(reader_task);
1076 if !new_listener {
1077 return Ok(ReadOutcome::InProgress(listener));
1079 }
1080
1081 let _span = tracing::trace_span!(
1082 "recomputation",
1083 cell_type = get_value_type(cell.type_id()).ty.global_name,
1084 cell_index = cell.index()
1085 )
1086 .entered();
1087
1088 let _ = task.add_scheduled(
1089 TaskExecutionReason::CellNotAvailable,
1090 EventDescription::new(|| task.get_task_desc_fn()),
1091 );
1092 ctx.schedule_task(&task, TaskPriority::Recomputation);
1093
1094 Ok(ReadOutcome::Scheduled(listener))
1095 }
1096
1097 fn listen_to_cell(
1098 &self,
1099 task: &mut impl TaskGuard,
1100 task_id: TaskId,
1101 reader: Option<TaskId>,
1102 reader_task: &Option<impl TaskGuard>,
1103 cell: CellId,
1104 ) -> (EventListener, bool) {
1105 let note = || {
1106 let reader_desc = reader_task.as_ref().map(|r| r.get_task_desc_fn());
1107 move || {
1108 if let Some(reader_desc) = reader_desc.as_ref() {
1109 format!("try_read_task_cell (in progress) from {}", (reader_desc)())
1110 } else if let Some(reader_id) = reader {
1111 format!("try_read_task_cell (in progress) from {reader_id:?}")
1112 } else {
1113 "try_read_task_cell (in progress, untracked)".to_string()
1114 }
1115 }
1116 };
1117 if let Some(in_progress) = task.get_in_progress_cells(&cell) {
1118 let listener = in_progress.event.listen_with_note(note);
1120 return (listener, false);
1121 }
1122 let in_progress = InProgressCellState::new(task_id, cell);
1123 let listener = in_progress.event.listen_with_note(note);
1124 let old = task.insert_in_progress_cells(cell, in_progress);
1125 debug_assert!(old.is_none(), "InProgressCell already exists");
1126 (listener, true)
1127 }
1128
1129 #[allow(clippy::type_complexity, reason = "only used for tests")]
1136 fn snapshot_and_persist(
1137 &self,
1138 parent_span: Option<tracing::Id>,
1139 reason: SnapshotReason,
1140 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1141 ) -> Result<(Instant, bool, Option<(GcStats, GcPassResult)>)> {
1142 let snapshot_span =
1143 tracing::trace_span!(parent: parent_span.clone(), "snapshot", reason = reason.as_str())
1144 .entered();
1145 let _snapshot_in_progress = self.snapshot_in_progress.lock();
1149
1150 let start = Instant::now();
1154 let wall_start = SystemTime::now();
1158 let mut snapshot_phase = self.snapshot_coord.begin_snapshot();
1159 let (gc_elapsed, gc_roots_to_persist, gc_outcome) = if self.gc_enabled {
1160 let gc_span = tracing::info_span!(
1161 "gc",
1162 stats = tracing::field::Empty,
1163 interrupted = tracing::field::Empty
1164 )
1165 .entered();
1166 let (stats, result, roots) =
1167 self.gc_collect(turbo_tasks, &snapshot_phase, reason.gc_is_interruptible());
1168 gc_span.record("stats", display(&stats));
1169 gc_span.record("interrupted", result.interrupted);
1170 if result.interrupted {
1171 drop(snapshot_phase);
1174 drop(gc_span);
1175 return Ok((start, false, Some((stats, result))));
1176 }
1177 (Some(start.elapsed()), roots, Some((stats, result)))
1178 } else {
1179 (None, None, None)
1180 };
1181
1182 debug_assert!(self.should_persist());
1183
1184 let (snapshot_guard, has_modifications) = self.storage.start_snapshot();
1186
1187 let suspended_operations = snapshot_phase.take_suspended_operations();
1188
1189 let snapshot_time = Instant::now();
1190 drop(snapshot_phase);
1191
1192 if !has_modifications && gc_roots_to_persist.is_none() {
1193 drop(snapshot_guard);
1196 return Ok((start, false, gc_outcome));
1197 }
1198
1199 #[cfg(feature = "print_cache_item_size")]
1200 #[derive(Default)]
1201 struct TaskCacheStats {
1202 data: usize,
1203 #[cfg(feature = "print_cache_item_size_with_compressed")]
1204 data_compressed: usize,
1205 data_count: usize,
1206 meta: usize,
1207 #[cfg(feature = "print_cache_item_size_with_compressed")]
1208 meta_compressed: usize,
1209 meta_count: usize,
1210 upper_count: usize,
1211 collectibles_count: usize,
1212 aggregated_collectibles_count: usize,
1213 children_count: usize,
1214 followers_count: usize,
1215 collectibles_dependents_count: usize,
1216 aggregated_dirty_containers_count: usize,
1217 output_size: usize,
1218 }
1219 #[cfg(feature = "print_cache_item_size")]
1222 struct FormatSizes {
1223 size: usize,
1224 #[cfg(feature = "print_cache_item_size_with_compressed")]
1225 compressed_size: usize,
1226 }
1227 #[cfg(feature = "print_cache_item_size")]
1228 impl std::fmt::Display for FormatSizes {
1229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1230 use turbo_tasks::util::FormatBytes;
1231 #[cfg(feature = "print_cache_item_size_with_compressed")]
1232 {
1233 write!(
1234 f,
1235 "{} ({} compressed)",
1236 FormatBytes(self.size),
1237 FormatBytes(self.compressed_size)
1238 )
1239 }
1240 #[cfg(not(feature = "print_cache_item_size_with_compressed"))]
1241 {
1242 write!(f, "{}", FormatBytes(self.size))
1243 }
1244 }
1245 }
1246 #[cfg(feature = "print_cache_item_size")]
1247 impl TaskCacheStats {
1248 #[cfg(feature = "print_cache_item_size_with_compressed")]
1249 fn compressed_size(data: &[u8]) -> usize {
1250 lz4_flex::block::compress(data).len()
1251 }
1252
1253 fn add_data(&mut self, data: &[u8]) {
1254 self.data += data.len();
1255 #[cfg(feature = "print_cache_item_size_with_compressed")]
1256 {
1257 self.data_compressed += Self::compressed_size(data);
1258 }
1259 self.data_count += 1;
1260 }
1261
1262 fn add_meta(&mut self, data: &[u8]) {
1263 self.meta += data.len();
1264 #[cfg(feature = "print_cache_item_size_with_compressed")]
1265 {
1266 self.meta_compressed += Self::compressed_size(data);
1267 }
1268 self.meta_count += 1;
1269 }
1270
1271 fn add_counts(&mut self, storage: &TaskStorage) {
1272 let counts = storage.meta_counts();
1273 self.upper_count += counts.upper;
1274 self.collectibles_count += counts.collectibles;
1275 self.aggregated_collectibles_count += counts.aggregated_collectibles;
1276 self.children_count += counts.children;
1277 self.followers_count += counts.followers;
1278 self.collectibles_dependents_count += counts.collectibles_dependents;
1279 self.aggregated_dirty_containers_count += counts.aggregated_dirty_containers;
1280 if let Some(output) = storage.get_output() {
1281 use turbo_bincode::turbo_bincode_encode;
1282
1283 self.output_size += turbo_bincode_encode(&output)
1284 .map(|data| data.len())
1285 .unwrap_or(0);
1286 }
1287 }
1288
1289 fn task_name(storage: &TaskStorage) -> String {
1291 storage
1292 .get_persistent_task_type()
1293 .map(|t| t.to_string())
1294 .unwrap_or_else(|| "<unknown>".to_string())
1295 }
1296
1297 fn sort_key(&self) -> usize {
1300 #[cfg(feature = "print_cache_item_size_with_compressed")]
1301 {
1302 self.data_compressed + self.meta_compressed
1303 }
1304 #[cfg(not(feature = "print_cache_item_size_with_compressed"))]
1305 {
1306 self.data + self.meta
1307 }
1308 }
1309
1310 fn format_total(&self) -> FormatSizes {
1311 FormatSizes {
1312 size: self.data + self.meta,
1313 #[cfg(feature = "print_cache_item_size_with_compressed")]
1314 compressed_size: self.data_compressed + self.meta_compressed,
1315 }
1316 }
1317
1318 fn format_data(&self) -> FormatSizes {
1319 FormatSizes {
1320 size: self.data,
1321 #[cfg(feature = "print_cache_item_size_with_compressed")]
1322 compressed_size: self.data_compressed,
1323 }
1324 }
1325
1326 fn format_avg_data(&self) -> FormatSizes {
1327 FormatSizes {
1328 size: self.data.checked_div(self.data_count).unwrap_or(0),
1329 #[cfg(feature = "print_cache_item_size_with_compressed")]
1330 compressed_size: self
1331 .data_compressed
1332 .checked_div(self.data_count)
1333 .unwrap_or(0),
1334 }
1335 }
1336
1337 fn format_meta(&self) -> FormatSizes {
1338 FormatSizes {
1339 size: self.meta,
1340 #[cfg(feature = "print_cache_item_size_with_compressed")]
1341 compressed_size: self.meta_compressed,
1342 }
1343 }
1344
1345 fn format_avg_meta(&self) -> FormatSizes {
1346 FormatSizes {
1347 size: self.meta.checked_div(self.meta_count).unwrap_or(0),
1348 #[cfg(feature = "print_cache_item_size_with_compressed")]
1349 compressed_size: self
1350 .meta_compressed
1351 .checked_div(self.meta_count)
1352 .unwrap_or(0),
1353 }
1354 }
1355 }
1356 #[cfg(feature = "print_cache_item_size")]
1357 let task_cache_stats: Mutex<FxHashMap<_, TaskCacheStats>> =
1358 Mutex::new(FxHashMap::default());
1359
1360 let process = |task_id: TaskId, inner: &TaskStorage, buffer: &mut TurboBincodeBuffer| {
1367 let encode_category = |task_id: TaskId,
1368 data: &TaskStorage,
1369 category: SpecificTaskDataCategory,
1370 buffer: &mut TurboBincodeBuffer|
1371 -> Option<TurboBincodeBuffer> {
1372 match encode_task_data(task_id, data, category, buffer) {
1373 Ok(encoded) => {
1374 #[cfg(feature = "print_cache_item_size")]
1375 {
1376 let mut stats = task_cache_stats.lock();
1377 let entry = stats.entry(TaskCacheStats::task_name(inner)).or_default();
1378 match category {
1379 SpecificTaskDataCategory::Meta => entry.add_meta(&encoded),
1380 SpecificTaskDataCategory::Data => entry.add_data(&encoded),
1381 }
1382 }
1383 Some(encoded)
1384 }
1385 Err(err) => {
1386 panic!(
1387 "Serializing task {} failed ({:?}): {:?}",
1388 self.debug_get_task_description(task_id),
1389 category,
1390 err
1391 );
1392 }
1393 }
1394 };
1395 if task_id.is_transient() {
1396 unreachable!("transient task_ids should never be enqueued to be persisted");
1397 }
1398
1399 if self.gc_enabled {
1400 if inner.flags.deleted() {
1401 debug_assert!(
1402 !inner.flags.new_task(),
1403 "a scanned GC-deleted task must be persisted; new tasks are discarded by \
1404 GC"
1405 );
1406 let task_type_hash = compute_task_type_hash(
1407 inner
1408 .get_persistent_task_type()
1409 .expect("a GC-deleted task must have a task type"),
1410 );
1411 return SnapshotItem::Delete {
1412 task_id,
1413 task_type_hash,
1414 };
1415 } else {
1416 debug_assert!(
1417 !inner.gc_collectible(),
1418 "tasks scheduled for persistent must not be collectible, this implies a \
1419 missed task during GC"
1420 );
1421 }
1422 } else {
1423 debug_assert!(
1424 !inner.flags.deleted(),
1425 "Deleted flags should only be set by GC and it is disabled"
1426 )
1427 }
1428
1429 let encode_meta = inner.flags.meta_modified();
1430 let encode_data = inner.flags.data_modified();
1431
1432 #[cfg(feature = "print_cache_item_size")]
1433 if encode_data || encode_meta {
1434 task_cache_stats
1435 .lock()
1436 .entry(TaskCacheStats::task_name(inner))
1437 .or_default()
1438 .add_counts(inner);
1439 }
1440
1441 let meta = if encode_meta {
1442 encode_category(task_id, inner, SpecificTaskDataCategory::Meta, buffer)
1443 } else {
1444 None
1445 };
1446
1447 let data = if encode_data {
1448 encode_category(task_id, inner, SpecificTaskDataCategory::Data, buffer)
1449 } else {
1450 None
1451 };
1452 let task_type_hash = if inner.flags.new_task() {
1453 let task_type = inner.get_persistent_task_type().expect(
1454 "It is not possible for a new_task to not have a persistent_task_type. Task \
1455 creation for persistent tasks uses a single ExecutionContextImpl for \
1456 creating the task (which sets new_task) and connect_child (which sets \
1457 persistent_task_type) and take_snapshot waits for all operations to complete \
1458 or suspend before we start snapshotting. So task creation will always set \
1459 the task_type.",
1460 );
1461 Some(compute_task_type_hash(task_type))
1462 } else {
1463 None
1464 };
1465
1466 SnapshotItem::Put {
1467 task_id,
1468 meta,
1469 data,
1470 task_type_hash,
1471 }
1472 };
1473
1474 let task_snapshots =
1475 self.storage
1476 .take_snapshot(snapshot_guard, &process, reason.drain_entries());
1477
1478 drop(snapshot_span);
1479 let snapshot_duration = start.elapsed();
1480 let task_count = task_snapshots.len();
1481
1482 if task_snapshots.is_empty() && gc_roots_to_persist.is_none() {
1483 std::hint::cold_path();
1487 return Ok((snapshot_time, false, gc_outcome));
1488 }
1489
1490 let persist_start = Instant::now();
1491 let span = tracing::info_span!(
1492 parent: parent_span,
1493 "persist",
1494 reason = reason.as_str(),
1495 snapshot_meta = tracing::field::Empty,
1496 )
1497 .entered();
1498 let snapshot_meta = self.backing_storage.save_snapshot(
1502 suspended_operations,
1503 gc_roots_to_persist,
1504 task_snapshots,
1505 )?;
1506 span.record("snapshot_meta", display(snapshot_meta));
1507
1508 #[cfg(feature = "print_cache_item_size")]
1509 {
1510 let mut task_cache_stats = task_cache_stats
1511 .into_inner()
1512 .into_iter()
1513 .collect::<Vec<_>>();
1514 if !task_cache_stats.is_empty() {
1515 use turbo_tasks::util::FormatBytes;
1516
1517 use crate::utils::markdown_table::print_markdown_table;
1518
1519 task_cache_stats.sort_unstable_by(|(key_a, stats_a), (key_b, stats_b)| {
1520 (stats_b.sort_key(), key_b).cmp(&(stats_a.sort_key(), key_a))
1521 });
1522
1523 println!(
1524 "Task cache stats: {}",
1525 FormatSizes {
1526 size: task_cache_stats
1527 .iter()
1528 .map(|(_, s)| s.data + s.meta)
1529 .sum::<usize>(),
1530 #[cfg(feature = "print_cache_item_size_with_compressed")]
1531 compressed_size: task_cache_stats
1532 .iter()
1533 .map(|(_, s)| s.data_compressed + s.meta_compressed)
1534 .sum::<usize>()
1535 },
1536 );
1537
1538 print_markdown_table(
1539 [
1540 "Task",
1541 " Total Size",
1542 " Data Size",
1543 " Data Count x Avg",
1544 " Data Count x Avg",
1545 " Meta Size",
1546 " Meta Count x Avg",
1547 " Meta Count x Avg",
1548 " Uppers",
1549 " Coll",
1550 " Agg Coll",
1551 " Children",
1552 " Followers",
1553 " Coll Deps",
1554 " Agg Dirty",
1555 " Output Size",
1556 ],
1557 task_cache_stats.iter(),
1558 |(task_desc, stats)| {
1559 [
1560 task_desc.to_string(),
1561 format!(" {}", stats.format_total()),
1562 format!(" {}", stats.format_data()),
1563 format!(" {} x", stats.data_count),
1564 format!("{}", stats.format_avg_data()),
1565 format!(" {}", stats.format_meta()),
1566 format!(" {} x", stats.meta_count),
1567 format!("{}", stats.format_avg_meta()),
1568 format!(" {}", stats.upper_count),
1569 format!(" {}", stats.collectibles_count),
1570 format!(" {}", stats.aggregated_collectibles_count),
1571 format!(" {}", stats.children_count),
1572 format!(" {}", stats.followers_count),
1573 format!(" {}", stats.collectibles_dependents_count),
1574 format!(" {}", stats.aggregated_dirty_containers_count),
1575 format!(" {}", FormatBytes(stats.output_size)),
1576 ]
1577 },
1578 );
1579 }
1580 }
1581
1582 let elapsed = start.elapsed();
1583 let persist_duration = persist_start.elapsed();
1584 if elapsed > Duration::from_secs(10) {
1586 turbo_tasks.send_compilation_event(Arc::new(TimingEvent::new(
1587 "Finished writing to filesystem cache".to_string(),
1588 elapsed,
1589 )));
1590 }
1591
1592 let (persist_wall_start, persist_wall_duration) = if let Some(gc_elapsed) = gc_elapsed {
1593 turbo_tasks.send_compilation_event(Arc::new(TraceEvent::new_with_duration(
1594 "turbopack-gc",
1595 wall_start,
1596 gc_elapsed,
1597 serde_json::json!([]),
1598 )));
1599 (wall_start + gc_elapsed, elapsed.saturating_sub(gc_elapsed))
1600 } else {
1601 (wall_start, elapsed)
1602 };
1603 turbo_tasks.send_compilation_event(Arc::new(TraceEvent::new_with_duration(
1604 "turbopack-persistence",
1605 persist_wall_start,
1606 persist_wall_duration,
1607 serde_json::json!([
1608 ["reason", reason.as_str()],
1609 [
1610 "snapshot_duration_ms",
1611 snapshot_duration.as_secs_f64() * 1000.0,
1612 ],
1613 [
1614 "persist_duration_ms",
1615 persist_duration.as_secs_f64() * 1000.0,
1616 ],
1617 ["task_count", task_count],
1618 ["bytes_written", snapshot_meta.bytes_written,],
1619 ["bytes_deleted", snapshot_meta.bytes_deleted,]
1620 ]),
1621 )));
1622
1623 Ok((snapshot_time, true, gc_outcome))
1624 }
1625
1626 fn startup(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1627 if self.should_restore() {
1628 let uncompleted_operations = self
1632 .backing_storage
1633 .uncompleted_operations()
1634 .expect("Failed to get uncompleted operations");
1635 if !uncompleted_operations.is_empty() {
1636 let mut ctx = self.execute_context(turbo_tasks);
1637 for op in uncompleted_operations {
1638 op.execute(&mut ctx);
1639 }
1640 }
1641 }
1642
1643 if matches!(self.options.storage_mode, Some(StorageMode::ReadWrite)) {
1646 let _span = trace_span!("persisting background job").entered();
1648 let _span = tracing::info_span!("thread").entered();
1649 turbo_tasks.schedule_backend_background_job(TurboTasksBackendJob::Snapshot);
1650 }
1651 }
1652
1653 fn stopping(&self) {
1654 *self.stopping.write() = true;
1656 self.stopping_event.notify(usize::MAX);
1657 }
1658
1659 #[allow(unused_variables)]
1660 fn stop(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1661 #[cfg(feature = "verify_aggregation_graph")]
1662 {
1663 self.is_idle.store(false, Ordering::Release);
1664 self.verify_aggregation_graph(turbo_tasks, false);
1665 }
1666 self.storage.drop_task_cache();
1668 if self.should_persist()
1669 && let Err(err) =
1670 self.snapshot_and_persist(Span::current().into(), SnapshotReason::Stop, turbo_tasks)
1671 {
1672 eprintln!("Persisting failed during shutdown: {err:?}");
1673 }
1674 self.storage.drop_contents();
1675 if let Err(err) = self.backing_storage.shutdown() {
1676 println!("Shutting down failed: {err}");
1677 }
1678 }
1679
1680 #[allow(unused_variables)]
1681 fn idle_start(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1682 self.idle_start_event.notify(usize::MAX);
1683
1684 #[cfg(feature = "verify_aggregation_graph")]
1685 {
1686 use tokio::select;
1687
1688 self.is_idle.store(true, Ordering::Release);
1689 let turbo_tasks = turbo_tasks.pin();
1693 tokio::task::spawn(async move {
1694 let backend = &turbo_tasks.backend();
1695 select! {
1696 _ = tokio::time::sleep(Duration::from_secs(5)) => {
1697 }
1699 _ = backend.idle_end_event.listen() => {
1700 return;
1701 }
1702 }
1703 if !backend.is_idle.load(Ordering::Relaxed) {
1704 return;
1705 }
1706 backend.verify_aggregation_graph(&turbo_tasks, true);
1707 });
1708 }
1709 }
1710
1711 fn idle_end(&self) {
1712 #[cfg(feature = "verify_aggregation_graph")]
1713 self.is_idle.store(false, Ordering::Release);
1714 self.idle_end_event.notify(usize::MAX);
1715 }
1716
1717 fn get_or_create_task(
1718 &self,
1719 native_fn: &'static NativeFunction,
1720 this: Option<RawVc>,
1721 arg: &mut dyn DynTaskInputsStorage,
1722 parent_task: Option<TaskId>,
1723 persistence: TaskPersistence,
1724 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1725 ) -> TaskId {
1726 let transient = matches!(persistence, TaskPersistence::Transient);
1727
1728 if transient
1729 && let Some(parent_task) = parent_task
1730 && !parent_task.is_transient()
1731 {
1732 let task_type = CachedTaskType {
1733 native_fn,
1734 this,
1735 arg: arg.take_box(),
1736 };
1737 self.panic_persistent_calling_transient(
1738 self.debug_get_task_description(parent_task),
1739 Some(&task_type),
1740 );
1741 }
1742
1743 let is_root = native_fn.is_root;
1744
1745 let arg_ref = arg.as_ref();
1747 let hash = CachedTaskType::hash_from_components(
1748 self.storage.task_cache.hasher(),
1749 native_fn,
1750 this,
1751 arg_ref,
1752 );
1753 let shard = get_shard(&self.storage.task_cache, hash);
1757
1758 let mut ctx = self.execute_context(turbo_tasks);
1759 let mut created_new = false;
1760 if let Some(task_id) =
1764 get_in_shard(shard, hash, |k| k.eq_components(native_fn, this, arg_ref))
1765 {
1766 self.track_cache_hit_by_fn(native_fn);
1767 operation::ConnectChildOperation::run(
1768 parent_task,
1769 task_id,
1770 false,
1771 ctx,
1772 );
1773 return task_id;
1774 }
1775
1776 let task_id = if !transient
1781 && let Some((task_id, stored_type)) = ctx.task_by_type(native_fn, this, arg_ref)
1782 {
1783 self.track_cache_hit_by_fn(native_fn);
1784 with_entry_in_shard(
1787 shard,
1788 self.storage.task_cache.hasher(),
1789 hash,
1790 arg,
1791 |k, arg| k.eq_components(native_fn, this, arg.as_ref()),
1792 |entry, _arg| {
1793 if let Entry::Vacant(entry) = entry {
1794 entry.insert((stored_type, task_id));
1795 }
1796 },
1797 );
1798 task_id
1799 } else {
1800 let (task_id, created) = with_entry_in_shard(
1801 shard,
1802 self.storage.task_cache.hasher(),
1803 hash,
1804 arg,
1805 |k, arg| k.eq_components(native_fn, this, arg.as_ref()),
1806 |entry, arg| match entry {
1807 Entry::Occupied(entry) => {
1808 (entry.get().1, false)
1811 }
1812 Entry::Vacant(entry) => {
1813 let task_type = CachedTaskTypeArc::new(CachedTaskType {
1817 native_fn,
1818 this,
1819 arg: arg.take_box(),
1820 });
1821 let task_id = if transient {
1822 self.transient_task_id_factory.get()
1823 } else {
1824 self.persisted_task_id_factory.get()
1825 };
1826 self.storage
1830 .initialize_new_task(task_id, Some(task_type.clone()));
1831 entry.insert((task_type, task_id));
1832 (task_id, true)
1833 }
1834 },
1835 );
1836
1837 created_new = created;
1840 if created {
1841 self.track_cache_miss_by_fn(native_fn);
1842 if is_root {
1845 AggregationUpdateQueue::run(
1846 AggregationUpdateJob::UpdateAggregationNumber {
1847 task_id,
1848 base_aggregation_number: u32::MAX,
1849 distance: None,
1850 },
1851 &mut ctx,
1852 );
1853 } else if native_fn.is_session_dependent && self.should_track_dependencies() {
1854 const SESSION_DEPENDENT_AGGREGATION_NUMBER: u32 = u32::MAX >> 2;
1855 AggregationUpdateQueue::run(
1856 AggregationUpdateJob::UpdateAggregationNumber {
1857 task_id,
1858 base_aggregation_number: SESSION_DEPENDENT_AGGREGATION_NUMBER,
1859 distance: None,
1860 },
1861 &mut ctx,
1862 );
1863 }
1864 } else {
1865 self.track_cache_hit_by_fn(native_fn);
1866 }
1867
1868 task_id
1869 };
1870
1871 operation::ConnectChildOperation::run(parent_task, task_id, created_new, ctx);
1874
1875 task_id
1876 }
1877
1878 fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1879 if !self.should_track_dependencies() {
1880 panic!("Dependency tracking is disabled so invalidation is not allowed");
1881 }
1882 operation::InvalidateOperation::run(
1883 smallvec![task_id],
1884 #[cfg(feature = "task_dirty_cause")]
1885 TaskDirtyCause::Invalidator,
1886 self.execute_context(turbo_tasks),
1887 );
1888 }
1889
1890 fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks<TurboTasksBackend>) {
1891 if !self.should_track_dependencies() {
1892 panic!("Dependency tracking is disabled so invalidation is not allowed");
1893 }
1894 operation::InvalidateOperation::run(
1895 tasks.iter().copied().collect(),
1896 #[cfg(feature = "task_dirty_cause")]
1897 TaskDirtyCause::Unknown,
1898 self.execute_context(turbo_tasks),
1899 );
1900 }
1901
1902 fn invalidate_tasks_set(
1903 &self,
1904 tasks: &AutoSet<TaskId, BuildHasherDefault<FxHasher>, 2>,
1905 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1906 ) {
1907 if !self.should_track_dependencies() {
1908 panic!("Dependency tracking is disabled so invalidation is not allowed");
1909 }
1910 operation::InvalidateOperation::run(
1911 tasks.iter().copied().collect(),
1912 #[cfg(feature = "task_dirty_cause")]
1913 TaskDirtyCause::Unknown,
1914 self.execute_context(turbo_tasks),
1915 );
1916 }
1917
1918 fn invalidate_serialization(
1919 &self,
1920 task_id: TaskId,
1921 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1922 ) {
1923 if task_id.is_transient() {
1924 return;
1925 }
1926 let mut ctx = self.execute_context(turbo_tasks);
1927 let mut task = ctx.task(task_id, TaskDataCategory::Data);
1928 task.invalidate_serialization();
1929 }
1930
1931 fn debug_get_task_description(&self, task_id: TaskId) -> String {
1932 let task = self.storage.access_mut(task_id);
1933 if let Some(value) = task.get_persistent_task_type() {
1934 format!("{task_id:?} {}", value)
1935 } else if let Some(value) = task.get_transient_task_type() {
1936 format!("{task_id:?} {}", value)
1937 } else {
1938 format!("{task_id:?} unknown")
1939 }
1940 }
1941
1942 fn get_task_name(
1943 &self,
1944 task_id: TaskId,
1945 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1946 ) -> String {
1947 let mut ctx = self.execute_context(turbo_tasks);
1948 let task = ctx.open_or_create_task_storage(task_id, TaskDataCategory::Data);
1951 if let Some(value) = task.get_persistent_task_type() {
1952 value.to_string()
1953 } else if let Some(value) = task.get_transient_task_type() {
1954 value.to_string()
1955 } else {
1956 "unknown".to_string()
1957 }
1958 }
1959
1960 fn debug_get_cached_task_type(&self, task_id: TaskId) -> Option<CachedTaskTypeArc> {
1961 let task = self.storage.access_mut(task_id);
1962 task.get_persistent_task_type().cloned()
1963 }
1964
1965 fn task_execution_canceled(
1966 &self,
1967 task_id: TaskId,
1968 turbo_tasks: &TurboTasks<TurboTasksBackend>,
1969 ) {
1970 let mut ctx = self.execute_context(turbo_tasks);
1971 let mut task = ctx.task(task_id, TaskDataCategory::All);
1972 if let Some(in_progress) = task.take_in_progress() {
1973 match in_progress {
1974 InProgressState::Scheduled {
1975 done_event,
1976 reason: _,
1977 } => done_event.notify(usize::MAX),
1978 InProgressState::InProgress(InProgressStateInner { done_event, .. }) => {
1979 done_event.notify(usize::MAX)
1980 }
1981 InProgressState::Canceled => {}
1982 }
1983 }
1984 let in_progress_cells = task.take_in_progress_cells();
1987 if let Some(ref cells) = in_progress_cells {
1988 for state in cells.values() {
1989 state.event.notify(usize::MAX);
1990 }
1991 }
1992
1993 task.set_output(OutputValue::Error(Arc::new(TaskError::Error(Box::new(
1998 TaskErrorItem {
1999 message: TurboTasksExecutionErrorMessage::PIISafe(std::borrow::Cow::Borrowed(
2000 "task execution was canceled by shutdown",
2001 )),
2002 source: None,
2003 },
2004 )))));
2005
2006 let data_update = if self.should_track_dependencies() && !task_id.is_transient() {
2012 task.update_dirty_state(Some(Dirtyness::SessionDependent))
2013 } else {
2014 None
2015 };
2016
2017 let old = task.set_in_progress(InProgressState::Canceled);
2018 debug_assert!(old.is_none(), "InProgress already exists");
2019 drop(task);
2020
2021 if let Some(data_update) = data_update {
2022 AggregationUpdateQueue::run(data_update, &mut ctx);
2023 }
2024
2025 drop(in_progress_cells);
2026 }
2027
2028 fn try_start_task_execution(
2029 &self,
2030 task_id: TaskId,
2031 priority: TaskPriority,
2032 turbo_tasks: &TurboTasks<TurboTasksBackend>,
2033 ) -> Option<TaskExecutionSpec<'_>> {
2034 let execution_reason;
2035 let task_type;
2036 #[cfg(feature = "task_dirty_cause")]
2037 let cause;
2038 {
2039 let mut ctx = self.execute_context(turbo_tasks);
2040 let mut task = ctx.task(task_id, TaskDataCategory::All);
2041 task.assert_not_deleted("try_start_task_execution");
2042 task_type = task.get_task_type().to_owned();
2043 let once_task = matches!(task_type, TaskType::Transient(ref tt) if matches!(&**tt, TransientTask::Once(_)));
2044 if let Some(tasks) = task.prefetch() {
2045 drop(task);
2046 ctx.prepare_tasks(tasks, "prefetch");
2047 task = ctx.task(task_id, TaskDataCategory::All);
2048 }
2049 let in_progress = task.take_in_progress()?;
2050 let InProgressState::Scheduled { done_event, reason } = in_progress else {
2051 let old = task.set_in_progress(in_progress);
2052 debug_assert!(old.is_none(), "InProgress already exists");
2053 return None;
2054 };
2055 execution_reason = reason;
2056 #[cfg(feature = "task_dirty_cause")]
2057 {
2058 cause = match task.get_dirty() {
2059 Some(Dirtyness::Dirty { cause, .. }) => Some(cause.clone()),
2060 _ => None,
2061 };
2062 }
2063 let old = task.set_in_progress(InProgressState::InProgress(Box::new(
2064 InProgressStateInner {
2065 stale: false,
2066 once_task,
2067 done_event,
2068 marked_as_completed: false,
2069 new_children: Default::default(),
2070 },
2071 )));
2072 debug_assert!(old.is_none(), "InProgress already exists");
2073
2074 enum Collectible {
2076 Current(CollectibleRef, i32),
2077 Outdated(CollectibleRef),
2078 }
2079 let collectibles = task
2080 .iter_collectibles()
2081 .map(|(&collectible, &value)| Collectible::Current(collectible, value))
2082 .chain(
2083 task.iter_outdated_collectibles()
2084 .map(|(collectible, _count)| Collectible::Outdated(*collectible)),
2085 )
2086 .collect::<Vec<_>>();
2087 for collectible in collectibles {
2088 match collectible {
2089 Collectible::Current(collectible, value) => {
2090 let _ = task.insert_outdated_collectible(collectible, value);
2091 }
2092 Collectible::Outdated(collectible) => {
2093 if task
2094 .collectibles()
2095 .is_none_or(|m| m.get(&collectible).is_none())
2096 {
2097 task.remove_outdated_collectibles(&collectible);
2098 }
2099 }
2100 }
2101 }
2102
2103 if self.should_track_dependencies() {
2104 let cell_dependencies = task.iter_cell_dependencies().collect();
2109 task.set_outdated_cell_dependencies(cell_dependencies);
2110 let cell_dependencies_hashed = task.iter_cell_dependencies_hashed().collect();
2111 task.set_outdated_cell_dependencies_hashed(cell_dependencies_hashed);
2112
2113 let outdated_output_dependencies = task.iter_output_dependencies().collect();
2114 task.set_outdated_output_dependencies(outdated_output_dependencies);
2115 }
2116 }
2117
2118 let (span, future) = match task_type {
2119 TaskType::Cached(task_type) => {
2120 let CachedTaskType {
2121 native_fn,
2122 this,
2123 arg,
2124 } = &*task_type;
2125 (
2126 native_fn.span(
2127 task_id.persistence(),
2128 execution_reason,
2129 priority,
2130 #[cfg(feature = "task_dirty_cause")]
2131 cause.as_ref(),
2132 ),
2133 native_fn.execute(*this, &**arg),
2134 )
2135 }
2136 TaskType::Transient(task_type) => {
2137 let span = tracing::trace_span!(
2140 "turbo_tasks::root_task",
2141 inline_execution = tracing::field::Empty
2142 );
2143 let future = match &*task_type {
2144 TransientTask::Root(f) => f(),
2145 TransientTask::Once(future_mutex) => take(&mut *future_mutex.lock())?,
2146 };
2147 (span, future)
2148 }
2149 };
2150 Some(TaskExecutionSpec { future, span })
2151 }
2152
2153 fn task_execution_completed(
2157 &self,
2158 task_id: TaskId,
2159 result: Result<RawVc, TurboTasksExecutionError>,
2160 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2161 #[cfg(feature = "verify_determinism")] stateful: bool,
2162 has_invalidator: bool,
2163 turbo_tasks: &TurboTasks<TurboTasksBackend>,
2164 ) -> Option<TaskPriority> {
2165 #[cfg(not(feature = "trace_task_details"))]
2180 let span = tracing::trace_span!(
2181 "task execution completed",
2182 new_children = tracing::field::Empty
2183 )
2184 .entered();
2185 #[cfg(feature = "trace_task_details")]
2186 let span = tracing::trace_span!(
2187 "task execution completed",
2188 task_id = display(task_id),
2189 result = match result.as_ref() {
2190 Ok(value) => display(either::Either::Left(value)),
2191 Err(err) => display(either::Either::Right(err)),
2192 },
2193 new_children = tracing::field::Empty,
2194 immutable = tracing::field::Empty,
2195 new_output = tracing::field::Empty,
2196 output_dependents = tracing::field::Empty,
2197 aggregation_number = tracing::field::Empty,
2198 stale = tracing::field::Empty,
2199 )
2200 .entered();
2201
2202 let is_error = result.is_err();
2203
2204 let mut ctx = self.execute_context(turbo_tasks);
2205
2206 let TaskExecutionCompletePrepareResult {
2207 new_children,
2208 is_now_immutable,
2209 #[cfg(feature = "verify_determinism")]
2210 no_output_set,
2211 new_output,
2212 #[cfg(feature = "task_dirty_cause")]
2213 function_id,
2214 output_dependent_tasks,
2215 is_recomputation,
2216 is_session_dependent,
2217 } = match self.task_execution_completed_prepare(
2218 &mut ctx,
2219 #[cfg(feature = "trace_task_details")]
2220 &span,
2221 task_id,
2222 result,
2223 cell_counters,
2224 #[cfg(feature = "verify_determinism")]
2225 stateful,
2226 has_invalidator,
2227 ) {
2228 Ok(r) => r,
2229 Err(stale_priority) => {
2230 #[cfg(feature = "trace_task_details")]
2232 span.record("stale", "prepare");
2233 return Some(stale_priority);
2234 }
2235 };
2236
2237 #[cfg(feature = "trace_task_details")]
2238 span.record("new_output", new_output.is_some());
2239 #[cfg(feature = "trace_task_details")]
2240 span.record("output_dependents", output_dependent_tasks.len());
2241
2242 if !output_dependent_tasks.is_empty() {
2247 self.task_execution_completed_invalidate_output_dependent(
2248 &mut ctx,
2249 task_id,
2250 #[cfg(feature = "task_dirty_cause")]
2251 function_id,
2252 output_dependent_tasks,
2253 );
2254 }
2255
2256 let has_new_children = !new_children.is_empty();
2257 span.record("new_children", new_children.len());
2258
2259 if has_new_children
2260 && let Some(stale_priority) =
2261 self.task_execution_completed_connect(&mut ctx, task_id, new_children)
2262 {
2263 #[cfg(feature = "trace_task_details")]
2265 span.record("stale", "connect");
2266 return Some(stale_priority);
2267 }
2268
2269 let (stale_priority, in_progress_cells) = self.task_execution_completed_finish(
2270 &mut ctx,
2271 task_id,
2272 #[cfg(feature = "verify_determinism")]
2273 no_output_set,
2274 new_output,
2275 is_now_immutable,
2276 is_session_dependent,
2277 );
2278 if let Some(stale_priority) = stale_priority {
2279 #[cfg(feature = "trace_task_details")]
2281 span.record("stale", "finish");
2282 return Some(stale_priority);
2283 }
2284
2285 let removed_data = self.task_execution_completed_cleanup(
2286 &mut ctx,
2287 task_id,
2288 cell_counters,
2289 is_error,
2290 is_recomputation,
2291 );
2292
2293 drop(removed_data);
2295 drop(in_progress_cells);
2296
2297 None
2298 }
2299
2300 fn task_execution_completed_prepare(
2301 &self,
2302 ctx: &mut impl ExecuteContext<'_>,
2303 #[cfg(feature = "trace_task_details")] span: &Span,
2304 task_id: TaskId,
2305 result: Result<RawVc, TurboTasksExecutionError>,
2306 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2307 #[cfg(feature = "verify_determinism")] stateful: bool,
2308 has_invalidator: bool,
2309 ) -> Result<TaskExecutionCompletePrepareResult, TaskPriority> {
2310 let mut task = ctx.task(task_id, TaskDataCategory::All);
2311 let is_recomputation = task.is_dirty().is_none();
2312 let is_session_dependent = self.should_track_dependencies()
2315 && matches!(task.get_task_type(), TaskTypeRef::Cached(tt) if tt.native_fn.is_session_dependent);
2316 let Some(in_progress) = task.get_in_progress_mut() else {
2317 panic!("Task execution completed, but task is not in progress: {task:#?}");
2318 };
2319 if matches!(in_progress, InProgressState::Canceled) {
2320 return Ok(TaskExecutionCompletePrepareResult {
2321 new_children: Default::default(),
2322 is_now_immutable: false,
2323 #[cfg(feature = "verify_determinism")]
2324 no_output_set: false,
2325 #[cfg(feature = "task_dirty_cause")]
2326 function_id: None,
2327 new_output: None,
2328 output_dependent_tasks: Default::default(),
2329 is_recomputation,
2330 is_session_dependent,
2331 });
2332 }
2333 let &mut InProgressState::InProgress(InProgressStateInner {
2334 stale,
2335 ref mut new_children,
2336 once_task: is_once_task,
2337 ..
2338 }) = in_progress
2339 else {
2340 panic!("Task execution completed, but task is not in progress: {task:#?}");
2341 };
2342
2343 #[cfg(not(feature = "no_fast_stale"))]
2345 if stale && !is_once_task {
2346 let stale_priority = compute_stale_priority(&task);
2347 let Some(InProgressState::InProgress(InProgressStateInner {
2348 done_event,
2349 mut new_children,
2350 ..
2351 })) = task.take_in_progress()
2352 else {
2353 unreachable!();
2354 };
2355 let old = task.set_in_progress(InProgressState::Scheduled {
2356 done_event,
2357 reason: TaskExecutionReason::Stale,
2358 });
2359 debug_assert!(old.is_none(), "InProgress already exists");
2360 for task in task.iter_children() {
2363 new_children.remove(&task);
2364 }
2365 drop(task);
2366
2367 AggregationUpdateQueue::run(
2370 AggregationUpdateJob::DecreaseActiveCounts {
2371 task_ids: new_children.into_iter().collect(),
2372 },
2373 ctx,
2374 );
2375 return Err(stale_priority);
2376 }
2377
2378 let mut new_children = take(new_children);
2380
2381 #[cfg(feature = "task_dirty_cause")]
2383 let function_id = match task.get_task_type() {
2384 TaskTypeRef::Cached(task_type) => {
2385 Some(turbo_tasks::registry::get_function_id(task_type.native_fn))
2386 }
2387 TaskTypeRef::Transient(_) => None,
2388 };
2389
2390 #[cfg(feature = "verify_determinism")]
2392 if stateful {
2393 task.set_stateful(true);
2394 }
2395
2396 if has_invalidator {
2398 task.set_invalidator(true);
2399 }
2400
2401 if result.is_ok() || is_recomputation {
2411 let old_counters: FxHashMap<_, _> = task
2412 .iter_cell_type_max_index()
2413 .map(|(&k, &v)| (k, v))
2414 .collect();
2415 let mut counters_to_remove = old_counters.clone();
2416
2417 for (&cell_type, &max_index) in cell_counters.iter() {
2418 if let Some(old_max_index) = counters_to_remove.remove(&cell_type) {
2419 if old_max_index != max_index {
2420 task.insert_cell_type_max_index(cell_type, max_index);
2421 }
2422 } else {
2423 task.insert_cell_type_max_index(cell_type, max_index);
2424 }
2425 }
2426 for (cell_type, _) in counters_to_remove {
2427 task.remove_cell_type_max_index(&cell_type);
2428 }
2429 }
2430
2431 let mut queue = AggregationUpdateQueue::new();
2432
2433 let mut old_edges = Vec::new();
2434
2435 let has_children = !new_children.is_empty();
2436 let is_immutable = task.immutable();
2437 let task_dependencies_for_immutable =
2438 if !is_immutable
2440 && !is_session_dependent
2442 && !task.invalidator()
2444 && task.is_collectibles_dependencies_empty()
2446 {
2447 Some(
2448 task.iter_output_dependencies()
2450 .chain(task.iter_cell_dependencies().map(|r| r.task))
2451 .chain(task.iter_cell_dependencies_hashed().map(|(r, _)| r.task))
2452 .collect::<FxHashSet<_>>(),
2453 )
2454 } else {
2455 None
2456 };
2457
2458 if has_children {
2459 let _aggregation_number =
2461 prepare_new_children(task_id, &mut task, &new_children, &mut queue);
2462
2463 #[cfg(feature = "trace_task_details")]
2464 span.record("aggregation_number", _aggregation_number);
2465
2466 old_edges.extend(
2468 task.iter_children()
2469 .filter(|task| !new_children.remove(task))
2470 .map(OutdatedEdge::Child),
2471 );
2472 } else {
2473 old_edges.extend(task.iter_children().map(OutdatedEdge::Child));
2474 }
2475
2476 old_edges.extend(
2477 task.iter_outdated_collectibles()
2478 .map(|(&collectible, &count)| OutdatedEdge::Collectible(collectible, count)),
2479 );
2480
2481 if self.should_track_dependencies() {
2482 old_edges.extend(
2489 task.iter_outdated_cell_dependencies()
2490 .map(OutdatedEdge::CellDependency),
2491 );
2492 old_edges.extend(
2493 task.iter_outdated_cell_dependencies_hashed()
2494 .map(|(r, k)| OutdatedEdge::HashedCellDependency(r, k)),
2495 );
2496 old_edges.extend(
2497 task.iter_outdated_output_dependencies()
2498 .map(OutdatedEdge::OutputDependency),
2499 );
2500 }
2501
2502 let current_output = task.get_output();
2504 #[cfg(feature = "verify_determinism")]
2505 let no_output_set = current_output.is_none();
2506 let new_output = match result.map(RawVc::unpack) {
2507 Ok(RawVcUnpacked::TaskOutput(output_task_id)) => {
2508 if let Some(OutputValue::Output(current_task_id)) = current_output
2509 && *current_task_id == output_task_id
2510 {
2511 None
2512 } else {
2513 Some(OutputValue::Output(output_task_id))
2514 }
2515 }
2516 Ok(RawVcUnpacked::TaskCell(output_task_id, cell)) => {
2517 if let Some(OutputValue::Cell(CellRef {
2518 task: current_task_id,
2519 cell: current_cell,
2520 })) = current_output
2521 && *current_task_id == output_task_id
2522 && *current_cell == cell
2523 {
2524 None
2525 } else {
2526 Some(OutputValue::Cell(CellRef {
2527 task: output_task_id,
2528 cell,
2529 }))
2530 }
2531 }
2532 Ok(RawVcUnpacked::LocalOutput(..)) => {
2533 panic!("Non-local tasks must not return a local Vc");
2534 }
2535 Err(err) => {
2536 if let Some(OutputValue::Error(old_error)) = current_output
2537 && **old_error == err
2538 {
2539 None
2540 } else {
2541 Some(OutputValue::Error(Arc::new((&err).into())))
2542 }
2543 }
2544 };
2545 let mut output_dependent_tasks = SmallVec::<[_; 4]>::new();
2546 if new_output.is_some() && ctx.should_track_dependencies() {
2548 output_dependent_tasks = task.iter_output_dependent().collect();
2549 }
2550
2551 drop(task);
2552
2553 let mut is_now_immutable = false;
2555 if let Some(dependencies) = task_dependencies_for_immutable
2556 && dependencies
2557 .iter()
2558 .all(|&task_id| ctx.task(task_id, TaskDataCategory::Data).immutable())
2559 {
2560 is_now_immutable = true;
2561 }
2562 #[cfg(feature = "trace_task_details")]
2563 span.record("immutable", is_immutable || is_now_immutable);
2564
2565 if !queue.is_empty() || !old_edges.is_empty() {
2566 #[cfg(any(
2567 feature = "trace_task_completion",
2568 feature = "trace_aggregation_update_stats"
2569 ))]
2570 let _span =
2571 tracing::trace_span!("remove old edges and prepare new children", stats = Empty)
2572 .entered();
2573 #[cfg(feature = "trace_aggregation_update_stats")]
2577 {
2578 let stats = CleanupOldEdgesOperation::run(task_id, old_edges, queue, ctx);
2579 _span.record("stats", tracing::field::debug(stats));
2580 }
2581 #[cfg(not(feature = "trace_aggregation_update_stats"))]
2582 CleanupOldEdgesOperation::run(task_id, old_edges, queue, ctx);
2583 }
2584
2585 Ok(TaskExecutionCompletePrepareResult {
2586 new_children,
2587 is_now_immutable,
2588 #[cfg(feature = "verify_determinism")]
2589 no_output_set,
2590 #[cfg(feature = "task_dirty_cause")]
2591 function_id,
2592 new_output,
2593 output_dependent_tasks,
2594 is_recomputation,
2595 is_session_dependent,
2596 })
2597 }
2598
2599 fn task_execution_completed_invalidate_output_dependent(
2600 &self,
2601 ctx: &mut impl ExecuteContext<'_>,
2602 task_id: TaskId,
2603 #[cfg(feature = "task_dirty_cause")] function_id: Option<FunctionId>,
2604 output_dependent_tasks: SmallVec<[TaskId; 4]>,
2605 ) {
2606 debug_assert!(!output_dependent_tasks.is_empty());
2607
2608 #[cfg(feature = "task_dirty_cause")]
2609 let cause = match function_id {
2610 Some(function) => TaskDirtyCause::OutputChange { function },
2611 None => TaskDirtyCause::RootOutputChange,
2612 };
2613
2614 if output_dependent_tasks.len() > 1 {
2615 ctx.prepare_tasks(
2616 output_dependent_tasks
2617 .iter()
2618 .map(|&id| (id, TaskDataCategory::All)),
2619 "invalidate output dependents",
2620 );
2621 }
2622
2623 fn process_output_dependents(
2624 ctx: &mut impl ExecuteContext<'_>,
2625 task_id: TaskId,
2626 #[cfg(feature = "task_dirty_cause")] cause: &TaskDirtyCause,
2627 dependent_task_id: TaskId,
2628 queue: &mut AggregationUpdateQueue,
2629 ) {
2630 #[cfg(feature = "trace_task_output_dependencies")]
2631 let span = tracing::trace_span!(
2632 "invalidate output dependency",
2633 task = %task_id,
2634 dependent_task = %dependent_task_id,
2635 result = tracing::field::Empty,
2636 )
2637 .entered();
2638 let mut make_stale = true;
2639 let mut dependent = ctx.task(dependent_task_id, TaskDataCategory::All);
2640 let transient_task_type = dependent.get_transient_task_type();
2641 if transient_task_type.is_some_and(|tt| matches!(&**tt, TransientTask::Once(_))) {
2642 #[cfg(feature = "trace_task_output_dependencies")]
2644 span.record("result", "once task");
2645 return;
2646 }
2647 if dependent.outdated_output_dependencies_contains(&task_id) {
2648 #[cfg(feature = "trace_task_output_dependencies")]
2649 span.record("result", "outdated dependency");
2650 make_stale = false;
2655 } else if !dependent.output_dependencies_contains(&task_id) {
2656 #[cfg(feature = "trace_task_output_dependencies")]
2659 span.record("result", "no backward dependency");
2660 return;
2661 }
2662 make_task_dirty_internal(
2663 &mut dependent,
2664 make_stale,
2665 #[cfg(feature = "task_dirty_cause")]
2666 cause.clone(),
2667 queue,
2668 ctx,
2669 );
2670 #[cfg(feature = "trace_task_output_dependencies")]
2671 span.record("result", "marked dirty");
2672 }
2673
2674 if output_dependent_tasks.len() > DEPENDENT_TASKS_DIRTY_PARALLELIZATION_THRESHOLD {
2675 let chunk_size = good_chunk_size(output_dependent_tasks.len());
2676 let chunks = into_chunks(output_dependent_tasks.to_vec(), chunk_size);
2677 let _ = scope_bounded(chunks.len(), |scope| {
2678 for chunk in chunks {
2679 let child_ctx = ctx.child_context();
2680 #[cfg(feature = "task_dirty_cause")]
2681 let cause = &cause;
2682 scope.spawn(move || {
2683 let mut ctx = child_ctx.create();
2684 let mut queue = AggregationUpdateQueue::new();
2685 for dependent_task_id in chunk {
2686 process_output_dependents(
2687 &mut ctx,
2688 task_id,
2689 #[cfg(feature = "task_dirty_cause")]
2690 cause,
2691 dependent_task_id,
2692 &mut queue,
2693 )
2694 }
2695 queue.execute(&mut ctx);
2696 });
2697 }
2698 });
2699 } else {
2700 let mut queue = AggregationUpdateQueue::new();
2701 for dependent_task_id in output_dependent_tasks {
2702 process_output_dependents(
2703 ctx,
2704 task_id,
2705 #[cfg(feature = "task_dirty_cause")]
2706 &cause,
2707 dependent_task_id,
2708 &mut queue,
2709 );
2710 }
2711 queue.execute(ctx);
2712 }
2713 }
2714
2715 fn task_execution_completed_connect(
2716 &self,
2717 ctx: &mut impl ExecuteContext<'_>,
2718 task_id: TaskId,
2719 new_children: FxHashSet<TaskId>,
2720 ) -> Option<TaskPriority> {
2721 debug_assert!(!new_children.is_empty());
2722
2723 let mut task = ctx.task(task_id, TaskDataCategory::All);
2724 let Some(in_progress) = task.get_in_progress() else {
2725 panic!("Task execution completed, but task is not in progress: {task:#?}");
2726 };
2727 if matches!(in_progress, InProgressState::Canceled) {
2728 return None;
2730 }
2731 let InProgressState::InProgress(InProgressStateInner {
2732 #[cfg(not(feature = "no_fast_stale"))]
2733 stale,
2734 once_task: is_once_task,
2735 ..
2736 }) = in_progress
2737 else {
2738 panic!("Task execution completed, but task is not in progress: {task:#?}");
2739 };
2740
2741 #[cfg(not(feature = "no_fast_stale"))]
2743 if *stale && !is_once_task {
2744 let stale_priority = compute_stale_priority(&task);
2745 let Some(InProgressState::InProgress(InProgressStateInner { done_event, .. })) =
2746 task.take_in_progress()
2747 else {
2748 unreachable!();
2749 };
2750 let old = task.set_in_progress(InProgressState::Scheduled {
2751 done_event,
2752 reason: TaskExecutionReason::Stale,
2753 });
2754 debug_assert!(old.is_none(), "InProgress already exists");
2755 drop(task);
2756
2757 AggregationUpdateQueue::run(
2760 AggregationUpdateJob::DecreaseActiveCounts {
2761 task_ids: new_children.into_iter().collect(),
2762 },
2763 ctx,
2764 );
2765 return Some(stale_priority);
2766 }
2767
2768 let has_active_count = ctx.should_track_activeness()
2769 && task
2770 .get_activeness()
2771 .is_some_and(|activeness| activeness.active_counter > 0);
2772 connect_children(
2773 ctx,
2774 task_id,
2775 task,
2776 new_children,
2777 has_active_count,
2778 ctx.should_track_activeness(),
2779 );
2780
2781 None
2782 }
2783
2784 #[allow(clippy::type_complexity)]
2785 fn task_execution_completed_finish(
2786 &self,
2787 ctx: &mut impl ExecuteContext<'_>,
2788 task_id: TaskId,
2789 #[cfg(feature = "verify_determinism")] no_output_set: bool,
2790 new_output: Option<OutputValue>,
2791 is_now_immutable: bool,
2792 is_session_dependent: bool,
2793 ) -> (
2794 Option<TaskPriority>,
2795 Option<
2796 auto_hash_map::AutoMap<CellId, InProgressCellState, BuildHasherDefault<FxHasher>, 1>,
2797 >,
2798 ) {
2799 let mut task = ctx.task(task_id, TaskDataCategory::All);
2800 let Some(in_progress) = task.take_in_progress() else {
2801 panic!("Task execution completed, but task is not in progress: {task:#?}");
2802 };
2803 if matches!(in_progress, InProgressState::Canceled) {
2804 return (None, None);
2806 }
2807 let InProgressState::InProgress(InProgressStateInner {
2808 done_event,
2809 once_task: is_once_task,
2810 stale,
2811 marked_as_completed: _,
2812 new_children,
2813 }) = in_progress
2814 else {
2815 panic!("Task execution completed, but task is not in progress: {task:#?}");
2816 };
2817 debug_assert!(new_children.is_empty());
2818
2819 if stale && !is_once_task {
2821 let stale_priority = compute_stale_priority(&task);
2822 let old = task.set_in_progress(InProgressState::Scheduled {
2823 done_event,
2824 reason: TaskExecutionReason::Stale,
2825 });
2826 debug_assert!(old.is_none(), "InProgress already exists");
2827 return (Some(stale_priority), None);
2828 }
2829
2830 let mut old_content = None;
2832 if let Some(value) = new_output {
2833 old_content = task.set_output(value);
2834 }
2835
2836 if is_now_immutable {
2839 task.set_immutable(true);
2840 }
2841
2842 let in_progress_cells = task.take_in_progress_cells();
2844 if let Some(ref cells) = in_progress_cells {
2845 for state in cells.values() {
2846 state.event.notify(usize::MAX);
2847 }
2848 }
2849
2850 let new_dirtyness = if is_session_dependent {
2852 Some(Dirtyness::SessionDependent)
2853 } else {
2854 None
2855 };
2856 #[cfg(feature = "verify_determinism")]
2857 let dirty_changed = task.get_dirty().cloned() != new_dirtyness;
2858 let data_update = task.update_dirty_state(new_dirtyness);
2859
2860 #[cfg(feature = "verify_determinism")]
2864 let stale_priority: Option<TaskPriority> =
2865 ((dirty_changed || no_output_set) && !task_id.is_transient() && !is_once_task)
2866 .then(TaskPriority::leaf);
2867 #[cfg(not(feature = "verify_determinism"))]
2868 let stale_priority: Option<TaskPriority> = None;
2869 if stale_priority.is_some() {
2870 let old = task.set_in_progress(InProgressState::Scheduled {
2871 done_event,
2872 reason: TaskExecutionReason::Stale,
2873 });
2874 debug_assert!(old.is_none(), "InProgress already exists");
2875 drop(task);
2876 } else {
2877 drop(task);
2878
2879 done_event.notify(usize::MAX);
2881 }
2882
2883 drop(old_content);
2884
2885 if let Some(data_update) = data_update {
2886 AggregationUpdateQueue::run(data_update, ctx);
2887 }
2888
2889 (stale_priority, in_progress_cells)
2891 }
2892
2893 fn task_execution_completed_cleanup(
2894 &self,
2895 ctx: &mut impl ExecuteContext<'_>,
2896 task_id: TaskId,
2897 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
2898 is_error: bool,
2899 is_recomputation: bool,
2900 ) -> Vec<SharedReference> {
2901 let mut task = ctx.task(task_id, TaskDataCategory::All);
2902 let mut removed_cell_data = Vec::new();
2903 if !is_error || is_recomputation {
2909 let to_remove: Vec<_> = task
2915 .iter_cell_data()
2916 .filter_map(|(cell, _)| {
2917 cell_counters
2918 .get(&cell.type_id())
2919 .is_none_or(|start_index| cell.index() >= *start_index)
2920 .then_some(*cell)
2921 })
2922 .collect();
2923 removed_cell_data.reserve_exact(to_remove.len());
2924 for cell in to_remove {
2925 if let Some(data) =
2926 task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence)
2927 {
2928 removed_cell_data.push(data);
2929 }
2930 }
2931 let to_remove_hash: Vec<_> = task
2933 .iter_cell_data_hash()
2934 .filter_map(|(cell, _)| {
2935 cell_counters
2936 .get(&cell.type_id())
2937 .is_none_or(|start_index| cell.index() >= *start_index)
2938 .then_some(*cell)
2939 })
2940 .collect();
2941 for cell in to_remove_hash {
2942 task.remove_cell_data_hash(&cell);
2943 }
2944 }
2945
2946 task.cleanup_after_execution();
2950
2951 drop(task);
2952
2953 removed_cell_data
2955 }
2956
2957 fn log_unrecoverable_persist_error() {
2960 eprintln!(
2961 "Persisting is disabled for this session due to an unrecoverable error. Stopping the \
2962 background persisting process."
2963 );
2964 }
2965
2966 fn run_backend_job<'a>(
2967 &'a self,
2968 job: TurboTasksBackendJob,
2969 turbo_tasks: &'a TurboTasks<TurboTasksBackend>,
2970 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
2971 Box::pin(async move {
2972 match job {
2973 TurboTasksBackendJob::Snapshot => {
2974 debug_assert!(self.should_persist());
2975
2976 static IDLE_TIMEOUT: LazyLock<Duration> = LazyLock::new(|| {
2979 std::env::var("TURBO_ENGINE_SNAPSHOT_IDLE_TIMEOUT_MILLIS")
2980 .ok()
2981 .and_then(|v| v.parse::<u64>().ok())
2982 .map(Duration::from_millis)
2983 .unwrap_or(Duration::from_secs(2))
2984 });
2985
2986 static MIN_SNAPSHOT_ACTIVE_TIME: LazyLock<Duration> = LazyLock::new(|| {
2994 std::env::var("TURBO_ENGINE_SNAPSHOT_MIN_ACTIVE_TIME_MILLIS")
2995 .ok()
2996 .and_then(|v| v.parse::<u64>().ok())
2997 .map(Duration::from_millis)
2998 .unwrap_or(Duration::from_secs(1))
2999 });
3000
3001 let mut last_snapshot = self.start_time;
3002 let mut idle_start_listener = self.idle_start_event.listen();
3003 let mut idle_end_listener = self.idle_end_event.listen();
3004 let mut fresh_idle = true;
3007 let mut is_first = true;
3008 let mut eviction_control = EvictionControl::new(self.options.eviction_mode);
3011 let mut active_time = Stopwatch::new();
3016 'outer: loop {
3017 const FIRST_SNAPSHOT_WAIT: Duration = Duration::from_secs(300);
3018 const SNAPSHOT_INTERVAL: Duration = Duration::from_secs(120);
3019 let idle_timeout = *IDLE_TIMEOUT;
3020 let (time, mut reason) = if is_first {
3021 (FIRST_SNAPSHOT_WAIT, SnapshotReason::InitialSnapshotTimeout)
3022 } else {
3023 (SNAPSHOT_INTERVAL, SnapshotReason::RegularSnapshotInterval)
3024 };
3025
3026 if !turbo_tasks.is_idle() {
3029 active_time.start();
3030 }
3031
3032 let until = last_snapshot + time;
3033 if until > Instant::now() {
3034 let mut stop_listener = self.stopping_event.listen();
3035 if *self.stopping.read() {
3036 return;
3037 }
3038 let mut idle_time = if turbo_tasks.is_idle() && fresh_idle {
3039 Instant::now() + idle_timeout
3040 } else {
3041 far_future()
3042 };
3043 loop {
3044 tokio::select! {
3045 _ = &mut stop_listener => {
3046 return;
3047 },
3048 _ = &mut idle_start_listener => {
3049 active_time.stop();
3051 idle_time = Instant::now() + idle_timeout;
3052 idle_start_listener = self.idle_start_event.listen()
3053 },
3054 _ = &mut idle_end_listener => {
3055 active_time.start();
3057 idle_time = far_future();
3058 idle_end_listener = self.idle_end_event.listen()
3059 },
3060 _ = tokio::time::sleep_until(until) => {
3061 break;
3062 },
3063 _ = tokio::time::sleep_until(idle_time) => {
3064 if turbo_tasks.is_idle() {
3065 reason = SnapshotReason::IdleTimeout;
3066 break;
3067 }
3068 },
3069 }
3070 }
3071 }
3072
3073 if active_time.elapsed() < *MIN_SNAPSHOT_ACTIVE_TIME {
3080 fresh_idle = false;
3087 is_first = false;
3088 last_snapshot = Instant::now();
3089 continue 'outer;
3090 }
3091
3092 let background_span =
3096 tracing::info_span!(parent: None, "background snapshot");
3097 match self.snapshot_and_persist(background_span.id(), reason, turbo_tasks) {
3098 Err(err) => {
3099 eprintln!("Persisting failed: {err:?}");
3102 Self::log_unrecoverable_persist_error();
3103 return;
3104 }
3105 Ok((snapshot_start, new_data, _gc_outcome)) => {
3106 fresh_idle = new_data;
3108 is_first = false;
3109 last_snapshot = snapshot_start;
3110 active_time.reset();
3118
3119 macro_rules! check_idle_ended {
3123 () => {{
3124 tokio::select! {
3125 biased;
3126 _ = &mut idle_end_listener => {
3127 idle_end_listener = self.idle_end_event.listen();
3128 true
3129 },
3130 _ = std::future::ready(()) => false,
3131 }
3132 }};
3133 }
3134 let ran_eviction = if eviction_control.should_evict(new_data) {
3148 self.storage.evict_after_snapshot(background_span.id());
3153 true
3154 } else {
3155 false
3156 };
3157
3158 let mut ran_compaction = false;
3165 const MAX_IDLE_COMPACTION_PASSES: usize = 10;
3166 for _ in 0..MAX_IDLE_COMPACTION_PASSES {
3167 if check_idle_ended!() {
3168 continue 'outer;
3169 }
3170 let compact_span = tracing::info_span!(
3174 parent: background_span.id(),
3175 "compact database",
3176 stats = tracing::field::Empty,
3177 )
3178 .entered();
3179 match self.backing_storage.compact() {
3180 Ok(Some(stats)) => {
3181 compact_span.record("stats", display(stats));
3182 ran_compaction = true;
3183 }
3184 Ok(None) => break,
3185 Err(err) => {
3186 eprintln!("Compaction failed: {err:?}");
3187 if self.backing_storage.has_unrecoverable_write_error()
3188 {
3189 Self::log_unrecoverable_persist_error();
3190 return;
3191 }
3192 break;
3193 }
3194 }
3195 }
3196 if !check_idle_ended!()
3201 && (new_data || ran_compaction || ran_eviction)
3202 {
3203 TurboMalloc::collect(true);
3204 }
3205
3206 if ran_eviction {
3209 eviction_control.record_eviction();
3210 }
3211 }
3212 }
3213 }
3214 }
3215 }
3216 })
3217 }
3218
3219 fn try_read_own_task_cell(
3220 &self,
3221 task_id: TaskId,
3222 cell: CellId,
3223 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3224 ) -> Result<TypedCellContent> {
3225 let mut ctx = self.execute_context(turbo_tasks);
3226 let task = ctx.task(task_id, TaskDataCategory::Data);
3227 task.assert_not_deleted("try_read_own_task_cell");
3228 if let Some(content) = task.get_cell_data(&cell).cloned() {
3229 Ok(CellContent(Some(content)).into_typed(cell.type_id()))
3230 } else {
3231 Ok(CellContent(None).into_typed(cell.type_id()))
3232 }
3233 }
3234
3235 fn read_task_collectibles(
3236 &self,
3237 task_id: TaskId,
3238 collectible_type: TraitTypeId,
3239 reader_id: Option<TaskId>,
3240 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3241 ) -> AutoMap<RawVc, i32, BuildHasherDefault<FxHasher>, 1> {
3242 let mut ctx = self.execute_context(turbo_tasks);
3243 let mut collectibles = AutoMap::default();
3244 {
3245 let mut task = ctx.task(task_id, TaskDataCategory::All);
3246 task.assert_not_deleted("read_task_collectibles");
3247 if task
3248 .get_persistent_task_type()
3249 .is_some_and(|t| !t.native_fn.is_root)
3250 {
3251 drop(task);
3252 panic!(
3253 "Reading collectibles of non-root task {} (reader: {}). The `root` attribute \
3254 is missing on the task.",
3255 self.debug_get_task_description(task_id),
3256 reader_id.map_or_else(
3257 || "unknown".to_string(),
3258 |r| self.debug_get_task_description(r)
3259 )
3260 );
3261 }
3262 for (collectible, count) in task.iter_aggregated_collectibles() {
3263 if *count > 0 && collectible.collectible_type == collectible_type {
3264 *collectibles
3265 .entry(RawVc::task_cell(
3266 collectible.cell.task,
3267 collectible.cell.cell,
3268 ))
3269 .or_insert(0) += 1;
3270 }
3271 }
3272 for (&collectible, &count) in task.iter_collectibles() {
3273 if collectible.collectible_type == collectible_type {
3274 *collectibles
3275 .entry(RawVc::task_cell(
3276 collectible.cell.task,
3277 collectible.cell.cell,
3278 ))
3279 .or_insert(0) += count;
3280 }
3281 }
3282 if let Some(reader_id) = reader_id {
3283 let _ = task.add_collectibles_dependents((collectible_type, reader_id));
3284 }
3285 }
3286 if let Some(reader_id) = reader_id {
3287 let mut reader = ctx.task(reader_id, TaskDataCategory::Data);
3288 let target = CollectiblesRef {
3289 task: task_id,
3290 collectible_type,
3291 };
3292 if !reader.remove_outdated_collectibles_dependencies(&target) {
3293 let _ = reader.add_collectibles_dependencies(target);
3294 }
3295 }
3296 collectibles
3297 }
3298
3299 fn emit_collectible(
3300 &self,
3301 collectible_type: TraitTypeId,
3302 collectible: RawVc,
3303 task_id: TaskId,
3304 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3305 ) {
3306 self.assert_valid_collectible(task_id, collectible);
3307
3308 let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3309 panic!("Collectibles need to be resolved");
3310 };
3311 let cell = CellRef {
3312 task: collectible_task,
3313 cell,
3314 };
3315 operation::UpdateCollectibleOperation::run(
3316 task_id,
3317 CollectibleRef {
3318 collectible_type,
3319 cell,
3320 },
3321 1,
3322 self.execute_context(turbo_tasks),
3323 );
3324 }
3325
3326 fn unemit_collectible(
3327 &self,
3328 collectible_type: TraitTypeId,
3329 collectible: RawVc,
3330 count: u32,
3331 task_id: TaskId,
3332 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3333 ) {
3334 self.assert_valid_collectible(task_id, collectible);
3335
3336 let Some((collectible_task, cell)) = collectible.as_task_cell() else {
3337 panic!("Collectibles need to be resolved");
3338 };
3339 let cell = CellRef {
3340 task: collectible_task,
3341 cell,
3342 };
3343 operation::UpdateCollectibleOperation::run(
3344 task_id,
3345 CollectibleRef {
3346 collectible_type,
3347 cell,
3348 },
3349 -(i32::try_from(count).unwrap()),
3350 self.execute_context(turbo_tasks),
3351 );
3352 }
3353
3354 fn update_task_cell(
3355 &self,
3356 task_id: TaskId,
3357 cell: CellId,
3358 content: CellContent,
3359 updated_key_hashes: Option<SmallVec<[u64; 2]>>,
3360 content_hash: Option<CellHash>,
3361 verification_mode: VerificationMode,
3362 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3363 ) {
3364 operation::UpdateCellOperation::run(
3365 task_id,
3366 cell,
3367 content,
3368 updated_key_hashes,
3369 content_hash,
3370 verification_mode,
3371 self.execute_context(turbo_tasks),
3372 );
3373 }
3374
3375 fn mark_own_task_as_finished(&self, task: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
3376 let mut ctx = self.execute_context(turbo_tasks);
3377 let mut task = ctx.task(task, TaskDataCategory::Data);
3378 if let Some(InProgressState::InProgress(InProgressStateInner {
3379 marked_as_completed,
3380 ..
3381 })) = task.get_in_progress_mut()
3382 {
3383 *marked_as_completed = true;
3384 }
3389 }
3390
3391 fn connect_task(
3392 &self,
3393 task: TaskId,
3394 parent_task: Option<TaskId>,
3395 turbo_tasks: &TurboTasks<TurboTasksBackend>,
3396 ) {
3397 self.assert_not_persistent_calling_transient(parent_task, task);
3398 ConnectChildOperation::run(
3399 parent_task,
3400 task,
3401 false,
3402 self.execute_context(turbo_tasks),
3403 );
3404 }
3405
3406 fn create_transient_task(&self, task_type: TransientTaskType) -> TaskId {
3407 let task_id = self.transient_task_id_factory.get();
3408 {
3409 let mut task = self.storage.access_mut(task_id);
3410 task.init_transient_task(task_id, task_type, self.should_track_activeness());
3411 }
3412 #[cfg(feature = "verify_aggregation_graph")]
3413 self.root_tasks.lock().insert(task_id);
3414 task_id
3415 }
3416
3417 fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<TurboTasksBackend>) {
3418 let Some(mut ctx) = self.try_execute_context(turbo_tasks) else {
3423 return;
3424 };
3425
3426 #[cfg(feature = "verify_aggregation_graph")]
3427 self.root_tasks.lock().remove(&task_id);
3428
3429 let mut task = ctx.task(task_id, TaskDataCategory::All);
3430 let is_dirty = task.is_dirty();
3431 let has_dirty_containers = task.has_dirty_containers();
3432 if is_dirty.is_some() || has_dirty_containers {
3433 if let Some(activeness_state) = task.get_activeness_mut() {
3434 activeness_state.unset_root_type();
3436 activeness_state.set_active_until_clean();
3437 };
3438 } else {
3439 if let Some(activeness_state) = task.take_activeness() {
3440 activeness_state.all_clean_event.notify(usize::MAX);
3443 }
3444 let old_edges = capture_all_edges(&task);
3446 drop(task);
3447
3448 if !old_edges.is_empty() {
3449 CleanupOldEdgesOperation::run(
3450 task_id,
3451 old_edges,
3452 AggregationUpdateQueue::new(),
3453 &mut ctx,
3454 );
3455 }
3456 }
3457 }
3458
3459 #[cfg(feature = "verify_aggregation_graph")]
3460 fn verify_aggregation_graph(&self, turbo_tasks: &TurboTasks<TurboTasksBackend>, idle: bool) {
3461 if env::var("TURBO_ENGINE_VERIFY_GRAPH").ok().as_deref() == Some("0") {
3462 return;
3463 }
3464 use std::{collections::VecDeque, env, io::stdout};
3465
3466 use crate::backend::operation::{get_uppers, is_aggregating_node};
3467
3468 let mut ctx = self.execute_context(turbo_tasks);
3469 let root_tasks = self.root_tasks.lock().clone();
3470
3471 for task_id in root_tasks.into_iter() {
3472 let mut queue = VecDeque::new();
3473 let mut visited = FxHashSet::default();
3474 let mut aggregated_nodes = FxHashSet::default();
3475 let mut collectibles = FxHashMap::default();
3476 let root_task_id = task_id;
3477 visited.insert(task_id);
3478 aggregated_nodes.insert(task_id);
3479 queue.push_back(task_id);
3480 let mut counter = 0;
3481 while let Some(task_id) = queue.pop_front() {
3482 counter += 1;
3483 if counter % 100000 == 0 {
3484 println!(
3485 "queue={}, visited={}, aggregated_nodes={}",
3486 queue.len(),
3487 visited.len(),
3488 aggregated_nodes.len()
3489 );
3490 }
3491 let task = ctx.task(task_id, TaskDataCategory::All);
3492 if idle && !self.is_idle.load(Ordering::Relaxed) {
3493 return;
3494 }
3495
3496 let uppers = get_uppers(&task);
3497 if task_id != root_task_id
3498 && !uppers.iter().any(|upper| aggregated_nodes.contains(upper))
3499 {
3500 panic!(
3501 "Task {} {} doesn't report to any root but is reachable from one (uppers: \
3502 {:?})",
3503 task_id,
3504 task.get_task_description(),
3505 uppers
3506 );
3507 }
3508
3509 for (collectible, _) in task.iter_aggregated_collectibles() {
3510 collectibles
3511 .entry(*collectible)
3512 .or_insert_with(|| (false, Vec::new()))
3513 .1
3514 .push(task_id);
3515 }
3516
3517 for (&collectible, &value) in task.iter_collectibles() {
3518 if value > 0 {
3519 if let Some((flag, _)) = collectibles.get_mut(&collectible) {
3520 *flag = true
3521 } else {
3522 panic!(
3523 "Task {} has a collectible {:?} that is not in any upper task",
3524 task_id, collectible
3525 );
3526 }
3527 }
3528 }
3529
3530 let is_dirty = task.has_dirty();
3531 let has_dirty_container = task.has_dirty_containers();
3532 let should_be_in_upper = is_dirty || has_dirty_container;
3533
3534 let aggregation_number = get_aggregation_number(&task);
3535 if is_aggregating_node(aggregation_number) {
3536 aggregated_nodes.insert(task_id);
3537 }
3538 for child_id in task.iter_children() {
3545 if visited.insert(child_id) {
3547 queue.push_back(child_id);
3548 }
3549 }
3550 drop(task);
3551
3552 if should_be_in_upper {
3553 for upper_id in uppers {
3554 let upper = ctx.task(upper_id, TaskDataCategory::All);
3555 let in_upper = upper
3556 .get_aggregated_dirty_containers(&task_id)
3557 .is_some_and(|&dirty| dirty > 0);
3558 if !in_upper {
3559 let containers: Vec<_> = upper
3560 .iter_aggregated_dirty_containers()
3561 .map(|(&k, &v)| (k, v))
3562 .collect();
3563 let upper_task_desc = upper.get_task_description();
3564 drop(upper);
3565 panic!(
3566 "Task {} ({}) is dirty, but is not listed in the upper task {} \
3567 ({})\nThese dirty containers are present:\n{:#?}",
3568 task_id,
3569 ctx.task(task_id, TaskDataCategory::Data)
3570 .get_task_description(),
3571 upper_id,
3572 upper_task_desc,
3573 containers,
3574 );
3575 }
3576 }
3577 }
3578 }
3579
3580 for (collectible, (flag, task_ids)) in collectibles {
3581 if !flag {
3582 use std::io::Write;
3583 let mut stdout = stdout().lock();
3584 writeln!(
3585 stdout,
3586 "{:?} that is not emitted in any child task but in these aggregated \
3587 tasks: {:#?}",
3588 collectible,
3589 task_ids
3590 .iter()
3591 .map(|t| format!(
3592 "{t} {}",
3593 ctx.task(*t, TaskDataCategory::Data).get_task_description()
3594 ))
3595 .collect::<Vec<_>>()
3596 )
3597 .unwrap();
3598
3599 let task_id = collectible.cell.task;
3600 let mut queue = {
3601 let task = ctx.task(task_id, TaskDataCategory::All);
3602 get_uppers(&task)
3603 };
3604 let mut visited = FxHashSet::default();
3605 for &upper_id in queue.iter() {
3606 visited.insert(upper_id);
3607 writeln!(stdout, "{task_id:?} -> {upper_id:?}").unwrap();
3608 }
3609 while let Some(task_id) = queue.pop() {
3610 let task = ctx.task(task_id, TaskDataCategory::All);
3611 let desc = task.get_task_description();
3612 let aggregated_collectible = task
3613 .get_aggregated_collectibles(&collectible)
3614 .copied()
3615 .unwrap_or_default();
3616 let uppers = get_uppers(&task);
3617 drop(task);
3618 writeln!(
3619 stdout,
3620 "upper {task_id} {desc} collectible={aggregated_collectible}"
3621 )
3622 .unwrap();
3623 if task_ids.contains(&task_id) {
3624 writeln!(
3625 stdout,
3626 "Task has an upper connection to an aggregated task that doesn't \
3627 reference it. Upper connection is invalid!"
3628 )
3629 .unwrap();
3630 }
3631 for upper_id in uppers {
3632 writeln!(stdout, "{task_id:?} -> {upper_id:?}").unwrap();
3633 if !visited.contains(&upper_id) {
3634 queue.push(upper_id);
3635 }
3636 }
3637 }
3638 panic!("See stdout for more details");
3639 }
3640 }
3641 }
3642 }
3643
3644 fn assert_not_persistent_calling_transient(&self, parent_id: Option<TaskId>, child_id: TaskId) {
3645 if let Some(parent_id) = parent_id
3646 && !parent_id.is_transient()
3647 && child_id.is_transient()
3648 {
3649 self.panic_persistent_calling_transient(
3650 self.debug_get_task_description(parent_id),
3651 self.debug_get_cached_task_type(child_id).as_deref(),
3652 );
3653 }
3654 }
3655
3656 fn panic_persistent_calling_transient(
3657 &self,
3658 parent: String,
3659 child: Option<&CachedTaskType>,
3660 ) -> ! {
3661 panic!(
3662 "Persistent task {} is not allowed to call, read, or connect to transient task {}.",
3663 parent,
3664 child.map_or("unknown", |t| t.get_name()),
3665 );
3666 }
3667
3668 fn assert_valid_collectible(&self, task_id: TaskId, collectible: RawVc) {
3669 let Some((col_task_id, _)) = collectible.as_task_cell() else {
3671 let task_info = if let Some(col_task_ty) = collectible
3673 .try_get_task_id()
3674 .map(|t| self.debug_get_task_description(t))
3675 {
3676 Cow::Owned(format!(" (return type of {col_task_ty})"))
3677 } else {
3678 Cow::Borrowed("")
3679 };
3680 panic!("Collectible{task_info} must be a ResolvedVc")
3681 };
3682 if col_task_id.is_transient() && !task_id.is_transient() {
3683 panic!(
3685 "Collectible is transient; transient collectibles cannot be emitted from \
3686 persistent tasks"
3687 )
3688 }
3689 }
3690}
3691
3692impl Backend for TurboTasksBackend {
3693 fn startup(&self, turbo_tasks: &TurboTasks<Self>) {
3694 self.startup(turbo_tasks);
3695 }
3696
3697 fn stopping(&self, _turbo_tasks: &TurboTasks<Self>) {
3698 self.stopping();
3699 }
3700
3701 fn stop(&self, turbo_tasks: &TurboTasks<Self>) {
3702 self.stop(turbo_tasks);
3703 }
3704
3705 fn idle_start(&self, turbo_tasks: &TurboTasks<Self>) {
3706 self.idle_start(turbo_tasks);
3707 }
3708
3709 fn idle_end(&self, _turbo_tasks: &TurboTasks<Self>) {
3710 self.idle_end();
3711 }
3712
3713 fn get_or_create_task(
3714 &self,
3715 native_fn: &'static NativeFunction,
3716 this: Option<RawVc>,
3717 arg: &mut dyn DynTaskInputsStorage,
3718 parent_task: Option<TaskId>,
3719 persistence: TaskPersistence,
3720 turbo_tasks: &TurboTasks<Self>,
3721 ) -> TaskId {
3722 self.get_or_create_task(native_fn, this, arg, parent_task, persistence, turbo_tasks)
3723 }
3724
3725 fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3726 self.invalidate_task(task_id, turbo_tasks);
3727 }
3728
3729 fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks<Self>) {
3730 self.invalidate_tasks(tasks, turbo_tasks);
3731 }
3732
3733 fn invalidate_tasks_set(
3734 &self,
3735 tasks: &AutoSet<TaskId, BuildHasherDefault<FxHasher>, 2>,
3736 turbo_tasks: &TurboTasks<Self>,
3737 ) {
3738 self.invalidate_tasks_set(tasks, turbo_tasks);
3739 }
3740
3741 fn invalidate_serialization(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3742 self.invalidate_serialization(task_id, turbo_tasks);
3743 }
3744
3745 fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
3746 self.task_execution_canceled(task, turbo_tasks)
3747 }
3748
3749 fn try_start_task_execution(
3750 &self,
3751 task_id: TaskId,
3752 priority: TaskPriority,
3753 turbo_tasks: &TurboTasks<Self>,
3754 ) -> Option<TaskExecutionSpec<'_>> {
3755 self.try_start_task_execution(task_id, priority, turbo_tasks)
3756 }
3757
3758 fn task_execution_completed(
3759 &self,
3760 task_id: TaskId,
3761 result: Result<RawVc, TurboTasksExecutionError>,
3762 cell_counters: &AutoMap<ValueTypeId, u32, BuildHasherDefault<FxHasher>, 8>,
3763 #[cfg(feature = "verify_determinism")] stateful: bool,
3764 has_invalidator: bool,
3765 turbo_tasks: &TurboTasks<Self>,
3766 ) -> Option<TaskPriority> {
3767 self.task_execution_completed(
3768 task_id,
3769 result,
3770 cell_counters,
3771 #[cfg(feature = "verify_determinism")]
3772 stateful,
3773 has_invalidator,
3774 turbo_tasks,
3775 )
3776 }
3777
3778 type BackendJob = TurboTasksBackendJob;
3779
3780 fn run_backend_job<'a>(
3781 &'a self,
3782 job: Self::BackendJob,
3783 turbo_tasks: &'a TurboTasks<Self>,
3784 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
3785 self.run_backend_job(job, turbo_tasks)
3786 }
3787
3788 fn try_read_task_output(
3789 &self,
3790 task_id: TaskId,
3791 reader: Option<TaskId>,
3792 options: ReadOutputOptions,
3793 turbo_tasks: &TurboTasks<Self>,
3794 ) -> Result<ReadOutcome<RawVc>> {
3795 self.try_read_task_output(task_id, reader, options, turbo_tasks)
3796 }
3797
3798 fn try_read_task_cell(
3799 &self,
3800 task_id: TaskId,
3801 cell: CellId,
3802 reader: Option<TaskId>,
3803 options: ReadCellOptions,
3804 turbo_tasks: &TurboTasks<Self>,
3805 ) -> Result<ReadOutcome<TypedCellContent>> {
3806 self.try_read_task_cell(task_id, reader, cell, options, turbo_tasks)
3807 }
3808
3809 fn try_read_own_task_cell(
3810 &self,
3811 task_id: TaskId,
3812 cell: CellId,
3813 turbo_tasks: &TurboTasks<Self>,
3814 ) -> Result<TypedCellContent> {
3815 self.try_read_own_task_cell(task_id, cell, turbo_tasks)
3816 }
3817
3818 fn read_task_collectibles(
3819 &self,
3820 task_id: TaskId,
3821 collectible_type: TraitTypeId,
3822 reader: Option<TaskId>,
3823 turbo_tasks: &TurboTasks<Self>,
3824 ) -> AutoMap<RawVc, i32, BuildHasherDefault<FxHasher>, 1> {
3825 self.read_task_collectibles(task_id, collectible_type, reader, turbo_tasks)
3826 }
3827
3828 fn emit_collectible(
3829 &self,
3830 collectible_type: TraitTypeId,
3831 collectible: RawVc,
3832 task_id: TaskId,
3833 turbo_tasks: &TurboTasks<Self>,
3834 ) {
3835 self.emit_collectible(collectible_type, collectible, task_id, turbo_tasks)
3836 }
3837
3838 fn unemit_collectible(
3839 &self,
3840 collectible_type: TraitTypeId,
3841 collectible: RawVc,
3842 count: u32,
3843 task_id: TaskId,
3844 turbo_tasks: &TurboTasks<Self>,
3845 ) {
3846 self.unemit_collectible(collectible_type, collectible, count, task_id, turbo_tasks)
3847 }
3848
3849 fn update_task_cell(
3850 &self,
3851 task_id: TaskId,
3852 cell: CellId,
3853 content: CellContent,
3854 updated_key_hashes: Option<SmallVec<[u64; 2]>>,
3855 content_hash: Option<CellHash>,
3856 verification_mode: VerificationMode,
3857 turbo_tasks: &TurboTasks<Self>,
3858 ) {
3859 self.update_task_cell(
3860 task_id,
3861 cell,
3862 content,
3863 updated_key_hashes,
3864 content_hash,
3865 verification_mode,
3866 turbo_tasks,
3867 );
3868 }
3869
3870 fn mark_own_task_as_finished(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3871 self.mark_own_task_as_finished(task_id, turbo_tasks);
3872 }
3873
3874 fn pin_task_for_gc(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
3875 self.pin_task_for_gc(task, turbo_tasks);
3876 }
3877
3878 fn unpin_task_for_gc(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) {
3879 self.unpin_task_for_gc(task, turbo_tasks);
3880 }
3881
3882 fn connect_task(
3883 &self,
3884 task: TaskId,
3885 parent_task: Option<TaskId>,
3886 turbo_tasks: &TurboTasks<Self>,
3887 ) {
3888 self.connect_task(task, parent_task, turbo_tasks);
3889 }
3890
3891 fn create_transient_task(
3892 &self,
3893 task_type: TransientTaskType,
3894 _turbo_tasks: &TurboTasks<Self>,
3895 ) -> TaskId {
3896 self.create_transient_task(task_type)
3897 }
3898
3899 fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks<Self>) {
3900 self.dispose_root_task(task_id, turbo_tasks);
3901 }
3902
3903 fn task_statistics(&self) -> &TaskStatisticsApi {
3904 &self.task_statistics
3905 }
3906
3907 fn is_tracking_dependencies(&self) -> bool {
3908 self.options.dependency_tracking
3909 }
3910
3911 fn get_task_name(&self, task: TaskId, turbo_tasks: &TurboTasks<Self>) -> String {
3912 self.get_task_name(task, turbo_tasks)
3913 }
3914}
3915
3916fn far_future() -> Instant {
3918 Instant::now() + Duration::from_secs(86400 * 365 * 30)
3923}
3924
3925fn encode_task_data(
3937 task: TaskId,
3938 data: &TaskStorage,
3939 category: SpecificTaskDataCategory,
3940 scratch_buffer: &mut TurboBincodeBuffer,
3941) -> Result<TurboBincodeBuffer> {
3942 scratch_buffer.clear();
3943 let mut encoder = new_turbo_bincode_encoder(scratch_buffer);
3944 data.encode(category, &mut encoder)?;
3945
3946 if cfg!(feature = "verify_serialization") {
3947 TaskStorage::new()
3948 .decode(
3949 category,
3950 &mut new_turbo_bincode_decoder(&scratch_buffer[..]),
3951 )
3952 .with_context(|| {
3953 format!(
3954 "expected to be able to decode serialized data for '{category:?}' information \
3955 for {task}"
3956 )
3957 })?;
3958 }
3959 Ok(SmallVec::from_slice(scratch_buffer))
3960}