Turbo Tasks: Value Cells
Value Cells (the Vc<T>
type) 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.
The contents (T
) of a Vc<T>
are always a VcValueType
. These types are created using the #[turbo_tasks::value]
macro.
Understanding Cells
A Cell is a storage location for data associated with a task. Cells provide:
- Immutability: Once a value is stored in a cell, it becomes immutable until that task is re-executed.
- Recomputability: If invalidated or cache evicted, a cell's contents can be re-computed by re-executing its associated task.
- Dependency Tracking: When a cell's contents are read (with
.await
), the reading task is marked as dependent on the cell.
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).
Constructing a 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.
Updating a Cell
Every time a task runs, its cells are re-constructed.
When .cell()
or Vc::cell
is called, the value is compared to the previous execution's using PartialEq
. If it differs, the cell is updated, and all dependent tasks are invalidated. This behavior can be overridden to always 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.
You should use types with deterministic behavior such as IndexMap
or BTreeMap
in place of types like HashMap
(which gives randomized iteration order).
Reading from Cells
To read the value from a cell, Vc<T>
implements IntoFuture
:
#![allow(unused)] fn main() { let vc: Vc<T> = ...; let value: ReadRef<T> = vc.await?; }
A ReadRef<T>
represents a reference-counted snapshot of a cell's value at a given point in time.
Vc
s must be awaited because we may need to wait for the associated task to finish executing. This operation may fail.
Reading from a Vc
registers the current task as a dependent of the cell, meaning the task will be re-computed if the cell's value changes.
Eventual Consistency
Because turbo_tasks
is [eventually consistent], two adjacent .await
s 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.
Tasks affected by a read inconsistency can return errors. These errors will be discarded by the strongly consistent root task. Tasks should never panic.
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.
Defining Values
Values intended for storage in cells must be defined with the #[turbo_tasks::value]
attribute:
- Comparison: By default, values implement
PartialEq + Eq
for comparison.- Opt-out with
#[turbo_tasks::value(eq = "none")]
. - For custom comparison logic, use
#[turbo_tasks::value(eq = "manual")]
.
- Opt-out with
- Serialization: Values also implement
Serialize + Deserialize
for persistent caching.- Opt-out with
#[turbo_tasks::value(serialization = "none")]
.
- Opt-out with
- Tracing: Values implement
TraceRawVcs
to facilitate the discovery process of nestedVc
s.- This may be used for tracing garbage collection in the future.
- Debug Printing: Values implement
ValueDebugFormat
for debug printing, resolving nestedVc
s to their values.- Debug-print
Vc
types with thevdbg!(...)
macro.
- Debug-print
To opt-out of certain requirements for fields within a struct, use the following attributes:
#[turbo_tasks(debug_ignore)]
: Skips the field for debug printing.#[turbo_tasks(trace_ignore)]
: Skips the field for tracing nestedVc
s. It must not have nestedVc
s to be correct. This is useful for fields containing foreign types that can not implementVcValueType
.
Code Example
Here's an example of defining a value for use in a cell:
#![allow(unused)] fn main() { #[turbo_tasks::value] struct MyValue { // Fields go here... } }
In this example, MyValue
is a struct that can be stored in a cell.
Enforcing Cell Resolution
As mentioned earlier, a Vc
may either be "resolved" or "unresolved":
- Resolved: Points to a specific cell constructed within a task.
- Unresolved: Points to the return value of a task (a function with a specific set of arguments). This is a lazily evaluated pointer to a resolved cell within the task.
Internally, a Vc
may have either representation, but sometimes you want to enforce that a Vc
is either resolved:
-
Equality: When performing equality comparisons between
Vc
s, it's important to ensure that both sides are resolved, so that you are comparing equivalent representations ofVc
s. All task arguments are automatically resolved before function execution so that task memoization can compare arguments by cell id. -
Resolved
VcValueType
s: In the future, all fields inVcValueType
s will be required to useResolvedVc
instead ofVc
. This will facilitate more accurate equality comparisons.
The ResolvedVc
Type
ResolvedVc<T>
is a subtype of Vc<T>
that enforces resolution statically with types. It implements Deref<Target = Vc<T>>
and behaves similarly to Vc
types.
Constructing a ResolvedVc
ResolvedVc
s can be constructed using generated .resolved_cell()
methods or with the ResolvedVc::cell()
function (for transparent wrapper types).
A Vc
can be implicitly converted to a ResolvedVc
by using a ResolvedVc
type in a #[turbo_tasks::function]
argument via the external signature rewriting rules.
Vc
s can be be explicitly converted to ResolvedVc
s using .to_resolved().await?
. This may require waiting on the task to finish executing.
Reading a ResolvedVc
Even though a Vc
may be resolved as a ResolvedVc
, we must still use .await?
to read it's value, as the value could be invalidated or cache-evicted.