turbopack/crates/turbo-tasks/src/vc/README.md
Value cells represent the pending result of a computation, similar to a cell in a spreadsheet. When a Vc's contents change, the change is propagated by invalidating dependent tasks.
In order to get a reference to the pointed value, you need to .await the [Vc<T>] to get a ReadRef<T>:
let some_vc: Vc<T>;
let some_ref: ReadRef<T> = some_vc.await?;
some_ref.some_method_on_t();
The returned ReadRef<T> represents a [reference-counted][triomphe::Arc] snapshot of a cell's value at a given point in time.
A cell is a storage location for data associated with a task. Cells provide:
.await), the reading task is marked as dependent on the cell.bincode] crate.Cells are stored in arrays associated with the task that constructed them. A Vc can either point to a specific cell (this is a "resolved" cell), or the return value of a function (this is an "unresolved" cell).
Most types using the #[turbo_tasks::value] macro are given a .cell() method. This method returns a Vc of the type.
Transparent wrapper types that use #[turbo_tasks::value(transparent)] cannot define methods on their wrapped type, so instead the Vc::cell function can be used to construct these types.
Every time a task runs, its cells are re-constructed.
When .cell() or Vc::cell is called, the cell counter for the ValueTypeId is incremented, and the value is compared to the previous execution's using PartialEq. If the value with that index differs, the cell is updated, and all dependent tasks are invalidated.
The compare-then-update behavior can be overridden to always update and invalidate using the cell = "new" argument.
Because cells are keyed by a combination of their type and construction order, task functions should have a deterministic execution order. A function with inconsistent ordering may result in wasted work by invalidating additional cells, though it will still give correct results:
IndexMap, BTreeMap, or FrozenMap in place of types like HashMap (which gives randomized iteration order).VcsVcs implement IntoFuture and can be awaited, but there are few key differences compared to a normal Future:
The value pointed to by a Vc can be invalidated by changing dependencies or cache evicted, meaning that awaiting a Vc multiple times can give different results. A ReadRef is snapshot of the underlying cell at a point in time.
Reading (awaiting) Vcs causes the current task to be tracked a dependent of the Vc's task or task cell. When the read task or task cell changes, the current task may be re-executed.
Vc types are always [Copy]. Most Futures are not. This works because Vcs are represented as a few ids or indices into data structures managed by the turbo-tasks framework. Vc types are not reference counted, but do support tracing for a hypothetical (unimplemented) garbage collector.
An uncached turbo_tasks::function that returns a Vc begins after being called, even if the Vc is not awaited.
There are a couple of explicit "subtypes" of Vc. These can both be cheaply converted back into a Vc.
ResolvedVc: (aka [RawVc::TaskCell]) A reference to a cell constructed within a task, as part of a Vc::cell or value_type.cell() constructor. As the cell has been constructed at least once, the concrete type of the cell is known (allowing [downcasting][ResolvedVc::try_downcast]). This is stored as a combination of a task id, a type id, and a cell id.
OperationVc: (aka [RawVc::TaskOutput]) The synchronous return value of a turbo_tasks::function. Internally, this is stored using a task id. OperationVcs must first be [connect][crate::OperationVc::connect]ed before being read.
ResolvedVc is almost always preferred over the more awkward OperationVc API, but OperationVc can be useful when dealing with collectibles, when you need to [read the result of a function with strong consistency][crate::OperationVc::read_strongly_consistent], or with State.
These many representations are stored internally using a type-erased [RawVc]. Type erasure reduces the monomorphization (and therefore binary size and compilation time) required to support Vc and its subtypes.
This means that Vc often uses the same in-memory representation as a ResolvedVc or an OperationVc, but it does not expose the same methods (e.g. downcasting) because the exact memory representation is not statically defined.
| Representation | Equality | Downcasting | Strong Consistency | Collectibles | Non-Local | |
|---|---|---|---|---|---|---|
[Vc] | [One of many][RawVc] | ❌ Broken | ⚠️ After resolution | ❌ Eventual | ❌ No | ❌ No |
ResolvedVc | Task Id + Type Id + Cell Id | ✅ Yes* | ✅ Yes, cheaply | ❌ Eventual | ❌ No | ✅ Yes |
OperationVc | Task Id | ✅ Yes* | ⚠️ After resolution | ✅ Supported | ✅ Yes | ✅ Yes |
* see the type's documentation for details
Because Vcs can be equivalent but have different representation, it's not recommended to compare Vcs by equality. Instead, you should convert a Vc to an explicit subtype first (likely ResolvedVc). Future versions of Vc may not implement [Eq], [PartialEq], or [Hash].
While task functions are expected to be side-effect free, their execution behavior is still important for performance reasons, or to code using collectibles to represent issues or side-effects.
Even if not awaited, uncached function calls are guaranteed to execute (potentially emitting collectibles) before the root task finishes or before the completion of any strongly consistent read containing their call. However, the exact point when that execution begins is an implementation detail. Functions may execute more than once if one of their dependencies is invalidated.
Because turbo_tasks is eventually consistent, two adjacent .awaits of the same Vc<T> may return different values. If this happens, the task will eventually be invalidated and re-executed by [a strongly consistent root task][crate::OperationVc::read_strongly_consistent]. Top-level tasks will panic if they attempt to perform an eventually consistent read of a Vc.
Tasks affected by a read inconsistency can return errors. These errors will be discarded by the strongly consistent root task. Tasks should never panic due to a potentially-inconsistent value stored in a Vc.
Currently, all inconsistent tasks are polled to completion. Future versions of the turbo_tasks library may drop tasks that have been identified as inconsistent after some time. As non-root tasks should not perform side-effects, this should be safe, though it may introduce some issues with cross-process resource management.
In addition to the potentially-explicit "resolved" and "operation" representations of a Vc, there's another internal representation of a Vc, known as a "Local Vc", or [RawVc::LocalOutput].
This is a special case of the synchronous return value of a turbo_tasks::function when some of its arguments have not yet been resolved. These are stored in task-local state that is freed after their parent non-local task exits.
We prevent potentially-local Vcs from escaping the lifetime of a function using the [NonLocalValue] marker trait alongside some fallback runtime checks. We do this to avoid some ergonomic challenges that would come from using lifetime annotations with Vc.