Files
next.js/turbopack/crates/turbo-tasks-backend/tests/random_change.rs
f65b10a54d turbo-tasks: replace async resolve fns with custom Future types (ResolveRawVcFuture, ResolveVcFuture, ToResolvedVcFuture) (#91554)
### What?

Replace the `async fn resolve()`, `async fn
resolve_strongly_consistent()`, and `async fn to_resolved()` methods on
`RawVc`, `Vc<T>`, and `OperationVc<T>` with hand-written custom `Future`
implementations, following the existing `ReadRawVcFuture` pattern.

New types:
- **`ResolveRawVcFuture`** (`raw_vc.rs`) — core implementation, replaces
`async fn resolve_inner()`
- **`ResolveVcFuture<T>`** (`vc/mod.rs`) — typed wrapper over
`ResolveRawVcFuture`, returned by `Vc::resolve()`
- **`ResolveOperationVcFuture<T>`** (`vc/operation.rs`) — typed wrapper,
returned by `OperationVc::resolve()`
- **`ToResolvedVcFuture<T>`** (`vc/mod.rs`) — typed wrapper, returned by
`Vc::to_resolved()`

All new future types expose a `.strongly_consistent()` builder method,
enabling `resolve_strongly_consistent()` to be replaced by
`.resolve().strongly_consistent()` at call sites.

`ReadRawVcFuture` is also updated to delegate its phase-1 resolve loop
to `ResolveRawVcFuture` instead of duplicating the logic.
`std::task::ready!` is used throughout to simplify poll implementations.

Also adds `#[inline(never)]` to `ReadRawVcFuture::poll` and
`ResolveRawVcFuture::poll` to avoid inlining large poll implementations
into every await site.

### Why?

Performance, binary size, and improved API ergonomics:

- The hand-written `Future` pattern (already used by `ReadRawVcFuture`)
gives the compiler more predictable, smaller code than the state
machines generated for `async fn`. The `#[inline(never)]` attributes on
`poll` prevent large poll bodies from being duplicated at every await
site, which the async desugaring otherwise allows.
- The new builder API (`.resolve().strongly_consistent()`) is more
composable and removes the need for separate `_strongly_consistent`
method variants, reducing the number of methods on
`RawVc`/`Vc`/`OperationVc`.
- Having `ReadRawVcFuture` delegate to `ResolveRawVcFuture` removes the
duplicated resolve loop and ensures both paths stay in sync.

### How?

- `ResolveRawVcFuture` stores `current: RawVc`, `read_output_options:
ReadOutputOptions`, `strongly_consistent: bool`, and `listener:
Option<EventListener>`. Its `poll` replicates the loop from the old
`resolve_inner` using `try_read_task_output` / `try_read_local_output`.
- On `Err(listener)` from a `try_*` call, the listener is stored in
`self.listener` and `Poll::Pending` is returned. At the top of the loop,
`ready!(poll_listener(...))` re-polls it and short-circuits if still
pending.
- Consistency is downgraded to `Eventual` after the first `TaskOutput`
hop, matching the previous behavior.
- `strongly_consistent: true` keeps the
`SUPPRESS_EVENTUAL_CONSISTENCY_TOP_LEVEL_TASK_CHECK` suppression across
all polls (same logic as `ReadRawVcFuture`).
- `ReadRawVcFuture` now holds a `ResolveRawVcFuture` for phase 1 and
drives it via `Pin::new(&mut self.resolve).poll(cx)` before proceeding
to the cell read in phase 2. This eliminates the duplicated loop that
previously existed in both types.
- Typed wrappers (`ResolveVcFuture<T>`, `ResolveOperationVcFuture<T>`,
`ToResolvedVcFuture<T>`) delegate `poll` to the inner
`ResolveRawVcFuture` and map the output to the appropriate typed result.
- `OperationVc::resolve_strongly_consistent()` is removed; 16 call sites
updated to `.resolve().strongly_consistent()`.
- All new types implement `Unpin` and are exported from `lib.rs`.
- `std::task::ready!` is used in all `poll` implementations to reduce
boilerplate.

No behavioral changes — this is a pure implementation refactor.

### Binary size impact

A release build (`pnpm swc-build-native --release`) was measured before
and after the branch changes on the same merge-base commit (`a41bef94`):

| | Size |
|---|---|
| Base (`a41bef94`, before branch) | 199,690,656 bytes (~190.4 MB) |
| Branch (`6f7846f9`, after changes) | 199,252,384 bytes (~190.0 MB) |
| **Difference** | **−438,272 bytes (−428 KB, −0.22%)** |

The branch produces a slightly smaller binary. The reduction comes
primarily from the `#[inline(never)]` attributes preventing large `poll`
bodies from being duplicated at every await site.

---------

Co-authored-by: Tobias Koppers <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-04-04 08:11:57 +02:00

85 lines
2.3 KiB
Rust

#![feature(arbitrary_self_types)]
#![feature(arbitrary_self_types_pointers)]
#![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this
use anyhow::{Result, bail};
use rand::{RngExt, SeedableRng, rngs::StdRng};
use turbo_tasks::{ResolvedVc, State, Vc};
use turbo_tasks_testing::{Registration, register, run_once};
static REGISTRATION: Registration = register!();
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_random_change() {
run_once(&REGISTRATION, || async {
let state_op = make_state_operation();
let state_vc = state_op.resolve().strongly_consistent().await?;
let state = state_op.read_strongly_consistent().await?;
let mut rng = StdRng::from_seed(Default::default());
let func_op = func_operation(state_vc);
let func2_op = func2_operation(state_vc);
for _i in 0..10 {
let value = rng.random_range(0..100);
state.state.set(value);
let result = func_op.read_strongly_consistent().await?;
assert_eq!(result.value, value);
let result = func2_op.read_strongly_consistent().await?;
assert_eq!(result.value, value);
}
anyhow::Ok(())
})
.await
.unwrap()
}
#[turbo_tasks::value]
#[derive(Clone, Debug)]
struct Value {
value: i32,
}
#[turbo_tasks::value]
#[derive(Debug)]
struct ValueContainer {
state: State<i32>,
}
#[turbo_tasks::function(operation)]
fn make_state_operation() -> Vc<ValueContainer> {
ValueContainer {
state: State::new(0),
}
.cell()
}
#[turbo_tasks::function(operation)]
async fn func2_operation(input: ResolvedVc<ValueContainer>) -> Result<Vc<Value>> {
let state = input.await?;
let value = state.state.get();
println!("func2 {}", *value);
Ok(func(*input, -*value))
}
#[turbo_tasks::function(operation)]
async fn func_operation(input: ResolvedVc<ValueContainer>) -> Vc<Value> {
func(*input, 0)
}
#[turbo_tasks::function]
async fn func(input: Vc<ValueContainer>, nesting: i32) -> Result<Vc<Value>> {
let state = input.await?;
let value = state.state.get();
if nesting < *value {
return Ok(func(input, nesting + 1));
}
if nesting == *value {
println!("func {nesting}");
return Ok(Value { value: *value }.cell());
}
bail!("func no longer valid {}", nesting)
}