Skip to content

Commit

Permalink
[turbopack] Make ReadRef using VcCellMode semantics, add `VcCellMod…
Browse files Browse the repository at this point in the history
…e::raw_cell` API (#68467)

*This is a migrated PR. This was in the turbo repository before the
next.js merge.*
**Migrated From:** vercel/turborepo#8847

## Description

More fixes to `ReadRef` on top of
vercel/turborepo#8845. There's a bit of a cascade of
changes that needed to happen here:

- `ReadRef::cell` should actually use `VcCellMode` so that it supports
new/shared semantics correctly.
- ... however, `VcCellMode` needs to expose an API (`raw_cell`) for
working with `SharedReference` so that we can reuse the
`SharedReference`/`Arc` inside of `ReadRef` instead of cloning the data
inside of it.
- ... however, `CurrentCellRef` needs to expose a `compare_and_update`
method that works for `SharedReference`
(`compare_and_update_with_shared_reference`).
- ... however, the naming conventions for the `CurrentCellRef` methods
are a bit confusing at this point, so rename them.

## Bigger Picture

This my sneaky way of introducing the `raw_cell` API as part of a
smaller PR, since that API will be needed for converting a local `Vc` to
a resolved `Vc`. That's a similar problem of needing to reuse a
`SharedReference` along with `VcCellMode` semantics.

## Testing Instructions

```
cargo nextest r -p turbo-tasks -p turbo-tasks-memory
```
  • Loading branch information
bgw authored and ForsakenHarmony committed Aug 15, 2024
1 parent f78b2c0 commit 39c0f29
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 83 deletions.
15 changes: 7 additions & 8 deletions turbopack/crates/turbo-tasks/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::{
any::Any,
borrow::Cow,
fmt::{self, Debug, Display, Write},
future::Future,
Expand All @@ -22,8 +21,9 @@ use crate::{
raw_vc::CellId,
registry,
trait_helpers::{get_trait_method, has_trait, traits},
triomphe_utils::unchecked_sidecast_triomphe_arc,
FunctionId, RawVc, ReadRef, SharedReference, TaskId, TaskIdProvider, TaskIdSet, TraitRef,
TraitTypeId, ValueTypeId, VcValueTrait, VcValueType,
TraitTypeId, ValueTypeId, VcRead, VcValueTrait, VcValueType,
};

pub enum TaskType {
Expand Down Expand Up @@ -341,11 +341,14 @@ impl Display for CellContent {
}

impl TypedCellContent {
pub fn cast<T: Any + VcValueType>(self) -> Result<ReadRef<T>> {
pub fn cast<T: VcValueType>(self) -> Result<ReadRef<T>> {
let data = self.1 .0.ok_or_else(|| anyhow!("Cell is empty"))?;
let data = data
.downcast()
.downcast::<<T::Read as VcRead<T>>::Repr>()
.map_err(|_err| anyhow!("Unexpected type in cell"))?;
// SAFETY: `T` and `T::Read::Repr` must have equivalent memory representations,
// guaranteed by the unsafe implementation of `VcValueType`.
let data = unsafe { unchecked_sidecast_triomphe_arc(data) };
Ok(ReadRef::new_arc(data))
}

Expand All @@ -368,10 +371,6 @@ impl TypedCellContent {
)
}

pub fn try_cast<T: Any + VcValueType>(self) -> Option<ReadRef<T>> {
Some(ReadRef::new_arc(self.1 .0?.downcast().ok()?))
}

pub fn into_untyped(self) -> CellContent {
self.1
}
Expand Down
2 changes: 1 addition & 1 deletion turbopack/crates/turbo-tasks/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Completion {
// only updates the cell when it is empty (Completion::cell opted-out of
// that via `#[turbo_tasks::value(cell = "new")]`)
let cell = turbo_tasks::macro_helpers::find_cell_by_type(*COMPLETION_VALUE_TYPE_ID);
cell.conditional_update_shared(|old| old.is_none().then_some(Completion));
cell.conditional_update(|old| old.is_none().then_some(Completion));
let raw: RawVc = cell.into();
raw.into()
}
Expand Down
147 changes: 119 additions & 28 deletions turbopack/crates/turbo-tasks/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1522,57 +1522,148 @@ pub struct CurrentCellRef {
index: CellId,
}

type VcReadRepr<T> = <<T as VcValueType>::Read as VcRead<T>>::Repr;

impl CurrentCellRef {
pub fn conditional_update_shared<
T: VcValueType + 'static,
F: FnOnce(Option<&T>) -> Option<T>,
>(
/// Updates the cell if the given `functor` returns a value.
pub fn conditional_update<T>(&self, functor: impl FnOnce(Option<&T>) -> Option<T>)
where
T: VcValueType,
{
self.conditional_update_with_shared_reference(|old_shared_reference| {
let old_ref = old_shared_reference
.and_then(|sr| sr.0.downcast_ref::<VcReadRepr<T>>())
.map(|content| <T::Read as VcRead<T>>::repr_to_value_ref(content));
let new_value = functor(old_ref)?;
Some(SharedReference::new(triomphe::Arc::new(
<T::Read as VcRead<T>>::value_to_repr(new_value),
)))
})
}

/// Updates the cell if the given `functor` returns a `SharedReference`.
pub fn conditional_update_with_shared_reference(
&self,
functor: F,
functor: impl FnOnce(Option<&SharedReference>) -> Option<SharedReference>,
) {
let tt = turbo_tasks();
let content = tt
.read_own_task_cell(self.current_task, self.index)
.ok()
.and_then(|v| v.try_cast::<T>());
let update =
functor(content.as_deref().map(|content| {
<<T as VcValueType>::Read as VcRead<T>>::target_to_value_ref(content)
}));
let cell_content = tt.read_own_task_cell(self.current_task, self.index).ok();
let update = functor(cell_content.as_ref().and_then(|cc| cc.1 .0.as_ref()));
if let Some(update) = update {
tt.update_own_task_cell(
self.current_task,
self.index,
CellContent(Some(SharedReference::new(triomphe::Arc::new(update)))),
)
tt.update_own_task_cell(self.current_task, self.index, CellContent(Some(update)))
}
}

pub fn compare_and_update_shared<T: PartialEq + VcValueType + 'static>(&self, new_content: T) {
self.conditional_update_shared(|old_content| {
if let Some(old_content) = old_content {
if PartialEq::eq(&new_content, old_content) {
/// Replace the current cell's content with `new_value` if the current
/// content is not equal by value with the existing content.
///
/// The comparison happens using the value itself, not the
/// [`VcRead::Target`] of that value.
///
/// Take this example of a custom equality implementation on a transparent
/// wrapper type:
///
/// ```
/// #[turbo_tasks::value(transparent, eq = "manual")]
/// struct Wrapper(Vec<u32>);
///
/// impl PartialEq for Wrapper {
/// fn eq(&self, other: Wrapper) {
/// // Example: order doesn't matter for equality
/// let (mut this, mut other) = (self.clone(), other.clone());
/// this.sort_unstable();
/// other.sort_unstable();
/// this == other
/// }
/// }
///
/// impl Eq for Wrapper {}
/// ```
///
/// Comparisons of `Vc<Wrapper>` used when updating the cell will use
/// `Wrapper`'s custom equality implementation, rather than the one
/// provided by the target (`Vec<u32>`) type.
///
/// However, in most cases, the default derived implementation of
/// `PartialEq` is used which just forwards to the inner value's
/// `PartialEq`.
///
/// If you already have a `SharedReference`, consider calling
/// [`compare_and_update_with_shared_reference`] which can re-use the
/// `SharedReference` object.
pub fn compare_and_update<T>(&self, new_value: T)
where
T: PartialEq + VcValueType,
{
self.conditional_update(|old_value| {
if let Some(old_value) = old_value {
if old_value == &new_value {
return None;
}
}
Some(new_value)
});
}

/// Replace the current cell's content with `new_shared_reference` if the
/// current content is not equal by value with the existing content.
///
/// If you already have a `SharedReference`, this is a faster version of
/// [`CurrentCellRef::compare_and_update`].
///
/// The [`SharedReference`] is expected to use the `<T::Read as
/// VcRead<T>>::Repr` type for its representation of the value.
pub fn compare_and_update_with_shared_reference<T>(&self, new_shared_reference: SharedReference)
where
T: VcValueType + PartialEq,
{
fn extract_sr_value<T: VcValueType>(sr: &SharedReference) -> &T {
<T::Read as VcRead<T>>::repr_to_value_ref(
sr.0.downcast_ref::<VcReadRepr<T>>()
.expect("cannot update SharedReference of different type"),
)
}
self.conditional_update_with_shared_reference(|old_sr| {
if let Some(old_sr) = old_sr {
let old_value: &T = extract_sr_value(old_sr);
let new_value = extract_sr_value(&new_shared_reference);
if old_value == new_value {
return None;
}
}
Some(new_content)
Some(new_shared_reference)
});
}

pub fn update_shared<T: VcValueType + 'static>(&self, new_content: T) {
/// Unconditionally updates the content of the cell.
pub fn update<T>(&self, new_value: T)
where
T: VcValueType,
{
let tt = turbo_tasks();
tt.update_own_task_cell(
self.current_task,
self.index,
CellContent(Some(SharedReference::new(triomphe::Arc::new(new_content)))),
CellContent(Some(SharedReference::new(triomphe::Arc::new(
<T::Read as VcRead<T>>::value_to_repr(new_value),
)))),
)
}

pub fn update_shared_reference(&self, shared_ref: SharedReference) {
/// A faster version of [`Self::update`] if you already have a
/// [`SharedReference`].
///
/// If the passed-in [`SharedReference`] is the same as the existing cell's
/// by identity, no update is performed.
///
/// The [`SharedReference`] is expected to use the `<T::Read as
/// VcRead<T>>::Repr` type for its representation of the value.
pub fn update_with_shared_reference(&self, shared_ref: SharedReference) {
let tt = turbo_tasks();
let content = tt.read_own_task_cell(self.current_task, self.index).ok();
let update = if let Some(TypedCellContent(_, CellContent(shared_ref_exp))) = content {
shared_ref_exp.as_ref().ne(&Some(&shared_ref))
let update = if let Some(TypedCellContent(_, CellContent(Some(shared_ref_exp)))) = content {
// pointer equality (not value equality)
shared_ref_exp != shared_ref
} else {
true
};
Expand Down
11 changes: 6 additions & 5 deletions turbopack/crates/turbo-tasks/src/read_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use turbo_tasks_hash::DeterministicHash;

use crate::{
debug::{ValueDebugFormat, ValueDebugFormatString},
macro_helpers::find_cell_by_type,
trace::{TraceRawVcs, TraceRawVcsContext},
triomphe_utils::unchecked_sidecast_triomphe_arc,
vc::VcCellMode,
SharedReference, Vc, VcRead, VcValueType,
};

Expand Down Expand Up @@ -244,14 +244,15 @@ where
/// reference.
pub fn cell(read_ref: ReadRef<T>) -> Vc<T> {
let type_id = T::get_value_type_id();
let local_cell = find_cell_by_type(type_id);
// SAFETY: `T` and `T::Read::Repr` must have equivalent memory representations,
// guaranteed by the unsafe implementation of `VcValueType`.
local_cell.update_shared_reference(SharedReference::new(unsafe {
let value = unsafe {
unchecked_sidecast_triomphe_arc::<T, <T::Read as VcRead<T>>::Repr>(read_ref.0)
}));
};
Vc {
node: local_cell.into(),
node: <T::CellMode as VcCellMode<T>>::raw_cell(
SharedReference::new(value).typed(type_id),
),
_t: PhantomData,
}
}
Expand Down
2 changes: 1 addition & 1 deletion turbopack/crates/turbo-tasks/src/trait_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ where
// See Safety clause above.
let TypedSharedReference(ty, shared_ref) = trait_ref.shared_reference;
let local_cell = find_cell_by_type(ty);
local_cell.update_shared_reference(shared_ref);
local_cell.update_with_shared_reference(shared_ref);
let raw_vc: RawVc = local_cell.into();
raw_vc.into()
}
Expand Down
18 changes: 3 additions & 15 deletions turbopack/crates/turbo-tasks/src/vc/cast.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::{marker::PhantomData, mem::ManuallyDrop};
use std::marker::PhantomData;

use anyhow::Result;

use crate::{backend::TypedCellContent, ReadRef, TraitRef, VcRead, VcValueTrait, VcValueType};
use crate::{backend::TypedCellContent, ReadRef, TraitRef, VcValueTrait, VcValueType};

/// Trait defined to share behavior between values and traits within
/// [`ReadRawVcFuture`][crate::ReadRawVcFuture]. See [`VcValueTypeCast`] and
Expand All @@ -27,19 +27,7 @@ where
type Output = ReadRef<T>;

fn cast(content: TypedCellContent) -> Result<Self::Output> {
Ok(
// Safety: the `VcValueType` implementor must guarantee that both `T` and
// `Repr` are #[repr(transparent)].
unsafe {
// Downcast the cell content to the expected representation type, then
// transmute it to the expected type.
// See https://users.rust-lang.org/t/transmute-doesnt-work-on-generic-types/87272/9
std::mem::transmute_copy::<
ManuallyDrop<ReadRef<<T::Read as VcRead<T>>::Repr>>,
Self::Output,
>(&ManuallyDrop::new(content.cast()?))
},
)
content.cast()
}
}

Expand Down
55 changes: 45 additions & 10 deletions turbopack/crates/turbo-tasks/src/vc/cell_mode.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
use std::marker::PhantomData;
use std::{any::type_name, marker::PhantomData};

use super::{read::VcRead, traits::VcValueType};
use crate::{manager::find_cell_by_type, Vc};
use crate::{manager::find_cell_by_type, task::shared_reference::TypedSharedReference, RawVc, Vc};

/// Trait that controls the behavior of `Vc::cell` on a value type basis.
type VcReadTarget<T> = <<T as VcValueType>::Read as VcRead<T>>::Target;
type VcReadRepr<T> = <<T as VcValueType>::Read as VcRead<T>>::Repr;

/// Trait that controls the behavior of [`Vc::cell`] based on the value type's
/// [`VcValueType::CellMode`].
///
/// This trait must remain sealed within this crate.
pub trait VcCellMode<T>
where
T: VcValueType,
{
fn cell(value: <T::Read as VcRead<T>>::Target) -> Vc<T>;
/// Create a new cell.
fn cell(value: VcReadTarget<T>) -> Vc<T>;

/// Create a type-erased [`RawVc`] cell given a pre-existing type-erased
/// [`SharedReference`][crate::task::SharedReference].
///
/// This is used in APIs that already have a `SharedReference`, such as in
/// [`ReadRef::cell`][crate::ReadRef::cell] or in [`Vc::resolve`] when
/// resolving a local [`Vc`]. This avoids unnecessary cloning.
fn raw_cell(value: TypedSharedReference) -> RawVc;
}

/// Mode that always updates the cell's content.
Expand All @@ -22,14 +35,21 @@ impl<T> VcCellMode<T> for VcCellNewMode<T>
where
T: VcValueType,
{
fn cell(inner: <T::Read as VcRead<T>>::Target) -> Vc<T> {
fn cell(inner: VcReadTarget<T>) -> Vc<T> {
let cell = find_cell_by_type(T::get_value_type_id());
cell.update_shared(<T::Read as VcRead<T>>::target_to_repr(inner));
cell.update(<T::Read as VcRead<T>>::target_to_value(inner));
Vc {
node: cell.into(),
_t: PhantomData,
}
}

fn raw_cell(content: TypedSharedReference) -> RawVc {
debug_assert_repr::<T>(&content);
let cell = find_cell_by_type(content.0);
cell.update_with_shared_reference(content.1);
cell.into()
}
}

/// Mode that compares the cell's content with the new value and only updates
Expand All @@ -40,15 +60,30 @@ pub struct VcCellSharedMode<T> {

impl<T> VcCellMode<T> for VcCellSharedMode<T>
where
T: VcValueType,
<T::Read as VcRead<T>>::Repr: PartialEq,
T: VcValueType + PartialEq,
{
fn cell(inner: <T::Read as VcRead<T>>::Target) -> Vc<T> {
fn cell(inner: VcReadTarget<T>) -> Vc<T> {
let cell = find_cell_by_type(T::get_value_type_id());
cell.compare_and_update_shared(<T::Read as VcRead<T>>::target_to_repr(inner));
cell.compare_and_update(<T::Read as VcRead<T>>::target_to_value(inner));
Vc {
node: cell.into(),
_t: PhantomData,
}
}

fn raw_cell(content: TypedSharedReference) -> RawVc {
debug_assert_repr::<T>(&content);
let cell = find_cell_by_type(content.0);
cell.compare_and_update_with_shared_reference::<T>(content.1);
cell.into()
}
}

fn debug_assert_repr<T: VcValueType>(content: &TypedSharedReference) {
debug_assert!(
(*content.1 .0).is::<VcReadRepr<T>>(),
"SharedReference for type {} must use representation type {}",
type_name::<T>(),
type_name::<VcReadRepr<T>>(),
);
}
Loading

0 comments on commit 39c0f29

Please sign in to comment.