Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Vcs 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 .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.

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")].
  • Serialization: Values also implement Serialize + Deserialize for persistent caching.
    • Opt-out with #[turbo_tasks::value(serialization = "none")].
  • Tracing: Values implement TraceRawVcs to facilitate the discovery process of nested Vcs.
  • Debug Printing: Values implement ValueDebugFormat for debug printing, resolving nested Vcs to their values.

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 nested Vcs. It must not have nested Vcs to be correct. This is useful for fields containing foreign types that can not implement VcValueType.

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 Vcs, it's important to ensure that both sides are resolved, so that you are comparing equivalent representations of Vcs. All task arguments are automatically resolved before function execution so that task memoization can compare arguments by cell id.

  • Resolved VcValueTypes: In the future, all fields in VcValueTypes will be required to use ResolvedVc instead of Vc. 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

ResolvedVcs 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.

Vcs can be be explicitly converted to ResolvedVcs 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.