From 160606d4d5e027a3414260904b320d21ed8ed6a8 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Sun, 17 May 2026 15:39:34 -0700 Subject: [PATCH 01/19] Phase 1a: create empty turbo-tasks-handle crate This crate is the future home of the #[no_mangle] pub extern "Rust" fn providers that implement the dispatch surface declared (later in this phase) in turbo-tasks/src/handle.rs. For now it is a skeleton: lib.rs is empty (just a doc comment) and the crate exposes two optional features: - prod : pulls in turbo-tasks-backend (will emit __tt_prod_* providers). - test : pulls in turbo-tasks-testing (will emit __tt_test_* providers). Building with neither feature compiles to an empty lib, so 'cargo check --workspace' works without flags. Binaries and tests that actually consume the dispatch must enable the appropriate feature(s). --- Cargo.lock | 9 +++++++ Cargo.toml | 1 + .../crates/turbo-tasks-handle/Cargo.toml | 25 +++++++++++++++++++ .../crates/turbo-tasks-handle/src/lib.rs | 11 ++++++++ 4 files changed, 46 insertions(+) create mode 100644 turbopack/crates/turbo-tasks-handle/Cargo.toml create mode 100644 turbopack/crates/turbo-tasks-handle/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 243dabd82acb..6d99ff88ab8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10016,6 +10016,15 @@ dependencies = [ "turbo-tasks-fs", ] +[[package]] +name = "turbo-tasks-handle" +version = "0.1.0" +dependencies = [ + "turbo-tasks", + "turbo-tasks-backend", + "turbo-tasks-testing", +] + [[package]] name = "turbo-tasks-hash" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a1d6ec2fb973..90cd23cbe63d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,7 @@ turbo-tasks-bytes = { path = "turbopack/crates/turbo-tasks-bytes" } turbo-tasks-env = { path = "turbopack/crates/turbo-tasks-env" } turbo-tasks-fetch = { path = "turbopack/crates/turbo-tasks-fetch" } turbo-tasks-fs = { path = "turbopack/crates/turbo-tasks-fs" } +turbo-tasks-handle = { path = "turbopack/crates/turbo-tasks-handle", default-features = false } turbo-tasks-hash = { path = "turbopack/crates/turbo-tasks-hash" } turbo-tasks-macros = { path = "turbopack/crates/turbo-tasks-macros" } turbo-tasks-malloc = { path = "turbopack/crates/turbo-tasks-malloc", default-features = false } diff --git a/turbopack/crates/turbo-tasks-handle/Cargo.toml b/turbopack/crates/turbo-tasks-handle/Cargo.toml new file mode 100644 index 000000000000..9c804aca8e67 --- /dev/null +++ b/turbopack/crates/turbo-tasks-handle/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "turbo-tasks-handle" +version = "0.1.0" +description = "Concrete dispatch providers for the turbo-tasks task-local. Implements `extern \"Rust\"` symbols whose forward declarations live in `turbo-tasks`, allowing thin-LTO devirtualization of the prod and test handles." +license = "MIT" +edition = "2024" + +[lib] +bench = false + +[lints] +workspace = true + +[features] +# Auto-activated by binaries / test crates that need the corresponding arm. +# - `prod`: links `turbo-tasks-backend` and emits `__tt_prod_*` providers. +# - `test`: links `turbo-tasks-testing` and emits `__tt_test_*` providers. +prod = ["dep:turbo-tasks-backend"] +test = ["dep:turbo-tasks-testing"] + +[dependencies] +turbo-tasks = { workspace = true } + +turbo-tasks-backend = { workspace = true, optional = true } +turbo-tasks-testing = { workspace = true, optional = true } diff --git a/turbopack/crates/turbo-tasks-handle/src/lib.rs b/turbopack/crates/turbo-tasks-handle/src/lib.rs new file mode 100644 index 000000000000..e81e9f5de4d5 --- /dev/null +++ b/turbopack/crates/turbo-tasks-handle/src/lib.rs @@ -0,0 +1,11 @@ +//! See the crate-level docs in `Cargo.toml`. +//! +//! This crate is intentionally tiny: it only emits `#[no_mangle] pub extern +//! "Rust" fn` provider symbols that match the forward declarations in +//! [`turbo_tasks::handle`]. Consumers should depend on this crate with the +//! `prod` and/or `test` features enabled to pull in the corresponding arm. +//! +//! Building this crate with neither feature compiles to an empty lib that +//! exports no symbols. This is intentional so that `cargo check --workspace` +//! works without explicit feature flags; binaries and test harnesses that +//! actually consume the handle dispatch must enable the appropriate feature(s). From f65c9adc912cc9fb8fd0a9913971958568df0d8d Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Sun, 17 May 2026 19:27:26 -0700 Subject: [PATCH 02/19] Phase 1b: define TurboTasksHandle dispatch + first method (invalidate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the tagged-pointer dispatch infrastructure that replaces the Arc task-local. Only one method (`invalidate`) is on the dispatch surface so far — subsequent commits will add the rest. In turbo-tasks/src/handle.rs: * TurboTasksHandle { tag: HandleTag, ptr: NonNull<()> } — the tagged pointer that will replace Arc in TURBO_TASKS. * unsafe extern "Rust" forward declarations for __tt_prod_invalidate / __tt_test_invalidate. * impl TurboTasksHandle { fn invalidate(...) { match self.tag { ... } } } — direct dispatch over the tag; both arms are extern "Rust" calls that thin LTO will inline cross-crate. * Clone / Drop dispatch through __tt__clone_arc / drop_arc so the underlying Arc refcount stays correct. * Uses macro_metavar_expr_concat (nightly, stable on our toolchain) to auto-generate the per-arm symbol names from a single method list. In turbo-tasks-handle/src/lib.rs: * ProdHandleConcrete type alias matches what next-napi-bindings uses. * #[no_mangle] pub extern "Rust" fn __tt_prod_invalidate + clone/drop providers for the prod arm; mirror for the test arm (VcStorage). * from_prod / from_test constructors that convert an Arc<...> into a TurboTasksHandle by Arc::into_raw. No call sites are switched yet; TURBO_TASKS still holds Arc. CI green for all feature combinations of turbo-tasks-handle (none, prod, test, prod+test). --- Cargo.lock | 1 + .../crates/turbo-tasks-handle/Cargo.toml | 3 +- .../crates/turbo-tasks-handle/src/lib.rs | 157 +++++++++++++ turbopack/crates/turbo-tasks/src/handle.rs | 209 ++++++++++++++++++ turbopack/crates/turbo-tasks/src/lib.rs | 3 + 5 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 turbopack/crates/turbo-tasks/src/handle.rs diff --git a/Cargo.lock b/Cargo.lock index 6d99ff88ab8c..2e41bb7c8159 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10020,6 +10020,7 @@ dependencies = [ name = "turbo-tasks-handle" version = "0.1.0" dependencies = [ + "either", "turbo-tasks", "turbo-tasks-backend", "turbo-tasks-testing", diff --git a/turbopack/crates/turbo-tasks-handle/Cargo.toml b/turbopack/crates/turbo-tasks-handle/Cargo.toml index 9c804aca8e67..6b4edd5e4701 100644 --- a/turbopack/crates/turbo-tasks-handle/Cargo.toml +++ b/turbopack/crates/turbo-tasks-handle/Cargo.toml @@ -15,11 +15,12 @@ workspace = true # Auto-activated by binaries / test crates that need the corresponding arm. # - `prod`: links `turbo-tasks-backend` and emits `__tt_prod_*` providers. # - `test`: links `turbo-tasks-testing` and emits `__tt_test_*` providers. -prod = ["dep:turbo-tasks-backend"] +prod = ["dep:turbo-tasks-backend", "dep:either"] test = ["dep:turbo-tasks-testing"] [dependencies] turbo-tasks = { workspace = true } +either = { workspace = true, optional = true } turbo-tasks-backend = { workspace = true, optional = true } turbo-tasks-testing = { workspace = true, optional = true } diff --git a/turbopack/crates/turbo-tasks-handle/src/lib.rs b/turbopack/crates/turbo-tasks-handle/src/lib.rs index e81e9f5de4d5..95f9ba9315a3 100644 --- a/turbopack/crates/turbo-tasks-handle/src/lib.rs +++ b/turbopack/crates/turbo-tasks-handle/src/lib.rs @@ -9,3 +9,160 @@ //! exports no symbols. This is intentional so that `cargo check --workspace` //! works without explicit feature flags; binaries and test harnesses that //! actually consume the handle dispatch must enable the appropriate feature(s). + +#![feature(macro_metavar_expr_concat)] + +use std::sync::Arc; + +use turbo_tasks::{HandleTag, TurboTasksHandle}; + +// ===================================================================== +// Prod arm +// ===================================================================== + +/// The concrete prod handle type — matches what `next-napi-bindings` uses. +/// +/// If a future binary needs a different `Backend`/storage combination, +/// this crate's prod arm would need to be re-parameterized (or a parallel +/// crate added). Today there is exactly one prod handle type so we +/// hardcode it. +#[cfg(feature = "prod")] +pub type ProdHandleConcrete = turbo_tasks::TurboTasks< + turbo_tasks_backend::TurboTasksBackend< + either::Either< + turbo_tasks_backend::TurboBackingStorage, + turbo_tasks_backend::NoopBackingStorage, + >, + >, +>; + +#[cfg(feature = "prod")] +mod prod { + use super::*; + + /// Generates `#[no_mangle] pub extern "Rust" fn __tt_prod_(...)` + /// for a single dispatched method. The body casts `ptr` back to + /// `&ProdHandleConcrete` and forwards to the trait method. + macro_rules! provide_prod { + ( + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + #[unsafe(no_mangle)] + pub extern "Rust" fn ${concat(__tt_prod_, $name)}( + ptr: *const () + $(, $arg : $ty)* + ) $(-> $ret)? { + let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; + ::$name(tt $(, $arg)*) + } + }; + } + + // ---- dispatched methods ---------------------------------------------- + // + // Keep this list in sync with the matching `tt_decl_extern!` / + // `tt_decl_handle_method!` invocations in + // `turbopack/crates/turbo-tasks/src/handle.rs`. A future cleanup could + // generate both from a shared callback macro, but for now duplicating + // the list is the simplest source-of-truth. + + provide_prod!(fn invalidate(task: turbo_tasks::TaskId)); + + // ---- Arc clone / drop ------------------------------------------------- + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_clone_arc(ptr: *const ()) { + // Reconstitute the Arc transiently to bump the refcount, then leak + // it again so the next drop sees the same pointer. + let arc = unsafe { Arc::from_raw(ptr as *const ProdHandleConcrete) }; + let cloned = arc.clone(); + // Re-leak the original to keep the original handle alive. + std::mem::forget(arc); + // The clone is owned by the new handle that triggered this call; + // leak its pointer so the new handle owns it. + let _new_ptr = Arc::into_raw(cloned); + // `_new_ptr` must equal `ptr` because Arc::into_raw is reproducible + // for the same Arc — the new handle keeps `ptr` and we discard + // `_new_ptr`. (See the Drop impl in turbo-tasks for the matching + // decrement; both ends use the same pointer value.) + debug_assert_eq!(_new_ptr as *const (), ptr); + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_drop_arc(ptr: *const ()) { + // Reconstitute the Arc to decrement and drop. + drop(unsafe { Arc::from_raw(ptr as *const ProdHandleConcrete) }); + } + + /// Constructs a `TurboTasksHandle` pointing at the given prod Arc. + /// + /// The caller transfers ownership of one refcount into the handle. + pub fn from_prod(arc: Arc) -> TurboTasksHandle { + let ptr = Arc::into_raw(arc) as *mut (); + // Safety: ptr came from Arc::into_raw on a ProdHandleConcrete and + // the tag is consistent with that type. + unsafe { + TurboTasksHandle::from_raw_parts(HandleTag::Prod, std::ptr::NonNull::new_unchecked(ptr)) + } + } +} + +#[cfg(feature = "prod")] +pub use prod::from_prod; + +// ===================================================================== +// Test arm +// ===================================================================== + +#[cfg(feature = "test")] +mod test_arm { + use super::*; + + pub type TestHandleConcrete = turbo_tasks_testing::VcStorage; + + macro_rules! provide_test { + ( + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + #[unsafe(no_mangle)] + pub extern "Rust" fn ${concat(__tt_test_, $name)}( + ptr: *const () + $(, $arg : $ty)* + ) $(-> $ret)? { + let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; + ::$name(tt $(, $arg)*) + } + }; + } + + // ---- dispatched methods ---------------------------------------------- + // (Mirror of the `prod` block — keep in sync.) + + provide_test!(fn invalidate(task: turbo_tasks::TaskId)); + + // ---- Arc clone / drop ------------------------------------------------- + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_clone_arc(ptr: *const ()) { + let arc = unsafe { Arc::from_raw(ptr as *const TestHandleConcrete) }; + let cloned = arc.clone(); + std::mem::forget(arc); + let _new_ptr = Arc::into_raw(cloned); + debug_assert_eq!(_new_ptr as *const (), ptr); + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_drop_arc(ptr: *const ()) { + drop(unsafe { Arc::from_raw(ptr as *const TestHandleConcrete) }); + } + + pub fn from_test(arc: Arc) -> TurboTasksHandle { + let ptr = Arc::into_raw(arc) as *mut (); + unsafe { + TurboTasksHandle::from_raw_parts(HandleTag::Test, std::ptr::NonNull::new_unchecked(ptr)) + } + } +} + +#[cfg(feature = "test")] +pub use test_arm::from_test; diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs new file mode 100644 index 000000000000..9632939118d2 --- /dev/null +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -0,0 +1,209 @@ +//! Tagged-pointer dispatch for the turbo-tasks task-local. +//! +//! The task-local that holds the current `TurboTasksApi` implementation has +//! historically been an `Arc`. The `dyn` is necessary +//! because the prod handle (`TurboTasks`) is generic over a backend +//! type that the `task_local!` macro cannot name — but the cost is an +//! indirect vtable call on every dispatched method, and rustc currently does +//! not emit the LLVM metadata that `WholeProgramDevirt` needs to inline +//! through trait objects ([rust#68262], [rust#45774]). +//! +//! [rust#68262]: https://github.com/rust-lang/rust/issues/68262 +//! [rust#45774]: https://github.com/rust-lang/rust/issues/45774 +//! +//! This module replaces the `dyn` indirection with a tagged pointer that +//! dispatches through `extern "Rust"` forward declarations: +//! +//! ```text +//! call site provider crate +//! ┌──────────────────────────┐ ┌─────────────────────────────────────┐ +//! │ tt.invalidate(task) ─────┼──────────────► #[no_mangle] pub extern "Rust" fn │ +//! │ match self.tag { … } │ │ __tt_prod_invalidate(ptr, task) { │ +//! │ │ │ let tt: &TurboTasks<…> = …; │ +//! │ │ │ tt.invalidate(task) │ +//! │ │ │ } │ +//! └──────────────────────────┘ └─────────────────────────────────────┘ +//! ``` +//! +//! Under `lto = "thin"` + `codegen-units = 1` (this workspace's release +//! profile), the linker fully inlines the `extern "Rust"` call into the +//! caller. The `match` over a one-arm enum collapses entirely; over a +//! two-arm enum it compiles to `cmp + b.ne + direct call` and LLVM can +//! sometimes fuse the arms further. +//! +//! Providers (the `__tt__` symbols) live in the +//! `turbo-tasks-handle` crate. That crate depends on both +//! `turbo-tasks-backend` (for the prod arm) and `turbo-tasks-testing` +//! (for the test arm); each arm is gated by a Cargo feature. By centralising +//! the providers in one crate, we have exactly one place that knows about +//! the dispatch contract. + +use std::ptr::NonNull; + +// Re-export the macro for use by `turbo-tasks-handle`'s provider macro, +// which needs to iterate over the same method list. +// +// TODO: when we add `turbo_tasks_weak()` to the dispatch surface, also +// generate `__tt__downgrade` / `upgrade` and a `TurboTasksWeakHandle` +// type. For now `turbo_tasks_weak` keeps returning `Weak` +// because its only consumer (`turbo_tasks_future_scope`) is not on a hot path. + +/// Identifier for which concrete implementation a [`TurboTasksHandle`] points +/// at. Used as the tag in the tagged-pointer dispatch. +/// +/// New variants must coordinate with `turbo-tasks-handle`'s provider +/// emission and with every caller's `match` against this tag. +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum HandleTag { + /// `TurboTasks>` — the production handle used by + /// `next-napi-bindings`, benches, etc. + Prod = 0, + /// `VcStorage` — the test-only handle used by `turbo-tasks-testing`. + Test = 1, +} + +/// A type-erased reference to a concrete `TurboTasksApi` implementation. +/// +/// Logically equivalent to an `Arc`: the pointer is the +/// raw `Arc::into_raw(...)` of the concrete handle, the tag tells the +/// dispatch which provider to call. +/// +/// `Clone` and `Drop` route through `__tt__clone_arc` / +/// `__tt__drop_arc` so refcounting stays correct. +#[derive(Debug)] +pub struct TurboTasksHandle { + tag: HandleTag, + /// Points at the inner of an `Arc` owned via + /// `Arc::into_raw`. The lifetime is managed by `Clone` / `Drop` + /// dispatching through the provider crate. + ptr: NonNull<()>, +} + +// Safety: the underlying concrete handle types (`TurboTasks<…>` and +// `VcStorage`) are themselves `Send + Sync` and are reference-counted via +// `Arc`. The raw pointer is just an erased `Arc::into_raw` result; it does +// not introduce additional aliasing beyond what the Arc allowed. +unsafe impl Send for TurboTasksHandle {} +unsafe impl Sync for TurboTasksHandle {} + +impl TurboTasksHandle { + /// Constructs a handle from raw parts. Intended to be called only by + /// `turbo-tasks-handle`'s `from_prod` / `from_test` constructors, which + /// own the safety contract that `ptr` is a valid `Arc::into_raw` pointer + /// for the concrete type associated with `tag`. + /// + /// # Safety + /// + /// `ptr` must be a pointer obtained from `Arc::into_raw` on the concrete + /// handle type whose tag matches `tag`. Ownership of the refcount + /// transfers into the new `TurboTasksHandle`. + #[inline] + pub unsafe fn from_raw_parts(tag: HandleTag, ptr: NonNull<()>) -> Self { + Self { tag, ptr } + } + + /// The tag indicating which concrete implementation this handle points + /// at. Exposed for tests and diagnostics; the dispatch macro handles + /// the normal case. + #[inline] + pub fn tag(&self) -> HandleTag { + self.tag + } + + /// Raw pointer access, intended only for the dispatch macro and for + /// `turbo-tasks-handle`. Callers must respect the tag to interpret it. + #[inline] + pub fn raw_ptr(&self) -> *const () { + self.ptr.as_ptr() + } +} + +// ===================================================================== +// `extern "Rust"` forward declarations. +// +// Each dispatched `TurboTasksApi` method has two extern symbols — one per +// arm. The bodies are defined in `turbo-tasks-handle` and resolved at link +// time. Thin LTO inlines them. +// ===================================================================== + +/// Generates an `unsafe extern "Rust" { fn __tt_prod_(...); fn +/// __tt_test_(...); }` declaration pair for one dispatched method. +macro_rules! tt_decl_extern { + ( + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + unsafe extern "Rust" { + fn ${concat(__tt_prod_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; + fn ${concat(__tt_test_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; + } + }; +} + +/// Generates an inherent method on `TurboTasksHandle` that dispatches over +/// `match self.tag` to the corresponding `extern "Rust"` symbol. +macro_rules! tt_decl_handle_method { + ( + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + impl TurboTasksHandle { + #[inline] + pub fn $name(&self $(, $arg : $ty)*) $(-> $ret)? { + match self.tag { + HandleTag::Prod => unsafe { + ${concat(__tt_prod_, $name)}(self.ptr.as_ptr() $(, $arg)*) + }, + HandleTag::Test => unsafe { + ${concat(__tt_test_, $name)}(self.ptr.as_ptr() $(, $arg)*) + }, + } + } + } + }; +} + +// ---- dispatched methods ------------------------------------------------- +// +// Add new entries here when adding a method to the dispatch surface. Each +// entry must also be implemented in `turbo-tasks-handle`'s provider macro +// (which currently lists them explicitly — a future cleanup could share +// this list via a callback macro, but for now duplication is the cost of +// keeping both places easy to read). + +tt_decl_extern!(fn invalidate(task: crate::TaskId)); +tt_decl_handle_method!(fn invalidate(task: crate::TaskId)); + +// ===================================================================== +// Clone / Drop dispatch +// ===================================================================== + +unsafe extern "Rust" { + fn __tt_prod_clone_arc(ptr: *const ()); + fn __tt_prod_drop_arc(ptr: *const ()); + fn __tt_test_clone_arc(ptr: *const ()); + fn __tt_test_drop_arc(ptr: *const ()); +} + +impl Clone for TurboTasksHandle { + #[inline] + fn clone(&self) -> Self { + match self.tag { + HandleTag::Prod => unsafe { __tt_prod_clone_arc(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_clone_arc(self.ptr.as_ptr()) }, + } + Self { + tag: self.tag, + ptr: self.ptr, + } + } +} + +impl Drop for TurboTasksHandle { + #[inline] + fn drop(&mut self) { + match self.tag { + HandleTag::Prod => unsafe { __tt_prod_drop_arc(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_drop_arc(self.ptr.as_ptr()) }, + } + } +} diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index 6391b50c8597..c1af98f46b9b 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -10,6 +10,7 @@ #![feature(async_fn_traits)] #![feature(impl_trait_in_assoc_type)] #![feature(const_type_name)] +#![feature(macro_metavar_expr_concat)] pub mod backend; mod capture_future; @@ -24,6 +25,7 @@ mod effect; mod error; pub mod event; pub mod graph; +mod handle; mod id; mod id_factory; mod invalidation; @@ -81,6 +83,7 @@ pub use crate::{ }, effect::{Effect, EffectError, EffectStateStorage, Effects, emit_effect, take_effects}, error::PrettyPrintError, + handle::{HandleTag, TurboTasksHandle}, id::{ExecutionId, LocalTaskId, TRANSIENT_TASK_BIT, TaskId, TraitTypeId, ValueTypeId}, invalidation::{ InvalidationReason, InvalidationReasonKind, InvalidationReasonSet, Invalidator, From 07dd4bb97c01c9fdaf214e544aad1639d412f736 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Sun, 17 May 2026 19:45:33 -0700 Subject: [PATCH 03/19] Phase 1c: expand dispatch surface to full TurboTasksApi/CallApi method list Adds the remaining ~21 methods of the TurboTasksApi + TurboTasksCallApi trait surface to the tagged-pointer dispatch: TurboTasksCallApi: dynamic_call, native_call, trait_call, send_compilation_event, get_task_name TurboTasksApi: invalidate (already wired), invalidate_with_reason, invalidate_serialization, try_read_task_output, try_read_task_cell, try_read_local_output, read_task_collectibles, emit_collectible, unemit_collectible, unemit_collectibles, try_read_own_task_cell, read_own_task_cell, update_own_task_cell, mark_own_task_as_finished, connect_task, spawn_detached_for_testing, subscribe_to_compilation_events, is_tracking_dependencies The following remain OFF the dispatch surface, per the plan: run, run_once, run_once_with_reason, start_once_process, stop_and_wait, task_statistics Every caller of these already has a concrete TurboTasks in scope (via direct construction in #[tokio::test] / benches / fuzz / the napi top-level run), so they don't need extern "Rust" dispatch. Also switches the provider macro from UFCS `::` to method-call syntax `tt.`, which resolves both supertrait (TurboTasksCallApi) and subtrait (TurboTasksApi) methods uniformly. Cleans up Arc clone/drop to use Arc::increment_strong_count / decrement_strong_count instead of the from_raw -> clone -> forget dance. Cleaner and avoids the sketchy debug_assert on pointer identity. All four feature combinations of turbo-tasks-handle compile: none, prod, test, prod+test. Workspace check passes. --- Cargo.lock | 3 + .../crates/turbo-tasks-handle/Cargo.toml | 3 + .../crates/turbo-tasks-handle/src/lib.rs | 221 ++++++++++++++++-- turbopack/crates/turbo-tasks/src/handle.rs | 188 +++++++++++++++ 4 files changed, 390 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e41bb7c8159..386500e668be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10020,7 +10020,10 @@ dependencies = [ name = "turbo-tasks-handle" version = "0.1.0" dependencies = [ + "anyhow", "either", + "smallvec", + "tokio", "turbo-tasks", "turbo-tasks-backend", "turbo-tasks-testing", diff --git a/turbopack/crates/turbo-tasks-handle/Cargo.toml b/turbopack/crates/turbo-tasks-handle/Cargo.toml index 6b4edd5e4701..9643ece904b6 100644 --- a/turbopack/crates/turbo-tasks-handle/Cargo.toml +++ b/turbopack/crates/turbo-tasks-handle/Cargo.toml @@ -19,6 +19,9 @@ prod = ["dep:turbo-tasks-backend", "dep:either"] test = ["dep:turbo-tasks-testing"] [dependencies] +anyhow = { workspace = true } +smallvec = { workspace = true } +tokio = { workspace = true, features = ["sync"] } turbo-tasks = { workspace = true } either = { workspace = true, optional = true } diff --git a/turbopack/crates/turbo-tasks-handle/src/lib.rs b/turbopack/crates/turbo-tasks-handle/src/lib.rs index 95f9ba9315a3..bcad2ed0b457 100644 --- a/turbopack/crates/turbo-tasks-handle/src/lib.rs +++ b/turbopack/crates/turbo-tasks-handle/src/lib.rs @@ -38,11 +38,15 @@ pub type ProdHandleConcrete = turbo_tasks::TurboTasks< #[cfg(feature = "prod")] mod prod { + use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; + use super::*; /// Generates `#[no_mangle] pub extern "Rust" fn __tt_prod_(...)` /// for a single dispatched method. The body casts `ptr` back to - /// `&ProdHandleConcrete` and forwards to the trait method. + /// `&ProdHandleConcrete` and forwards through method call syntax so + /// both `TurboTasksApi` and `TurboTasksCallApi` methods resolve + /// correctly (the former is a super-trait of the latter). macro_rules! provide_prod { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? @@ -53,7 +57,7 @@ mod prod { $(, $arg : $ty)* ) $(-> $ret)? { let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; - ::$name(tt $(, $arg)*) + tt.$name($($arg),*) } }; } @@ -66,32 +70,112 @@ mod prod { // generate both from a shared callback macro, but for now duplicating // the list is the simplest source-of-truth. + // TurboTasksCallApi + provide_prod!(fn dynamic_call( + native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, + this: Option, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, + ) -> turbo_tasks::RawVc); + provide_prod!(fn native_call( + native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, + this: Option, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, + ) -> turbo_tasks::RawVc); + provide_prod!(fn trait_call( + trait_method: &'static turbo_tasks::TraitMethod, + this: turbo_tasks::RawVc, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, + ) -> turbo_tasks::RawVc); + provide_prod!(fn send_compilation_event( + event: ::std::sync::Arc, + )); + provide_prod!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); + + // TurboTasksApi provide_prod!(fn invalidate(task: turbo_tasks::TaskId)); + provide_prod!(fn invalidate_with_reason( + task: turbo_tasks::TaskId, + reason: turbo_tasks::util::StaticOrArc, + )); + provide_prod!(fn invalidate_serialization(task: turbo_tasks::TaskId)); + provide_prod!(fn try_read_task_output( + task: turbo_tasks::TaskId, + options: turbo_tasks::ReadOutputOptions, + ) -> ::anyhow::Result<::core::result::Result>); + provide_prod!(fn try_read_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + options: turbo_tasks::ReadCellOptions, + ) -> ::anyhow::Result<::core::result::Result>); + provide_prod!(fn try_read_local_output( + execution_id: turbo_tasks::ExecutionId, + local_task_id: turbo_tasks::LocalTaskId, + ) -> ::anyhow::Result<::core::result::Result>); + provide_prod!(fn read_task_collectibles( + task: turbo_tasks::TaskId, + trait_id: turbo_tasks::TraitTypeId, + ) -> turbo_tasks::backend::TaskCollectiblesMap); + provide_prod!(fn emit_collectible( + trait_type: turbo_tasks::TraitTypeId, + collectible: turbo_tasks::RawVc, + )); + provide_prod!(fn unemit_collectible( + trait_type: turbo_tasks::TraitTypeId, + collectible: turbo_tasks::RawVc, + count: u32, + )); + provide_prod!(fn unemit_collectibles( + trait_type: turbo_tasks::TraitTypeId, + collectibles: &turbo_tasks::backend::TaskCollectiblesMap, + )); + provide_prod!(fn try_read_own_task_cell( + current_task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + ) -> ::anyhow::Result); + provide_prod!(fn read_own_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + ) -> ::anyhow::Result); + provide_prod!(fn update_own_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + content: turbo_tasks::backend::CellContent, + updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, + content_hash: ::core::option::Option, + verification_mode: turbo_tasks::backend::VerificationMode, + )); + provide_prod!(fn mark_own_task_as_finished(task: turbo_tasks::TaskId)); + provide_prod!(fn connect_task(task: turbo_tasks::TaskId)); + provide_prod!(fn spawn_detached_for_testing( + f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, + )); + provide_prod!(fn subscribe_to_compilation_events( + event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, + ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); + provide_prod!(fn is_tracking_dependencies() -> bool); // ---- Arc clone / drop ------------------------------------------------- #[unsafe(no_mangle)] pub extern "Rust" fn __tt_prod_clone_arc(ptr: *const ()) { - // Reconstitute the Arc transiently to bump the refcount, then leak - // it again so the next drop sees the same pointer. - let arc = unsafe { Arc::from_raw(ptr as *const ProdHandleConcrete) }; - let cloned = arc.clone(); - // Re-leak the original to keep the original handle alive. - std::mem::forget(arc); - // The clone is owned by the new handle that triggered this call; - // leak its pointer so the new handle owns it. - let _new_ptr = Arc::into_raw(cloned); - // `_new_ptr` must equal `ptr` because Arc::into_raw is reproducible - // for the same Arc — the new handle keeps `ptr` and we discard - // `_new_ptr`. (See the Drop impl in turbo-tasks for the matching - // decrement; both ends use the same pointer value.) - debug_assert_eq!(_new_ptr as *const (), ptr); + // Bump the refcount of the Arc whose data pointer is `ptr`. The + // caller (`::clone`) is responsible for + // reusing the same `ptr` value in the new handle, so we don't need + // to return anything. + unsafe { + Arc::::increment_strong_count(ptr as *const ProdHandleConcrete) + } } #[unsafe(no_mangle)] pub extern "Rust" fn __tt_prod_drop_arc(ptr: *const ()) { - // Reconstitute the Arc to decrement and drop. - drop(unsafe { Arc::from_raw(ptr as *const ProdHandleConcrete) }); + // Decrement the refcount; runs the destructor when it reaches zero. + unsafe { + Arc::::decrement_strong_count(ptr as *const ProdHandleConcrete) + } } /// Constructs a `TurboTasksHandle` pointing at the given prod Arc. @@ -116,6 +200,8 @@ pub use prod::from_prod; #[cfg(feature = "test")] mod test_arm { + use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; + use super::*; pub type TestHandleConcrete = turbo_tasks_testing::VcStorage; @@ -130,7 +216,7 @@ mod test_arm { $(, $arg : $ty)* ) $(-> $ret)? { let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; - ::$name(tt $(, $arg)*) + tt.$name($($arg),*) } }; } @@ -138,22 +224,107 @@ mod test_arm { // ---- dispatched methods ---------------------------------------------- // (Mirror of the `prod` block — keep in sync.) + // TurboTasksCallApi + provide_test!(fn dynamic_call( + native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, + this: Option, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, + ) -> turbo_tasks::RawVc); + provide_test!(fn native_call( + native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, + this: Option, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, + ) -> turbo_tasks::RawVc); + provide_test!(fn trait_call( + trait_method: &'static turbo_tasks::TraitMethod, + this: turbo_tasks::RawVc, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, + ) -> turbo_tasks::RawVc); + provide_test!(fn send_compilation_event( + event: ::std::sync::Arc, + )); + provide_test!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); + + // TurboTasksApi provide_test!(fn invalidate(task: turbo_tasks::TaskId)); + provide_test!(fn invalidate_with_reason( + task: turbo_tasks::TaskId, + reason: turbo_tasks::util::StaticOrArc, + )); + provide_test!(fn invalidate_serialization(task: turbo_tasks::TaskId)); + provide_test!(fn try_read_task_output( + task: turbo_tasks::TaskId, + options: turbo_tasks::ReadOutputOptions, + ) -> ::anyhow::Result<::core::result::Result>); + provide_test!(fn try_read_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + options: turbo_tasks::ReadCellOptions, + ) -> ::anyhow::Result<::core::result::Result>); + provide_test!(fn try_read_local_output( + execution_id: turbo_tasks::ExecutionId, + local_task_id: turbo_tasks::LocalTaskId, + ) -> ::anyhow::Result<::core::result::Result>); + provide_test!(fn read_task_collectibles( + task: turbo_tasks::TaskId, + trait_id: turbo_tasks::TraitTypeId, + ) -> turbo_tasks::backend::TaskCollectiblesMap); + provide_test!(fn emit_collectible( + trait_type: turbo_tasks::TraitTypeId, + collectible: turbo_tasks::RawVc, + )); + provide_test!(fn unemit_collectible( + trait_type: turbo_tasks::TraitTypeId, + collectible: turbo_tasks::RawVc, + count: u32, + )); + provide_test!(fn unemit_collectibles( + trait_type: turbo_tasks::TraitTypeId, + collectibles: &turbo_tasks::backend::TaskCollectiblesMap, + )); + provide_test!(fn try_read_own_task_cell( + current_task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + ) -> ::anyhow::Result); + provide_test!(fn read_own_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + ) -> ::anyhow::Result); + provide_test!(fn update_own_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + content: turbo_tasks::backend::CellContent, + updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, + content_hash: ::core::option::Option, + verification_mode: turbo_tasks::backend::VerificationMode, + )); + provide_test!(fn mark_own_task_as_finished(task: turbo_tasks::TaskId)); + provide_test!(fn connect_task(task: turbo_tasks::TaskId)); + provide_test!(fn spawn_detached_for_testing( + f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, + )); + provide_test!(fn subscribe_to_compilation_events( + event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, + ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); + provide_test!(fn is_tracking_dependencies() -> bool); // ---- Arc clone / drop ------------------------------------------------- #[unsafe(no_mangle)] pub extern "Rust" fn __tt_test_clone_arc(ptr: *const ()) { - let arc = unsafe { Arc::from_raw(ptr as *const TestHandleConcrete) }; - let cloned = arc.clone(); - std::mem::forget(arc); - let _new_ptr = Arc::into_raw(cloned); - debug_assert_eq!(_new_ptr as *const (), ptr); + unsafe { + Arc::::increment_strong_count(ptr as *const TestHandleConcrete) + } } #[unsafe(no_mangle)] pub extern "Rust" fn __tt_test_drop_arc(ptr: *const ()) { - drop(unsafe { Arc::from_raw(ptr as *const TestHandleConcrete) }); + unsafe { + Arc::::decrement_strong_count(ptr as *const TestHandleConcrete) + } } pub fn from_test(arc: Arc) -> TurboTasksHandle { diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 9632939118d2..a05b6b2aa22f 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -170,9 +170,197 @@ macro_rules! tt_decl_handle_method { // this list via a callback macro, but for now duplication is the cost of // keeping both places easy to read). +// `TurboTasksCallApi` methods. +tt_decl_extern!(fn dynamic_call( + native_fn: &'static crate::native_function::NativeFunction, + this: Option, + arg: &mut dyn crate::StackDynTaskInputs, + persistence: crate::TaskPersistence, +) -> crate::RawVc); +tt_decl_handle_method!(fn dynamic_call( + native_fn: &'static crate::native_function::NativeFunction, + this: Option, + arg: &mut dyn crate::StackDynTaskInputs, + persistence: crate::TaskPersistence, +) -> crate::RawVc); + +tt_decl_extern!(fn native_call( + native_fn: &'static crate::native_function::NativeFunction, + this: Option, + arg: &mut dyn crate::StackDynTaskInputs, + persistence: crate::TaskPersistence, +) -> crate::RawVc); +tt_decl_handle_method!(fn native_call( + native_fn: &'static crate::native_function::NativeFunction, + this: Option, + arg: &mut dyn crate::StackDynTaskInputs, + persistence: crate::TaskPersistence, +) -> crate::RawVc); + +tt_decl_extern!(fn trait_call( + trait_method: &'static crate::TraitMethod, + this: crate::RawVc, + arg: &mut dyn crate::StackDynTaskInputs, + persistence: crate::TaskPersistence, +) -> crate::RawVc); +tt_decl_handle_method!(fn trait_call( + trait_method: &'static crate::TraitMethod, + this: crate::RawVc, + arg: &mut dyn crate::StackDynTaskInputs, + persistence: crate::TaskPersistence, +) -> crate::RawVc); + +tt_decl_extern!(fn send_compilation_event( + event: ::std::sync::Arc, +)); +tt_decl_handle_method!(fn send_compilation_event( + event: ::std::sync::Arc, +)); + +tt_decl_extern!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); +tt_decl_handle_method!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); + +// `TurboTasksApi` methods (inherits TurboTasksCallApi above). tt_decl_extern!(fn invalidate(task: crate::TaskId)); tt_decl_handle_method!(fn invalidate(task: crate::TaskId)); +tt_decl_extern!(fn invalidate_with_reason( + task: crate::TaskId, + reason: crate::util::StaticOrArc, +)); +tt_decl_handle_method!(fn invalidate_with_reason( + task: crate::TaskId, + reason: crate::util::StaticOrArc, +)); + +tt_decl_extern!(fn invalidate_serialization(task: crate::TaskId)); +tt_decl_handle_method!(fn invalidate_serialization(task: crate::TaskId)); + +tt_decl_extern!(fn try_read_task_output( + task: crate::TaskId, + options: crate::ReadOutputOptions, +) -> ::anyhow::Result<::core::result::Result>); +tt_decl_handle_method!(fn try_read_task_output( + task: crate::TaskId, + options: crate::ReadOutputOptions, +) -> ::anyhow::Result<::core::result::Result>); + +tt_decl_extern!(fn try_read_task_cell( + task: crate::TaskId, + index: crate::CellId, + options: crate::ReadCellOptions, +) -> ::anyhow::Result<::core::result::Result>); +tt_decl_handle_method!(fn try_read_task_cell( + task: crate::TaskId, + index: crate::CellId, + options: crate::ReadCellOptions, +) -> ::anyhow::Result<::core::result::Result>); + +tt_decl_extern!(fn try_read_local_output( + execution_id: crate::ExecutionId, + local_task_id: crate::LocalTaskId, +) -> ::anyhow::Result<::core::result::Result>); +tt_decl_handle_method!(fn try_read_local_output( + execution_id: crate::ExecutionId, + local_task_id: crate::LocalTaskId, +) -> ::anyhow::Result<::core::result::Result>); + +tt_decl_extern!(fn read_task_collectibles( + task: crate::TaskId, + trait_id: crate::TraitTypeId, +) -> crate::backend::TaskCollectiblesMap); +tt_decl_handle_method!(fn read_task_collectibles( + task: crate::TaskId, + trait_id: crate::TraitTypeId, +) -> crate::backend::TaskCollectiblesMap); + +tt_decl_extern!(fn emit_collectible( + trait_type: crate::TraitTypeId, + collectible: crate::RawVc, +)); +tt_decl_handle_method!(fn emit_collectible( + trait_type: crate::TraitTypeId, + collectible: crate::RawVc, +)); + +tt_decl_extern!(fn unemit_collectible( + trait_type: crate::TraitTypeId, + collectible: crate::RawVc, + count: u32, +)); +tt_decl_handle_method!(fn unemit_collectible( + trait_type: crate::TraitTypeId, + collectible: crate::RawVc, + count: u32, +)); + +tt_decl_extern!(fn unemit_collectibles( + trait_type: crate::TraitTypeId, + collectibles: &crate::backend::TaskCollectiblesMap, +)); +tt_decl_handle_method!(fn unemit_collectibles( + trait_type: crate::TraitTypeId, + collectibles: &crate::backend::TaskCollectiblesMap, +)); + +tt_decl_extern!(fn try_read_own_task_cell( + current_task: crate::TaskId, + index: crate::CellId, +) -> ::anyhow::Result); +tt_decl_handle_method!(fn try_read_own_task_cell( + current_task: crate::TaskId, + index: crate::CellId, +) -> ::anyhow::Result); + +tt_decl_extern!(fn read_own_task_cell( + task: crate::TaskId, + index: crate::CellId, +) -> ::anyhow::Result); +tt_decl_handle_method!(fn read_own_task_cell( + task: crate::TaskId, + index: crate::CellId, +) -> ::anyhow::Result); + +tt_decl_extern!(fn update_own_task_cell( + task: crate::TaskId, + index: crate::CellId, + content: crate::backend::CellContent, + updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, + content_hash: ::core::option::Option, + verification_mode: crate::backend::VerificationMode, +)); +tt_decl_handle_method!(fn update_own_task_cell( + task: crate::TaskId, + index: crate::CellId, + content: crate::backend::CellContent, + updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, + content_hash: ::core::option::Option, + verification_mode: crate::backend::VerificationMode, +)); + +tt_decl_extern!(fn mark_own_task_as_finished(task: crate::TaskId)); +tt_decl_handle_method!(fn mark_own_task_as_finished(task: crate::TaskId)); + +tt_decl_extern!(fn connect_task(task: crate::TaskId)); +tt_decl_handle_method!(fn connect_task(task: crate::TaskId)); + +tt_decl_extern!(fn spawn_detached_for_testing( + f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, +)); +tt_decl_handle_method!(fn spawn_detached_for_testing( + f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, +)); + +tt_decl_extern!(fn subscribe_to_compilation_events( + event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, +) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); +tt_decl_handle_method!(fn subscribe_to_compilation_events( + event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, +) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); + +tt_decl_extern!(fn is_tracking_dependencies() -> bool); +tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); + // ===================================================================== // Clone / Drop dispatch // ===================================================================== From f8edef2a2ed5465350d25757100ba163cce7ebc7 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Sun, 17 May 2026 19:51:29 -0700 Subject: [PATCH 04/19] Phase 2a-i: add TurboTasksWeakHandle dispatch alongside TurboTasksHandle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The thread-local will soon hold a TurboTasksHandle instead of an Arc, so turbo_tasks_weak() needs a parallel TurboTasksWeakHandle type — the existing Arc::downgrade(arc) path breaks once the task-local stops holding an Arc. Adds: * pub struct TurboTasksWeakHandle { tag, ptr } — mirrors TurboTasksHandle but holds a Weak::into_raw() pointer. * TurboTasksHandle::downgrade(&self) -> TurboTasksWeakHandle. * TurboTasksWeakHandle::upgrade(&self) -> Option. * Per-arm extern "Rust" symbols: __tt__downgrade, _upgrade, _clone_weak, _drop_weak. Providers in turbo-tasks-handle round- trip through Weak::from_raw/Weak::into_raw and Arc::downgrade / Weak::upgrade. The plan suggested deferring weak-handle work to a later phase if benches showed scope-spawning didn't matter. After auditing the weak-handle consumers (turbo-tasks-fs's filesystem watcher, four sites — all real and load-bearing), it's cleaner to build the weak handle now alongside the strong handle than to leave a half-Weak hybrid behind. No call sites switched yet. --- .../crates/turbo-tasks-handle/src/lib.rs | 75 +++++++++++++ turbopack/crates/turbo-tasks/src/handle.rs | 103 +++++++++++++++++- turbopack/crates/turbo-tasks/src/lib.rs | 2 +- 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/turbopack/crates/turbo-tasks-handle/src/lib.rs b/turbopack/crates/turbo-tasks-handle/src/lib.rs index bcad2ed0b457..879d2d8e2963 100644 --- a/turbopack/crates/turbo-tasks-handle/src/lib.rs +++ b/turbopack/crates/turbo-tasks-handle/src/lib.rs @@ -178,6 +178,47 @@ mod prod { } } + // ---- Weak refcount providers ----------------------------------------- + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const () { + // Reconstitute the Arc transiently to call `downgrade`, then leak + // the Arc back so its refcount is unchanged. The Weak we produce + // owns its own weak refcount. + let arc = unsafe { Arc::from_raw(arc_ptr as *const ProdHandleConcrete) }; + let weak = Arc::downgrade(&arc); + ::std::mem::forget(arc); + ::std::sync::Weak::into_raw(weak) as *const () + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const () { + // Reconstitute the Weak transiently to attempt upgrade, then leak + // it back so its refcount is unchanged. + let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; + let maybe_arc = weak.upgrade(); + ::std::mem::forget(weak); + match maybe_arc { + Some(arc) => Arc::into_raw(arc) as *const (), + None => ::std::ptr::null(), + } + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_clone_weak(weak_ptr: *const ()) { + // `Weak` has no `increment_weak_count` API, so we round-trip + // through `Weak::clone` and leak both copies. + let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; + let cloned = weak.clone(); + ::std::mem::forget(weak); + ::std::mem::forget(cloned); + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_drop_weak(weak_ptr: *const ()) { + drop(unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }); + } + /// Constructs a `TurboTasksHandle` pointing at the given prod Arc. /// /// The caller transfers ownership of one refcount into the handle. @@ -327,6 +368,40 @@ mod test_arm { } } + // ---- Weak refcount providers ----------------------------------------- + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_downgrade(arc_ptr: *const ()) -> *const () { + let arc = unsafe { Arc::from_raw(arc_ptr as *const TestHandleConcrete) }; + let weak = Arc::downgrade(&arc); + ::std::mem::forget(arc); + ::std::sync::Weak::into_raw(weak) as *const () + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_upgrade(weak_ptr: *const ()) -> *const () { + let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const TestHandleConcrete) }; + let maybe_arc = weak.upgrade(); + ::std::mem::forget(weak); + match maybe_arc { + Some(arc) => Arc::into_raw(arc) as *const (), + None => ::std::ptr::null(), + } + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_clone_weak(weak_ptr: *const ()) { + let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const TestHandleConcrete) }; + let cloned = weak.clone(); + ::std::mem::forget(weak); + ::std::mem::forget(cloned); + } + + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_drop_weak(weak_ptr: *const ()) { + drop(unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const TestHandleConcrete) }); + } + pub fn from_test(arc: Arc) -> TurboTasksHandle { let ptr = Arc::into_raw(arc) as *mut (); unsafe { diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index a05b6b2aa22f..91cfc658eef1 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -362,7 +362,7 @@ tt_decl_extern!(fn is_tracking_dependencies() -> bool); tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); // ===================================================================== -// Clone / Drop dispatch +// Clone / Drop dispatch — Arc-style refcounting through extern symbols. // ===================================================================== unsafe extern "Rust" { @@ -370,6 +370,22 @@ unsafe extern "Rust" { fn __tt_prod_drop_arc(ptr: *const ()); fn __tt_test_clone_arc(ptr: *const ()); fn __tt_test_drop_arc(ptr: *const ()); + + // Weak-handle support. Each arm provides: + // downgrade : *const Arc -> *const Weak (transfers no refcount, + // creates a fresh Weak; caller owns the returned weak). + // upgrade : *const Weak -> *const Arc (returns null if the + // Arc is gone; otherwise transfers one strong refcount). + // clone_weak: bumps the weak refcount. + // drop_weak : drops the weak refcount. + fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const (); + fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const (); + fn __tt_prod_clone_weak(weak_ptr: *const ()); + fn __tt_prod_drop_weak(weak_ptr: *const ()); + fn __tt_test_downgrade(arc_ptr: *const ()) -> *const (); + fn __tt_test_upgrade(weak_ptr: *const ()) -> *const (); + fn __tt_test_clone_weak(weak_ptr: *const ()); + fn __tt_test_drop_weak(weak_ptr: *const ()); } impl Clone for TurboTasksHandle { @@ -395,3 +411,88 @@ impl Drop for TurboTasksHandle { } } } + +impl TurboTasksHandle { + /// Downgrades to a weak handle, equivalent to `Arc::downgrade`. + #[inline] + pub fn downgrade(&self) -> TurboTasksWeakHandle { + let weak_ptr = match self.tag { + HandleTag::Prod => unsafe { __tt_prod_downgrade(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_downgrade(self.ptr.as_ptr()) }, + }; + TurboTasksWeakHandle { + tag: self.tag, + // `downgrade` always produces a valid pointer (a `Weak` is never + // null even when the strong count is zero); we can safely + // `NonNull::new_unchecked` it. + ptr: unsafe { NonNull::new_unchecked(weak_ptr as *mut ()) }, + } + } +} + +// ===================================================================== +// Weak-handle dispatch. +// +// Mirrors the strong-handle dispatch but holds the data pointer of a +// `Weak`. Used by long-lived non-task contexts (e.g. the filesystem +// watcher in `turbo-tasks-fs`) that need to reach back into TurboTasks +// without keeping it alive. +// ===================================================================== + +/// Weak counterpart to [`TurboTasksHandle`]. Constructed via +/// [`TurboTasksHandle::downgrade`]; upgraded via +/// [`TurboTasksWeakHandle::upgrade`]. +#[derive(Debug)] +pub struct TurboTasksWeakHandle { + tag: HandleTag, + /// Points at the inner of a `Weak` owned via + /// `Weak::into_raw`. The strong count may be zero by the time we + /// try to upgrade. + ptr: NonNull<()>, +} + +// Safety: as with `TurboTasksHandle`, the concrete weak pointer's data +// is `Send + Sync` for any `T: Send + Sync`. +unsafe impl Send for TurboTasksWeakHandle {} +unsafe impl Sync for TurboTasksWeakHandle {} + +impl TurboTasksWeakHandle { + /// Tries to recover a strong handle. Returns `None` if the underlying + /// concrete handle has been dropped. + #[inline] + pub fn upgrade(&self) -> Option { + let strong_ptr = match self.tag { + HandleTag::Prod => unsafe { __tt_prod_upgrade(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_upgrade(self.ptr.as_ptr()) }, + }; + let strong_ptr = NonNull::new(strong_ptr as *mut ())?; + // Safety: the provider returned a non-null `Arc::into_raw` pointer + // for the concrete type indicated by `self.tag`. Ownership of one + // strong refcount transfers in. + Some(unsafe { TurboTasksHandle::from_raw_parts(self.tag, strong_ptr) }) + } +} + +impl Clone for TurboTasksWeakHandle { + #[inline] + fn clone(&self) -> Self { + match self.tag { + HandleTag::Prod => unsafe { __tt_prod_clone_weak(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_clone_weak(self.ptr.as_ptr()) }, + } + Self { + tag: self.tag, + ptr: self.ptr, + } + } +} + +impl Drop for TurboTasksWeakHandle { + #[inline] + fn drop(&mut self) { + match self.tag { + HandleTag::Prod => unsafe { __tt_prod_drop_weak(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_drop_weak(self.ptr.as_ptr()) }, + } + } +} diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index c1af98f46b9b..ee9415fef1cb 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -83,7 +83,7 @@ pub use crate::{ }, effect::{Effect, EffectError, EffectStateStorage, Effects, emit_effect, take_effects}, error::PrettyPrintError, - handle::{HandleTag, TurboTasksHandle}, + handle::{HandleTag, TurboTasksHandle, TurboTasksWeakHandle}, id::{ExecutionId, LocalTaskId, TRANSIENT_TASK_BIT, TaskId, TraitTypeId, ValueTypeId}, invalidation::{ InvalidationReason, InvalidationReasonKind, InvalidationReasonSet, Invalidator, From 6599ba07cc16c621d4153dadcb1014bff471f763 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Sun, 17 May 2026 23:29:41 -0700 Subject: [PATCH 05/19] Phase 2a: flip TURBO_TASKS task-local from Arc to TurboTasksHandle (WIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the main flip. Workspace 'cargo check' is green; building test binaries hits a linkage issue (see end of message). Core change: * task_local! { TURBO_TASKS: TurboTasksHandle } in manager.rs (was Arc). * Public accessors turbo_tasks(), try_turbo_tasks(), with_turbo_tasks, turbo_tasks_weak(), turbo_tasks_scope, turbo_tasks_future_scope, with_turbo_tasks_for_testing, and the free run/run_once/ run_once_with_reason helpers now return/take TurboTasksHandle. * TurboTasksWeakHandle parallels TurboTasksHandle for the rare weak case (used by the fs watcher). Method dispatch surface expanded: * run, run_once, run_once_with_reason, start_once_process, stop_and_wait, task_statistics are now also on the dispatch surface — the test harness in turbo-tasks-testing type-erases its TestInstance.tt and calls turbo_tasks::run_once() on it, so 'off the dispatch surface' didn't hold for these methods after all. * task_statistics returns &TaskStatisticsApi; the extern provider returns *const TaskStatisticsApi and the handle wrapper re-binds the lifetime to &self. * provide_prod_trait! / provide_test_trait! variants force UFCS for methods whose inherent signature on TurboTasks shadows the trait method's signature (the run/stop family). Internal API signature changes: * Invalidator::invalidate, invalidate_with_reason now take &TurboTasksHandle (was &dyn TurboTasksApi). * read_task_output, read_local_output internal helpers take &TurboTasksHandle. wait_task_completion's own loop is inlined. * TurboTaskContextError.turbo_tasks field changed from Arc to TurboTasksHandle. * Backend operation::Context::turbo_tasks() returns TurboTasksHandle (uses thread-local). Construction sites: * TurboTasks::make_handle(self: Arc) -> TurboTasksHandle inherent method on TurboTasks and on VcStorage. * make_handle_from_ref(&self) for ergonomic use inside TurboTasks's own methods that need to scope into themselves. Downstream updates: * turbo-tasks-fs DiskFileSystemInner.turbo_tasks Weak → TurboTasksWeakHandle. * turbo-tasks-fs watcher signatures changed. * turbopack-dev-server, turbopack-cli, turbopack-cli's serve() signature take TurboTasksHandle; UpdateServer::run too. * turbo-tasks-testing TestInstance.tt and helpers use handles. Cargo deps: * next-napi-bindings dev-deps turbo-tasks-handle [features = 'prod']. * turbo-tasks-backend dev-deps turbo-tasks-handle [features = 'prod', 'test'] for tests + benches. ** KNOWN ISSUE — TEST BUILD LINKAGE ** Test binaries fail to link because cargo doesn't pull 'libturbo_tasks_handle.rlib' into the test binary's link command even though it's declared as a dev-dep. Investigation needed: - Likely cause: cargo skips a dev-dep that's never referenced by name in the test sources. - Quick workaround: add 'extern crate turbo_tasks_handle;' to each test file that lives in turbo-tasks-backend/tests/. ~30 files. - Better solution: probably move providers to a separate turbo-tasks-prod-handle / turbo-tasks-test-handle pair of crates that consumers reference explicitly, or use a build-script trick to force linkage. The cdylib (next-napi-bindings) does link correctly because cdylib linking includes all declared deps. --- Cargo.lock | 2 + crates/next-napi-bindings/Cargo.toml | 4 + .../crates/turbo-tasks-backend/Cargo.toml | 4 + .../turbo-tasks-backend/src/backend/mod.rs | 2 +- .../src/backend/operation/mod.rs | 9 +- .../turbo-tasks-backend/tests/eviction.rs | 14 +-- .../tests/read_ref_cell.rs | 2 +- .../tests/trait_ref_cell.rs | 2 +- .../crates/turbo-tasks-fetch/src/client.rs | 3 +- turbopack/crates/turbo-tasks-fs/src/lib.rs | 16 +-- .../crates/turbo-tasks-fs/src/watcher.rs | 22 ++-- .../crates/turbo-tasks-handle/src/lib.rs | 99 ++++++++++++++- .../crates/turbo-tasks-testing/src/run.rs | 12 +- turbopack/crates/turbo-tasks/src/backend.rs | 12 +- turbopack/crates/turbo-tasks/src/handle.rs | 72 +++++++++++ .../crates/turbo-tasks/src/invalidation.rs | 6 +- turbopack/crates/turbo-tasks/src/manager.rs | 115 +++++++++++------- turbopack/crates/turbo-tasks/src/raw_vc.rs | 9 +- turbopack/crates/turbo-tasks/src/scope.rs | 4 +- turbopack/crates/turbo-tasks/src/state.rs | 2 +- turbopack/crates/turbopack-cli/src/dev/mod.rs | 2 +- .../crates/turbopack-dev-server/src/lib.rs | 8 +- .../turbopack-dev-server/src/update/server.rs | 4 +- 23 files changed, 306 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 386500e668be..3b61259879cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4947,6 +4947,7 @@ dependencies = [ "turbo-tasks", "turbo-tasks-backend", "turbo-tasks-fs", + "turbo-tasks-handle", "turbo-tasks-malloc", "turbo-unix-path", "turbopack-core", @@ -9892,6 +9893,7 @@ dependencies = [ "turbo-persistence", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-handle", "turbo-tasks-hash", "turbo-tasks-malloc", "turbo-tasks-testing", diff --git a/crates/next-napi-bindings/Cargo.toml b/crates/next-napi-bindings/Cargo.toml index 5237da8bd97f..8a1d37e8ed79 100644 --- a/crates/next-napi-bindings/Cargo.toml +++ b/crates/next-napi-bindings/Cargo.toml @@ -94,6 +94,10 @@ swc_plugin_backend_wasmtime = { workspace = true } tokio = { workspace = true, features = ["full"] } turbo-rcstr = { workspace = true, features = ["napi"] } turbo-tasks = { workspace = true } +# Pulls in the `__tt_prod_*` extern "Rust" providers that satisfy the +# forward declarations in `turbo-tasks::handle`. Without this dep the +# cdylib fails to link. +turbo-tasks-handle = { workspace = true, features = ["prod"] } turbo-tasks-backend = { workspace = true } turbo-tasks-fs = { workspace = true } turbo-unix-path = { workspace = true } diff --git a/turbopack/crates/turbo-tasks-backend/Cargo.toml b/turbopack/crates/turbo-tasks-backend/Cargo.toml index d9ffbfaf0cd8..2b5aaa1a5407 100644 --- a/turbopack/crates/turbo-tasks-backend/Cargo.toml +++ b/turbopack/crates/turbo-tasks-backend/Cargo.toml @@ -73,6 +73,10 @@ triomphe = { workspace = true } turbo-tasks-malloc = { workspace = true , features = ["custom_allocator"]} rstest = { workspace = true } turbo-tasks-testing = { workspace = true } +# Tests and benches construct real `TurboTasks` instances; some also +# use `VcStorage` via `turbo-tasks-testing`. Pull in both arms of the +# handle dispatch so the extern "Rust" symbols are linked. +turbo-tasks-handle = { workspace = true, features = ["prod", "test"] } [[bench]] diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs index f7ccd8bdfdce..157445ddb0c8 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs @@ -730,7 +730,7 @@ impl TurboTasksBackendInner { return result.map_err(|error| { self.task_error_to_turbo_tasks_execution_error(&error, &mut ctx) - .with_task_context(task_id, turbo_tasks.pin()) + .with_task_context(task_id, turbo_tasks::turbo_tasks()) .into() }); } diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs b/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs index afad86b812d0..92bffaf0d60d 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs @@ -19,8 +19,7 @@ use tracing::info_span; use tracing::trace_span; use turbo_tasks::{ CellId, DynTaskInputs, FxIndexMap, RawVc, SharedReference, TaskExecutionReason, TaskId, - TaskPriority, TurboTasks, TurboTasksCallApi, backend::CachedTaskType, - macro_helpers::NativeFunction, + TaskPriority, TurboTasks, backend::CachedTaskType, macro_helpers::NativeFunction, }; pub use self::aggregation_update::ComputeDirtyAndCleanUpdate; @@ -97,7 +96,7 @@ pub trait ExecuteContext<'e>: Sized { fn suspending_requested(&self) -> bool; fn should_track_dependencies(&self) -> bool; fn should_track_activeness(&self) -> bool; - fn turbo_tasks(&self) -> Arc; + fn turbo_tasks(&self) -> turbo_tasks::TurboTasksHandle; /// Look up a TaskId from the backing storage for a given task type. /// /// Uses hash-based lookup which may return multiple candidates due to hash collisions, @@ -976,8 +975,8 @@ impl<'e, B: BackingStorage> ExecuteContext<'e> for ExecuteContextImpl<'e, B> { self.backend.should_track_activeness() } - fn turbo_tasks(&self) -> Arc { - self.turbo_tasks.pin() + fn turbo_tasks(&self) -> turbo_tasks::TurboTasksHandle { + turbo_tasks::turbo_tasks() } fn task_by_type( diff --git a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs index 2fa4a7088a01..cd8e74bbf0b9 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs @@ -81,7 +81,7 @@ async fn eviction_recompute() { let (tt, _persistence_dir) = create_tt("eviction_recompute"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); // Create state via operation (persistent task) @@ -123,7 +123,7 @@ async fn eviction_deep_chain() { let (tt, _persistence_dir) = create_tt("eviction_deep_chain"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); let state_op = create_state(10); @@ -180,7 +180,7 @@ async fn eviction_dependency_chain() { let (tt, _persistence_dir) = create_tt("eviction_dependency_chain"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); let state_op = create_state(10); @@ -358,7 +358,7 @@ async fn eviction_session_stateful_survives() { let (tt, _persistence_dir) = create_tt("eviction_session_stateful_survives"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); // read_session_counter internally creates+resolves create_session_counter(42). @@ -411,7 +411,7 @@ async fn eviction_transient_reader_invalidated() { let (tt, _persistence_dir) = create_tt("eviction_transient_reader_invalidated"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); // Create persistent state + compute tasks @@ -525,7 +525,7 @@ async fn eviction_stress_concurrent() { } }); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); let state_op = create_state(1); @@ -667,7 +667,7 @@ async fn eviction_persistable_never_preserves_live_cell() { let (tt, _persistence_dir) = create_tt("eviction_persistable_never_preserves_live_cell"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone(), async move { + let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { unmark_top_level_task_may_leak_eventually_consistent_state(); let state_op = create_state(0); diff --git a/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs b/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs index 83c816a23d20..d200823798ba 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/read_ref_cell.rs @@ -69,7 +69,7 @@ impl Counter { lock.0 += 1; let invalidators = take(&mut lock.1); for i in invalidators { - i.invalidate(&**tt); + i.invalidate(tt); } }); } diff --git a/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs b/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs index f0d0710d2137..1b520148d838 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/trait_ref_cell.rs @@ -78,7 +78,7 @@ impl Counter { lock.0 += 1; let invalidators = take(&mut lock.1); for i in invalidators { - i.invalidate(&**tt); + i.invalidate(tt); } }); } diff --git a/turbopack/crates/turbo-tasks-fetch/src/client.rs b/turbopack/crates/turbo-tasks-fetch/src/client.rs index f9c156a0c1a1..2c36279a52e4 100644 --- a/turbopack/crates/turbo-tasks-fetch/src/client.rs +++ b/turbopack/crates/turbo-tasks-fetch/src/client.rs @@ -285,8 +285,7 @@ impl FetchClientConfig { // FetchClientConfig to track outstanding timers and cancel them. turbo_tasks::spawn(async move { tokio::time::sleep(remaining).await; - invalidator - .invalidate_with_reason(&*turbo_tasks::turbo_tasks(), HttpTimeout {}); + invalidator.invalidate_with_reason(&turbo_tasks::turbo_tasks(), HttpTimeout {}); }); } } diff --git a/turbopack/crates/turbo-tasks-fs/src/lib.rs b/turbopack/crates/turbo-tasks-fs/src/lib.rs index fd6cad52cc47..b38ad3af8a83 100644 --- a/turbopack/crates/turbo-tasks-fs/src/lib.rs +++ b/turbopack/crates/turbo-tasks-fs/src/lib.rs @@ -41,7 +41,7 @@ use std::{ io::{self, BufRead, BufReader, ErrorKind, Read, Write as _}, mem::take, path::{MAIN_SEPARATOR, Path, PathBuf}, - sync::{Arc, LazyLock, Weak}, + sync::{Arc, LazyLock}, time::Duration, }; @@ -63,8 +63,8 @@ use tracing::Instrument; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ Completion, Effect, EffectStateStorage, InvalidationReason, NonLocalValue, ReadRef, ResolvedVc, - TaskInput, TurboTasksApi, ValueToString, ValueToStringRef, Vc, debug::ValueDebugFormat, - emit_effect, parallel, trace::TraceRawVcs, turbo_tasks_weak, turbobail, turbofmt, + TaskInput, ValueToString, ValueToStringRef, Vc, debug::ValueDebugFormat, emit_effect, parallel, + trace::TraceRawVcs, turbo_tasks_weak, turbobail, turbofmt, }; use turbo_tasks_hash::{ DeterministicHash, DeterministicHasher, HashAlgorithm, deterministic_hash, hash_xxh3_hash64, @@ -275,7 +275,7 @@ struct DiskFileSystemInner { /// watcher. #[turbo_tasks(debug_ignore, trace_ignore)] #[bincode(skip, default = "turbo_tasks_weak")] - turbo_tasks: Weak, + turbo_tasks: turbo_tasks::TurboTasksWeakHandle, /// Used by invalidators when called from a non-tokio thread, specifically in the fs watcher. #[turbo_tasks(debug_ignore, trace_ignore)] #[bincode(skip, default = "Handle::current")] @@ -336,7 +336,7 @@ impl DiskFileSystemInner { path: full_path.to_string_lossy().into_owned(), }; for invalidator in invalidators { - invalidator.invalidate_with_reason(&*turbo_tasks, reason.clone()); + invalidator.invalidate_with_reason(&turbo_tasks, reason.clone()); } } } @@ -375,7 +375,7 @@ impl DiskFileSystemInner { .flat_map(|(_, invalidators)| invalidators.into_iter()) .collect::>(); parallel::for_each_owned(invalidators, |invalidator| { - invalidator.invalidate(&*turbo_tasks) + invalidator.invalidate(&turbo_tasks) }); } @@ -405,7 +405,7 @@ impl DiskFileSystemInner { }) .collect::>(); parallel::for_each_owned(invalidators, |(reason, invalidator)| { - invalidator.invalidate_with_reason(&*turbo_tasks, reason) + invalidator.invalidate_with_reason(&turbo_tasks, reason) }); } @@ -474,7 +474,7 @@ impl DiskFileSystemInner { drop(dir_invalidator_map); parallel::for_each_owned(invalidators, |(reason, invalidator)| { - invalidator.invalidate_with_reason(&*turbo_tasks, reason) + invalidator.invalidate_with_reason(&turbo_tasks, reason) }); } diff --git a/turbopack/crates/turbo-tasks-fs/src/watcher.rs b/turbopack/crates/turbo-tasks-fs/src/watcher.rs index 173fea7d5a39..4337fab42e6e 100644 --- a/turbopack/crates/turbo-tasks-fs/src/watcher.rs +++ b/turbopack/crates/turbo-tasks-fs/src/watcher.rs @@ -22,8 +22,8 @@ use tokio::sync::{RwLock, RwLockWriteGuard}; use tracing::instrument; use turbo_rcstr::RcStr; use turbo_tasks::{ - FxIndexSet, InvalidationReason, InvalidationReasonKind, Invalidator, TurboTasksApi, parallel, - spawn_thread, util::StaticOrArc, + FxIndexSet, InvalidationReason, InvalidationReasonKind, Invalidator, parallel, spawn_thread, + util::StaticOrArc, }; use crate::{ @@ -424,14 +424,14 @@ impl DiskWatcher { }) .collect::>(); parallel::for_each_owned(invalidators, |(reason, invalidator)| { - invalidator.invalidate_with_reason(&*turbo_tasks, reason); + invalidator.invalidate_with_reason(&turbo_tasks, reason); }); } else { let invalidators = iter .flat_map(|(_, invalidators)| invalidators.into_iter()) .collect::>(); parallel::for_each_owned(invalidators, |invalidator| { - invalidator.invalidate(&*turbo_tasks); + invalidator.invalidate(&turbo_tasks); }); } } @@ -710,14 +710,14 @@ impl DiskWatcher { let mut invalidator_map = fs_inner.invalidator_map.lock().unwrap(); invalidate_path( &fs_inner, - &*turbo_tasks, + &turbo_tasks, report_invalidation_reason, &mut invalidator_map, batched_invalidate_path.drain(), ); invalidate_path_and_children_execute( &fs_inner, - &*turbo_tasks, + &turbo_tasks, report_invalidation_reason, &mut invalidator_map, batched_invalidate_path_and_children.drain(), @@ -727,14 +727,14 @@ impl DiskWatcher { let mut dir_invalidator_map = fs_inner.dir_invalidator_map.lock().unwrap(); invalidate_path( &fs_inner, - &*turbo_tasks, + &turbo_tasks, report_invalidation_reason, &mut dir_invalidator_map, batched_invalidate_path_dir.drain(), ); invalidate_path_and_children_execute( &fs_inner, - &*turbo_tasks, + &turbo_tasks, report_invalidation_reason, &mut dir_invalidator_map, batched_invalidate_path_and_children_dir.drain(), @@ -772,7 +772,7 @@ impl DiskWatcher { )] fn invalidate( inner: &DiskFileSystemInner, - turbo_tasks: &dyn TurboTasksApi, + turbo_tasks: &turbo_tasks::TurboTasksHandle, report_invalidation_reason: bool, path: &Path, invalidator: Invalidator, @@ -788,7 +788,7 @@ fn invalidate( fn invalidate_path( inner: &DiskFileSystemInner, - turbo_tasks: &dyn TurboTasksApi, + turbo_tasks: &turbo_tasks::TurboTasksHandle, report_invalidation_reason: bool, invalidator_map: &mut LockedInvalidatorMap, paths: impl Iterator, @@ -804,7 +804,7 @@ fn invalidate_path( fn invalidate_path_and_children_execute( inner: &DiskFileSystemInner, - turbo_tasks: &dyn TurboTasksApi, + turbo_tasks: &turbo_tasks::TurboTasksHandle, report_invalidation_reason: bool, invalidator_map: &mut LockedInvalidatorMap, paths: impl Iterator, diff --git a/turbopack/crates/turbo-tasks-handle/src/lib.rs b/turbopack/crates/turbo-tasks-handle/src/lib.rs index 879d2d8e2963..ca9b676da458 100644 --- a/turbopack/crates/turbo-tasks-handle/src/lib.rs +++ b/turbopack/crates/turbo-tasks-handle/src/lib.rs @@ -43,10 +43,10 @@ mod prod { use super::*; /// Generates `#[no_mangle] pub extern "Rust" fn __tt_prod_(...)` - /// for a single dispatched method. The body casts `ptr` back to - /// `&ProdHandleConcrete` and forwards through method call syntax so - /// both `TurboTasksApi` and `TurboTasksCallApi` methods resolve - /// correctly (the former is a super-trait of the latter). + /// for a single dispatched method, dispatched via method call syntax. + /// This resolves to whichever trait method or inherent method has the + /// matching name; for methods that don't have a colliding inherent + /// method on the concrete type, this is fine. macro_rules! provide_prod { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? @@ -62,6 +62,28 @@ mod prod { }; } + /// Same as `provide_prod!`, but forces UFCS dispatch to a specific + /// trait. Used for methods (`run`, `run_once`, `run_once_with_reason`, + /// `start_once_process`, `stop_and_wait`) where the concrete type has + /// an inherent method with the same name but a different return type + /// — without UFCS, the inherent method wins and the macro fails type + /// checking. + macro_rules! provide_prod_trait { + ( + $trait:path, + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + #[unsafe(no_mangle)] + pub extern "Rust" fn ${concat(__tt_prod_, $name)}( + ptr: *const () + $(, $arg : $ty)* + ) $(-> $ret)? { + let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; + ::$name(tt $(, $arg)*) + } + }; + } + // ---- dispatched methods ---------------------------------------------- // // Keep this list in sync with the matching `tt_decl_extern!` / @@ -94,6 +116,21 @@ mod prod { )); provide_prod!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); + provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( + reason: turbo_tasks::util::StaticOrArc, + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn start_once_process( + future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, + )); + provide_prod_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); + // TurboTasksApi provide_prod!(fn invalidate(task: turbo_tasks::TaskId)); provide_prod!(fn invalidate_with_reason( @@ -157,6 +194,20 @@ mod prod { ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); provide_prod!(fn is_tracking_dependencies() -> bool); + // `task_statistics` is special: the trait method returns + // `&TaskStatisticsApi` borrowed from `&self`, but extern "Rust" can't + // carry that lifetime through a `*const ()` receiver. The provider + // returns a raw pointer; the handle wrapper in `turbo-tasks` re-binds + // the lifetime to `&self`. This is sound because the underlying Arc + // (held by the handle) keeps the `TaskStatisticsApi` alive. + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_prod_task_statistics( + ptr: *const (), + ) -> *const turbo_tasks::task_statistics::TaskStatisticsApi { + let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; + tt.task_statistics() as *const _ + } + // ---- Arc clone / drop ------------------------------------------------- #[unsafe(no_mangle)] @@ -262,6 +313,23 @@ mod test_arm { }; } + /// Mirrors `provide_prod_trait!` — see its docs. + macro_rules! provide_test_trait { + ( + $trait:path, + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + #[unsafe(no_mangle)] + pub extern "Rust" fn ${concat(__tt_test_, $name)}( + ptr: *const () + $(, $arg : $ty)* + ) $(-> $ret)? { + let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; + ::$name(tt $(, $arg)*) + } + }; + } + // ---- dispatched methods ---------------------------------------------- // (Mirror of the `prod` block — keep in sync.) @@ -289,6 +357,21 @@ mod test_arm { )); provide_test!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); + provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn run( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn run_once( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( + reason: turbo_tasks::util::StaticOrArc, + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, + ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn start_once_process( + future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, + )); + provide_test_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); + // TurboTasksApi provide_test!(fn invalidate(task: turbo_tasks::TaskId)); provide_test!(fn invalidate_with_reason( @@ -352,6 +435,14 @@ mod test_arm { ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); provide_test!(fn is_tracking_dependencies() -> bool); + #[unsafe(no_mangle)] + pub extern "Rust" fn __tt_test_task_statistics( + ptr: *const (), + ) -> *const turbo_tasks::task_statistics::TaskStatisticsApi { + let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; + tt.task_statistics() as *const _ + } + // ---- Arc clone / drop ------------------------------------------------- #[unsafe(no_mangle)] diff --git a/turbopack/crates/turbo-tasks-testing/src/run.rs b/turbopack/crates/turbo-tasks-testing/src/run.rs index b51facbcafb5..06cbbb50b86f 100644 --- a/turbopack/crates/turbo-tasks-testing/src/run.rs +++ b/turbopack/crates/turbo-tasks-testing/src/run.rs @@ -1,18 +1,18 @@ use std::{env, fmt::Debug, future::Future, sync::Arc}; use anyhow::Result; -use turbo_tasks::{TurboTasks, TurboTasksApi, trace::TraceRawVcs}; +use turbo_tasks::{TurboTasks, TurboTasksHandle, trace::TraceRawVcs}; use turbo_tasks_backend::{BackingStorage, TurboTasksBackend}; /// A freshly created test instance: the `TurboTasks` handle (type-erased to -/// `Arc`) and a closure that, when called, takes a +/// `TurboTasksHandle`) and a closure that, when called, takes a /// snapshot and evicts all evictable tasks on that instance. /// /// The eviction closure captures the concrete backend type internally so -/// harness code holding an erased `TurboTasksApi` can still reach the +/// harness code holding an erased handle can still reach the /// `snapshot_and_evict` API. pub struct TestInstance { - pub tt: Arc, + pub tt: TurboTasksHandle, pub snapshot_and_evict: Box, } @@ -49,7 +49,7 @@ where .snapshot_and_evict_for_testing(&*tt_for_evict); }); TestInstance { - tt: tt as Arc, + tt: tt.make_handle(), snapshot_and_evict, } } @@ -125,7 +125,7 @@ where pub async fn run_with_tt( registration: &Registration, - mut fut: impl FnMut(Arc) -> F + Send + 'static, + mut fut: impl FnMut(TurboTasksHandle) -> F + Send + 'static, ) -> Result<()> where F: Future> + Send + 'static, diff --git a/turbopack/crates/turbo-tasks/src/backend.rs b/turbopack/crates/turbo-tasks/src/backend.rs index e69369a7c297..631c552d3e96 100644 --- a/turbopack/crates/turbo-tasks/src/backend.rs +++ b/turbopack/crates/turbo-tasks/src/backend.rs @@ -29,8 +29,8 @@ use turbo_tasks_hash::DeterministicHasher; use crate::{ RawVc, ReadCellOptions, ReadOutputOptions, ReadRef, SharedReference, TaskId, TaskIdSet, - TaskPriority, TraitRef, TraitTypeId, TurboTasksCallApi, TurboTasksPanic, ValueTypeId, - ValueTypePersistence, VcValueTrait, VcValueType, + TaskPriority, TraitRef, TraitTypeId, TurboTasksPanic, ValueTypeId, ValueTypePersistence, + VcValueTrait, VcValueType, dyn_task_inputs::{DynTaskInputs, StackDynTaskInputs}, event::EventListener, macro_helpers::NativeFunction, @@ -371,7 +371,7 @@ pub struct TurboTasksError { /// [`TurboTasksCallApi::get_task_name`]) rather than eagerly at error creation time. #[derive(Clone)] pub struct TurboTaskContextError { - pub turbo_tasks: Arc, + pub turbo_tasks: crate::TurboTasksHandle, pub task_id: TaskId, pub source: Option, } @@ -434,11 +434,7 @@ pub enum TurboTasksExecutionError { impl TurboTasksExecutionError { /// Wraps this error in a [`TaskContext`](TurboTasksExecutionError::TaskContext) layer /// identifying the normal task that encountered the error. - pub fn with_task_context( - self, - task_id: TaskId, - turbo_tasks: Arc, - ) -> Self { + pub fn with_task_context(self, task_id: TaskId, turbo_tasks: crate::TurboTasksHandle) -> Self { TurboTasksExecutionError::TaskContext(Arc::new(TurboTaskContextError { task_id, turbo_tasks, diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 91cfc658eef1..7a30308d9d87 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -220,6 +220,50 @@ tt_decl_handle_method!(fn send_compilation_event( tt_decl_extern!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); tt_decl_handle_method!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); +// `run`, `run_once`, `run_once_with_reason`, `start_once_process`, and +// `stop_and_wait` need to be on the dispatch surface because the test +// harness in `turbo-tasks-testing` constructs a type-erased +// `TestInstance.tt: TurboTasksHandle` and passes it to the free +// `turbo_tasks::run_once` / `turbo_tasks::run` helpers. Without these on +// the handle, we'd have to either expose the concrete backend type +// through `TestInstance` (cascades into `Registration`) or duplicate the +// helpers per arm. Putting them on the dispatch surface is one extern +// symbol per method per arm — cheap. +tt_decl_extern!(fn run( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); +tt_decl_handle_method!(fn run( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + +tt_decl_extern!(fn run_once( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); +tt_decl_handle_method!(fn run_once( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + +tt_decl_extern!(fn run_once_with_reason( + reason: crate::util::StaticOrArc, + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); +tt_decl_handle_method!(fn run_once_with_reason( + reason: crate::util::StaticOrArc, + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); + +tt_decl_extern!(fn start_once_process( + future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, +)); +tt_decl_handle_method!(fn start_once_process( + future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, +)); + +tt_decl_extern!(fn stop_and_wait() + -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); +tt_decl_handle_method!(fn stop_and_wait() + -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); + // `TurboTasksApi` methods (inherits TurboTasksCallApi above). tt_decl_extern!(fn invalidate(task: crate::TaskId)); tt_decl_handle_method!(fn invalidate(task: crate::TaskId)); @@ -361,6 +405,34 @@ tt_decl_handle_method!(fn subscribe_to_compilation_events( tt_decl_extern!(fn is_tracking_dependencies() -> bool); tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); +// `task_statistics` returns `&TaskStatisticsApi` borrowed from `&self`. +// The macro can't express the lifetime relationship through a `*const ()` +// receiver, so the providers return `*const TaskStatisticsApi` and the +// handle wrapper re-binds the lifetime to `&self`. +unsafe extern "Rust" { + fn __tt_prod_task_statistics( + ptr: *const (), + ) -> *const crate::task_statistics::TaskStatisticsApi; + fn __tt_test_task_statistics( + ptr: *const (), + ) -> *const crate::task_statistics::TaskStatisticsApi; +} + +impl TurboTasksHandle { + #[inline] + pub fn task_statistics(&self) -> &crate::task_statistics::TaskStatisticsApi { + // SAFETY: the provider returns a pointer to a `TaskStatisticsApi` + // owned by the underlying `TurboTasks` / `VcStorage`, which the + // handle holds alive via its Arc. The returned reference is bound + // to `&self`. + let ptr = match self.tag { + HandleTag::Prod => unsafe { __tt_prod_task_statistics(self.ptr.as_ptr()) }, + HandleTag::Test => unsafe { __tt_test_task_statistics(self.ptr.as_ptr()) }, + }; + unsafe { &*ptr } + } +} + // ===================================================================== // Clone / Drop dispatch — Arc-style refcounting through extern symbols. // ===================================================================== diff --git a/turbopack/crates/turbo-tasks/src/invalidation.rs b/turbopack/crates/turbo-tasks/src/invalidation.rs index 8dc999006016..638eb17bbc79 100644 --- a/turbopack/crates/turbo-tasks/src/invalidation.rs +++ b/turbopack/crates/turbo-tasks/src/invalidation.rs @@ -7,7 +7,7 @@ use turbo_dyn_eq_hash::{ }; use crate::{ - FxIndexMap, FxIndexSet, NonLocalValue, OperationValue, TaskId, TurboTasksApi, + FxIndexMap, FxIndexSet, NonLocalValue, OperationValue, TaskId, manager::{current_task_if_available, mark_invalidator}, trace::TraceRawVcs, util::StaticOrArc, @@ -33,13 +33,13 @@ pub struct Invalidator { } impl Invalidator { - pub fn invalidate(self, turbo_tasks: &dyn TurboTasksApi) { + pub fn invalidate(self, turbo_tasks: &crate::TurboTasksHandle) { turbo_tasks.invalidate(self.task); } pub fn invalidate_with_reason( self, - turbo_tasks: &dyn TurboTasksApi, + turbo_tasks: &crate::TurboTasksHandle, reason: T, ) { turbo_tasks.invalidate_with_reason( diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index c40a19b0d275..5bed75ce0f98 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -28,8 +28,8 @@ use turbo_tasks_hash::{DeterministicHash, hash_xxh3_hash128}; use crate::{ Completion, InvalidationReason, InvalidationReasonSet, OutputContent, ReadCellOptions, - ReadOutputOptions, ResolvedVc, SharedReference, TaskId, TraitMethod, ValueTypeId, Vc, VcRead, - VcValueTrait, VcValueType, + ReadOutputOptions, ResolvedVc, SharedReference, TaskId, TraitMethod, TurboTasksHandle, + ValueTypeId, Vc, VcRead, VcValueTrait, VcValueType, backend::{ Backend, CellContent, CellHash, TaskCollectiblesMap, TaskExecutionSpec, TransientTaskType, TurboTasksExecutionError, TypedCellContent, VerificationMode, @@ -542,8 +542,11 @@ impl CurrentTaskState { // TODO implement our own thread pool and make these thread locals instead task_local! { - /// The current TurboTasks instance - static TURBO_TASKS: Arc; + /// The current TurboTasks instance. A [`TurboTasksHandle`] is a + /// tagged pointer that dispatches `TurboTasksApi` method calls + /// through `extern "Rust"` symbols provided by `turbo-tasks-handle`. + /// See [`crate::handle`] for the dispatch design. + static TURBO_TASKS: TurboTasksHandle; static CURRENT_TASK_STATE: Arc>; @@ -592,6 +595,29 @@ impl TurboTasks { self.this.upgrade().unwrap() } + /// Builds a [`TurboTasksHandle`] that points at this `TurboTasks`. + /// Consumes one strong refcount from the given `Arc`; the handle + /// will drop that refcount when itself dropped. + pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { + let ptr = Arc::into_raw(self) as *mut (); + // Safety: `ptr` came from `Arc::into_raw` on a `TurboTasks`, + // which `turbo-tasks-handle`'s `__tt_prod_*` providers know how to + // cast back to. Tag is consistent with the prod arm by definition. + unsafe { + crate::TurboTasksHandle::from_raw_parts( + crate::HandleTag::Prod, + std::ptr::NonNull::new_unchecked(ptr), + ) + } + } + + /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. + /// Helper that clones the internal `Arc` first; equivalent to + /// `self.pin().make_handle()`. + pub fn make_handle_from_ref(&self) -> crate::TurboTasksHandle { + self.pin().make_handle() + } + /// Creates a new root task pub fn spawn_root_task(&self, functor: F) -> TaskId where @@ -670,7 +696,7 @@ impl TurboTasks { let result = TURBO_TASKS .scope( - self.pin(), + self.make_handle_from_ref(), CURRENT_TASK_STATE.scope(current_task_state, async { let result = CaptureFuture::new(future).await; @@ -902,17 +928,17 @@ impl TurboTasks { id: TaskId, consistency: ReadConsistency, ) -> Result<()> { - read_task_output( - self, - id, - ReadOutputOptions { - // INVALIDATION: This doesn't return a value, only waits for it to be ready. - tracking: ReadTracking::Untracked, - consistency, - }, - ) - .await?; - Ok(()) + let options = ReadOutputOptions { + // INVALIDATION: This doesn't return a value, only waits for it to be ready. + tracking: ReadTracking::Untracked, + consistency, + }; + loop { + match ::try_read_task_output(self, id, options)? { + Ok(_) => return Ok(()), + Err(listener) => listener.await, + } + } } /// Returns [UpdateInfo] with all updates aggregated over a given duration @@ -1012,7 +1038,7 @@ impl TurboTasks { } pub async fn stop_and_wait(&self) { - turbo_tasks_future_scope(self.pin(), async move { + turbo_tasks_future_scope(self.make_handle_from_ref(), async move { self.backend.stopping(self); self.stopped.store(true, Ordering::Release); { @@ -1052,7 +1078,7 @@ impl TurboTasks { self.begin_background_job(); tokio::spawn( TURBO_TASKS - .scope(this.clone(), async move { + .scope(this.clone().make_handle(), async move { if !this.stopped.load(Ordering::Acquire) { this = func(this).await; } @@ -1200,7 +1226,11 @@ impl Executor, ScheduledTask, TaskPriority> for TurboT .await }; - Either::Left(TURBO_TASKS.scope(this2, future).instrument(span)) + Either::Left( + TURBO_TASKS + .scope(this2.make_handle(), future) + .instrument(span), + ) } ScheduledTask::LocalTask { ty, @@ -1267,7 +1297,11 @@ impl Executor, ScheduledTask, TaskPriority> for TurboT }; let future = CURRENT_TASK_STATE.scope(global_task_state, future); - Either::Right(TURBO_TASKS.scope(this2, future).instrument(span)) + Either::Right( + TURBO_TASKS + .scope(this2.make_handle(), future) + .instrument(span), + ) } } } @@ -1641,7 +1675,7 @@ pub(crate) fn debug_assert_not_in_top_level_task(operation: &str) { } pub async fn run( - tt: Arc, + tt: TurboTasksHandle, future: impl Future> + Send + 'static, ) -> Result { let (tx, rx) = tokio::sync::oneshot::channel(); @@ -1658,7 +1692,7 @@ pub async fn run( } pub async fn run_once( - tt: Arc, + tt: TurboTasksHandle, future: impl Future> + Send + 'static, ) -> Result { let (tx, rx) = tokio::sync::oneshot::channel(); @@ -1675,7 +1709,7 @@ pub async fn run_once( } pub async fn run_once_with_reason( - tt: Arc, + tt: TurboTasksHandle, reason: impl InvalidationReason, future: impl Future> + Send + 'static, ) -> Result { @@ -1715,28 +1749,28 @@ pub fn trait_call( with_turbo_tasks(|tt| tt.trait_call(trait_method, this, arg, persistence)) } -pub fn turbo_tasks() -> Arc { - TURBO_TASKS.with(|arc| arc.clone()) +pub fn turbo_tasks() -> TurboTasksHandle { + TURBO_TASKS.with(|h| h.clone()) } -pub fn turbo_tasks_weak() -> Weak { - TURBO_TASKS.with(Arc::downgrade) +pub fn turbo_tasks_weak() -> crate::TurboTasksWeakHandle { + TURBO_TASKS.with(|h| h.downgrade()) } -pub fn try_turbo_tasks() -> Option> { - TURBO_TASKS.try_with(|arc| arc.clone()).ok() +pub fn try_turbo_tasks() -> Option { + TURBO_TASKS.try_with(|h| h.clone()).ok() } -pub fn with_turbo_tasks(func: impl FnOnce(&Arc) -> T) -> T { - TURBO_TASKS.with(|arc| func(arc)) +pub fn with_turbo_tasks(func: impl FnOnce(&TurboTasksHandle) -> T) -> T { + TURBO_TASKS.with(|h| func(h)) } -pub fn turbo_tasks_scope(tt: Arc, f: impl FnOnce() -> T) -> T { +pub fn turbo_tasks_scope(tt: TurboTasksHandle, f: impl FnOnce() -> T) -> T { TURBO_TASKS.sync_scope(tt, f) } pub fn turbo_tasks_future_scope( - tt: Arc, + tt: TurboTasksHandle, f: impl Future, ) -> impl Future { TURBO_TASKS.scope(tt, f) @@ -1850,19 +1884,6 @@ pub fn emit(collectible: ResolvedVc) { }) } -pub(crate) async fn read_task_output( - this: &dyn TurboTasksApi, - id: TaskId, - options: ReadOutputOptions, -) -> Result { - loop { - match this.try_read_task_output(id, options)? { - Ok(result) => return Ok(result), - Err(listener) => listener.await, - } - } -} - /// A reference to a task's cell with methods that allow updating the contents /// of the cell. /// @@ -2179,7 +2200,7 @@ pub fn find_cell_by_id(ty: ValueTypeId) -> CurrentCellRef { } pub(crate) async fn read_local_output( - this: &dyn TurboTasksApi, + this: &TurboTasksHandle, execution_id: ExecutionId, local_task_id: LocalTaskId, ) -> Result { diff --git a/turbopack/crates/turbo-tasks/src/raw_vc.rs b/turbopack/crates/turbo-tasks/src/raw_vc.rs index 397653d8c4b8..17878b6dc58e 100644 --- a/turbopack/crates/turbo-tasks/src/raw_vc.rs +++ b/turbopack/crates/turbo-tasks/src/raw_vc.rs @@ -2,7 +2,6 @@ use std::{ fmt::{Debug, Display}, future::Future, pin::Pin, - sync::Arc, task::{Poll, ready}, }; @@ -13,7 +12,7 @@ use serde::{Deserialize, Serialize}; use crate::{ CollectiblesSource, ReadCellOptions, ReadConsistency, ReadOutputOptions, ResolvedVc, TaskId, - TaskPersistence, TraitTypeId, ValueTypeId, VcValueTrait, + TaskPersistence, TraitTypeId, TurboTasksHandle, ValueTypeId, VcValueTrait, backend::TypedCellContent, event::EventListener, id::{ExecutionId, LocalTaskId}, @@ -152,7 +151,7 @@ impl RawVc { Ok(match self { RawVc::LocalOutput(execution_id, local_task_id, ..) => { let tt = turbo_tasks(); - let local_output = read_local_output(&*tt, execution_id, local_task_id).await?; + let local_output = read_local_output(&tt, execution_id, local_task_id).await?; debug_assert!( !matches!(local_output, RawVc::LocalOutput(_, _, _)), "a LocalOutput cannot point at other LocalOutputs" @@ -349,7 +348,7 @@ impl Future for ResolveRawVcFuture { // SAFETY: we are not moving self let this = unsafe { self.get_unchecked_mut() }; - let poll_fn = |tt: &Arc| -> Poll { + let poll_fn = |tt: &TurboTasksHandle| -> Poll { 'outer: loop { ready!(poll_listener(&mut this.listener, cx)); let listener = match this.current { @@ -486,7 +485,7 @@ impl Future for ReadRawVcFuture { // At this point `this.resolved` is `Some((task, index))`. let (task, index) = this.resolved.unwrap(); - let poll_fn = |tt: &Arc| -> Poll { + let poll_fn = |tt: &TurboTasksHandle| -> Poll { loop { ready!(poll_listener(&mut this.listener, cx)); let listener = match tt.try_read_task_cell(task, index, this.read_cell_options) { diff --git a/turbopack/crates/turbo-tasks/src/scope.rs b/turbopack/crates/turbo-tasks/src/scope.rs index 6fe7efc46120..cd3f6183ac34 100644 --- a/turbopack/crates/turbo-tasks/src/scope.rs +++ b/turbopack/crates/turbo-tasks/src/scope.rs @@ -17,7 +17,7 @@ use parking_lot::{Condvar, Mutex}; use tokio::{runtime::Handle, task::block_in_place}; use tracing::{Span, info_span}; -use crate::{TurboTasksApi, manager::try_turbo_tasks, turbo_tasks_scope}; +use crate::{manager::try_turbo_tasks, turbo_tasks_scope}; /// Number of worker tasks to spawn that process jobs. It's 1 less than the number of cpus as we /// also use the current task as worker. @@ -153,7 +153,7 @@ pub struct Scope<'scope, 'env: 'scope, R: Send + 'env> { index: AtomicUsize, inner: Arc, handle: Handle, - turbo_tasks: Option>, + turbo_tasks: Option, span: Span, /// Invariance over 'env, to make sure 'env cannot shrink, which is necessary for soundness. /// diff --git a/turbopack/crates/turbo-tasks/src/state.rs b/turbopack/crates/turbo-tasks/src/state.rs index 6b2d49b5500c..399ca6013922 100644 --- a/turbopack/crates/turbo-tasks/src/state.rs +++ b/turbopack/crates/turbo-tasks/src/state.rs @@ -85,7 +85,7 @@ fn notify_mutated( let _span = trace_span!("state value changed").entered(); with_turbo_tasks(|tt| { for invalidator in invalidators { - invalidator.invalidate(&**tt); + invalidator.invalidate(tt); } if let Some(serialization_invalidator) = serialization_invalidator { tt.invalidate_serialization(serialization_invalidator.task()); diff --git a/turbopack/crates/turbopack-cli/src/dev/mod.rs b/turbopack/crates/turbopack-cli/src/dev/mod.rs index 5db53d3b8bb7..6f914c1e7314 100644 --- a/turbopack/crates/turbopack-cli/src/dev/mod.rs +++ b/turbopack/crates/turbopack-cli/src/dev/mod.rs @@ -247,7 +247,7 @@ impl TurbopackDevServerBuilder { }; let issue_reporter_arc = Arc::new(move || issue_provider.get_issue_reporter()); - Ok(server.serve(tasks, source, issue_reporter_arc)) + Ok(server.serve(tasks.make_handle(), source, issue_reporter_arc)) } } diff --git a/turbopack/crates/turbopack-dev-server/src/lib.rs b/turbopack/crates/turbopack-dev-server/src/lib.rs index fa9231d652f9..542b6b256186 100644 --- a/turbopack/crates/turbopack-dev-server/src/lib.rs +++ b/turbopack/crates/turbopack-dev-server/src/lib.rs @@ -30,8 +30,8 @@ use socket2::{Domain, Protocol, Socket, Type}; use tokio::task::JoinHandle; use tracing::{Instrument, Level, Span, event, info_span}; use turbo_tasks::{ - Effects, NonLocalValue, OperationVc, PrettyPrintError, TurboTasksApi, Vc, run_once_with_reason, - take_effects, trace::TraceRawVcs, util::FormatDuration, + Effects, NonLocalValue, OperationVc, PrettyPrintError, Vc, run_once_with_reason, take_effects, + trace::TraceRawVcs, util::FormatDuration, }; use turbopack_core::issue::{IssueReporter, IssueSeverity, handle_issues}; @@ -124,7 +124,7 @@ impl DevServer { impl DevServerBuilder { pub fn serve( self, - turbo_tasks: Arc, + turbo_tasks: turbo_tasks::TurboTasksHandle, source_provider: impl SourceProvider + NonLocalValue + TraceRawVcs + Sync, get_issue_reporter: Arc Vc> + Send + Sync>, ) -> DevServer { @@ -195,7 +195,7 @@ impl DevServerBuilder { hyper_tungstenite::upgrade(request, None)?; let update_server = UpdateServer::new(source_provider, issue_reporter); - update_server.run(&*tt, websocket); + update_server.run(&tt, websocket); return Ok(response); } diff --git a/turbopack/crates/turbopack-dev-server/src/update/server.rs b/turbopack/crates/turbopack-dev-server/src/update/server.rs index 60390e28794e..545d25040cc4 100644 --- a/turbopack/crates/turbopack-dev-server/src/update/server.rs +++ b/turbopack/crates/turbopack-dev-server/src/update/server.rs @@ -13,7 +13,7 @@ use tokio::select; use tokio_stream::StreamMap; use tracing::{Level, instrument}; use turbo_tasks::{ - NonLocalValue, OperationVc, PrettyPrintError, ReadRef, TransientInstance, TurboTasksApi, Vc, + NonLocalValue, OperationVc, PrettyPrintError, ReadRef, TransientInstance, Vc, trace::TraceRawVcs, }; use turbo_tasks_fs::json::parse_json_with_source_context; @@ -52,7 +52,7 @@ where } /// Run the update server loop. - pub fn run(self, tt: &dyn TurboTasksApi, ws: HyperWebsocket) { + pub fn run(self, tt: &turbo_tasks::TurboTasksHandle, ws: HyperWebsocket) { tt.start_once_process(Box::pin(async move { if let Err(err) = self.run_internal(ws).await { println!("[UpdateServer]: error {err:#}"); From a60052d30f2616d2a3c3535a64e943e61bdf226f Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Mon, 18 May 2026 11:32:01 -0700 Subject: [PATCH 06/19] Phase 2b: delete turbo-tasks-handle; providers move into -backend/-testing The turbo-tasks-handle crate added in phase 1 created an awkward cycle: it needed turbo-tasks-backend (for ProdHandleConcrete) and turbo-tasks-testing (for VcStorage) as feature-gated deps, which made adding it as a dep of turbo-tasks-backend or turbo-tasks-testing impossible (cycle). The result was per-binary opt-in via dev-deps, which doesn't compose: every test/bench/example crate would need to explicitly enable features on a separate handle crate. This commit collapses turbo-tasks-handle out of existence: * The __tt_prod_* providers move into turbo-tasks-backend/src/ handle_providers.rs. turbo-tasks-backend already owns TurboTasksBackend and the prod handle concrete type. * The __tt_test_* providers move into turbo-tasks-testing/src/ handle_providers.rs. turbo-tasks-testing already owns VcStorage. * The dispatch contract (TurboTasksHandle, HandleTag, forward decls, forwarder macros) stays in turbo-tasks/src/handle.rs. * Two new Cargo features on turbo-tasks: prod_handle, test_handle. Each gates its arm's HandleTag variant, extern decls, and match arms. Single-variant builds (only prod or only test) collapse to a direct call; both-variant builds get cmp + b.ne + direct call. * turbo-tasks-backend deps on turbo-tasks [prod_handle]. turbo-tasks-testing deps on turbo-tasks [test_handle]. * TurboTasks::make_handle is gated behind feature = 'prod_handle'; VcStorage::make_handle is in turbo-tasks-testing so unconditional. Test binaries linking the providers still has a wrinkle: when a crate's test binary unifies test_handle on via dev-deps but the test code doesn't 'use turbo_tasks_testing', cargo doesn't pull libturbo_tasks_testing.rlib into the link command. The fix is 'extern crate turbo_tasks_testing;' in the affected files. So far applied to: - turbo-tasks-backend/src/lib.rs (#[cfg(test)]) - turbo-tasks-backend/tests/eviction.rs - turbo-tasks-backend/benches/mod.rs Still pending: a handful of other workspace crates that build tests without using turbo_tasks_testing directly. Diagnosed but not yet fixed (turbopack-cli, turbopack-tracing, etc.). --- Cargo.lock | 15 - Cargo.toml | 1 - crates/next-napi-bindings/Cargo.toml | 4 - .../crates/turbo-tasks-backend/Cargo.toml | 6 +- .../crates/turbo-tasks-backend/benches/mod.rs | 6 + .../src/handle_providers.rs | 252 +++++++++ .../crates/turbo-tasks-backend/src/lib.rs | 12 + .../turbo-tasks-backend/tests/eviction.rs | 6 + .../crates/turbo-tasks-handle/Cargo.toml | 29 - .../crates/turbo-tasks-handle/src/lib.rs | 505 ------------------ .../crates/turbo-tasks-testing/Cargo.toml | 2 +- turbopack/crates/turbo-tasks/Cargo.toml | 11 + turbopack/crates/turbo-tasks/src/handle.rs | 81 ++- turbopack/crates/turbo-tasks/src/manager.rs | 10 +- 14 files changed, 359 insertions(+), 581 deletions(-) create mode 100644 turbopack/crates/turbo-tasks-backend/src/handle_providers.rs delete mode 100644 turbopack/crates/turbo-tasks-handle/Cargo.toml delete mode 100644 turbopack/crates/turbo-tasks-handle/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3b61259879cd..243dabd82acb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4947,7 +4947,6 @@ dependencies = [ "turbo-tasks", "turbo-tasks-backend", "turbo-tasks-fs", - "turbo-tasks-handle", "turbo-tasks-malloc", "turbo-unix-path", "turbopack-core", @@ -9893,7 +9892,6 @@ dependencies = [ "turbo-persistence", "turbo-rcstr", "turbo-tasks", - "turbo-tasks-handle", "turbo-tasks-hash", "turbo-tasks-malloc", "turbo-tasks-testing", @@ -10018,19 +10016,6 @@ dependencies = [ "turbo-tasks-fs", ] -[[package]] -name = "turbo-tasks-handle" -version = "0.1.0" -dependencies = [ - "anyhow", - "either", - "smallvec", - "tokio", - "turbo-tasks", - "turbo-tasks-backend", - "turbo-tasks-testing", -] - [[package]] name = "turbo-tasks-hash" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 90cd23cbe63d..a1d6ec2fb973 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,7 +178,6 @@ turbo-tasks-bytes = { path = "turbopack/crates/turbo-tasks-bytes" } turbo-tasks-env = { path = "turbopack/crates/turbo-tasks-env" } turbo-tasks-fetch = { path = "turbopack/crates/turbo-tasks-fetch" } turbo-tasks-fs = { path = "turbopack/crates/turbo-tasks-fs" } -turbo-tasks-handle = { path = "turbopack/crates/turbo-tasks-handle", default-features = false } turbo-tasks-hash = { path = "turbopack/crates/turbo-tasks-hash" } turbo-tasks-macros = { path = "turbopack/crates/turbo-tasks-macros" } turbo-tasks-malloc = { path = "turbopack/crates/turbo-tasks-malloc", default-features = false } diff --git a/crates/next-napi-bindings/Cargo.toml b/crates/next-napi-bindings/Cargo.toml index 8a1d37e8ed79..5237da8bd97f 100644 --- a/crates/next-napi-bindings/Cargo.toml +++ b/crates/next-napi-bindings/Cargo.toml @@ -94,10 +94,6 @@ swc_plugin_backend_wasmtime = { workspace = true } tokio = { workspace = true, features = ["full"] } turbo-rcstr = { workspace = true, features = ["napi"] } turbo-tasks = { workspace = true } -# Pulls in the `__tt_prod_*` extern "Rust" providers that satisfy the -# forward declarations in `turbo-tasks::handle`. Without this dep the -# cdylib fails to link. -turbo-tasks-handle = { workspace = true, features = ["prod"] } turbo-tasks-backend = { workspace = true } turbo-tasks-fs = { workspace = true } turbo-unix-path = { workspace = true } diff --git a/turbopack/crates/turbo-tasks-backend/Cargo.toml b/turbopack/crates/turbo-tasks-backend/Cargo.toml index 2b5aaa1a5407..2acbcb1d0874 100644 --- a/turbopack/crates/turbo-tasks-backend/Cargo.toml +++ b/turbopack/crates/turbo-tasks-backend/Cargo.toml @@ -58,7 +58,7 @@ tracing = { workspace = true } turbo-bincode = { workspace = true } turbo-persistence = { workspace = true } turbo-rcstr = { workspace = true } -turbo-tasks = { workspace = true } +turbo-tasks = { workspace = true, features = ["prod_handle"] } turbo-tasks-hash = { workspace = true } turbo-tasks-malloc = { workspace = true, default-features = false } thread_local = { workspace = true } @@ -73,10 +73,6 @@ triomphe = { workspace = true } turbo-tasks-malloc = { workspace = true , features = ["custom_allocator"]} rstest = { workspace = true } turbo-tasks-testing = { workspace = true } -# Tests and benches construct real `TurboTasks` instances; some also -# use `VcStorage` via `turbo-tasks-testing`. Pull in both arms of the -# handle dispatch so the extern "Rust" symbols are linked. -turbo-tasks-handle = { workspace = true, features = ["prod", "test"] } [[bench]] diff --git a/turbopack/crates/turbo-tasks-backend/benches/mod.rs b/turbopack/crates/turbo-tasks-backend/benches/mod.rs index 537e93e49478..dd4fca4940bf 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/mod.rs @@ -1,6 +1,12 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linkage of `__tt_test_*` providers. See similar comment in +// `tests/eviction.rs`. Benches don't use `turbo_tasks_testing` directly +// but the feature-unified `test_handle` decl exists, so the binary +// needs the test-arm providers linked. +extern crate turbo_tasks_testing; + use criterion::{Criterion, criterion_group, criterion_main}; pub(crate) mod overhead; diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs new file mode 100644 index 000000000000..7ac93ae9ecee --- /dev/null +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -0,0 +1,252 @@ +//! `#[no_mangle] pub extern "Rust" fn __tt_prod_*` providers for the +//! production arm of the `TurboTasksHandle` dispatch. +//! +//! The forward declarations live in `turbo_tasks::handle` and are gated by +//! the `prod_handle` Cargo feature on `turbo-tasks`. `turbo-tasks-backend` +//! activates that feature in its dep entry, so these `#[no_mangle]` +//! symbols are linked into any binary that pulls in `turbo-tasks-backend`. +//! +//! Each provider: +//! 1. Casts the opaque `*const ()` receiver back to `&ProdHandleConcrete` (the production handle +//! type — `TurboTasks>`). +//! 2. Calls the trait method on the concrete type. +//! +//! Under thin LTO + `codegen-units = 1`, every step inlines into the +//! caller and the dispatch shape is `match tag => direct call` with no +//! indirect calls. See `turbo_tasks::handle` for the experiment that +//! verified this. + +use std::sync::Arc; + +use either::Either; +use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; + +use crate::{NoopBackingStorage, TurboBackingStorage, TurboTasksBackend}; + +/// The concrete prod handle type — matches what `next-napi-bindings` uses +/// (one of `Either`). +/// +/// If a future binary needs a different `Backend`/storage combination, +/// this provider crate would need a parallel module. Today there is +/// exactly one prod handle type so we hardcode it. +pub type ProdHandleConcrete = + turbo_tasks::TurboTasks>>; + +/// Generates `#[no_mangle] pub extern "Rust" fn __tt_prod_(...)` +/// for a single dispatched method, dispatched via method call syntax. +macro_rules! provide_prod { + ( + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + #[unsafe(no_mangle)] + pub extern "Rust" fn ${concat(__tt_prod_, $name)}( + ptr: *const () + $(, $arg : $ty)* + ) $(-> $ret)? { + let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; + tt.$name($($arg),*) + } + }; +} + +/// Same as `provide_prod!`, but forces UFCS dispatch to a specific +/// trait. Used for methods (`run`, `run_once`, `run_once_with_reason`, +/// `start_once_process`, `stop_and_wait`) where the concrete type has +/// an inherent method with the same name but a different return type — +/// without UFCS, the inherent method wins and the macro fails type +/// checking. +macro_rules! provide_prod_trait { + ( + $trait:path, + fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? + ) => { + #[unsafe(no_mangle)] + pub extern "Rust" fn ${concat(__tt_prod_, $name)}( + ptr: *const () + $(, $arg : $ty)* + ) $(-> $ret)? { + let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; + ::$name(tt $(, $arg)*) + } + }; +} + +// ---- dispatched methods --------------------------------------------------- +// +// Keep this list in sync with the matching `tt_decl_extern!` / +// `tt_decl_handle_method!` invocations in +// `turbopack/crates/turbo-tasks/src/handle.rs`. + +// TurboTasksCallApi +provide_prod!(fn dynamic_call( + native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, + this: Option, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, +) -> turbo_tasks::RawVc); +provide_prod!(fn native_call( + native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, + this: Option, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, +) -> turbo_tasks::RawVc); +provide_prod!(fn trait_call( + trait_method: &'static turbo_tasks::TraitMethod, + this: turbo_tasks::RawVc, + arg: &mut dyn turbo_tasks::StackDynTaskInputs, + persistence: turbo_tasks::TaskPersistence, +) -> turbo_tasks::RawVc); +provide_prod!(fn send_compilation_event( + event: ::std::sync::Arc, +)); +provide_prod!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); + +provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); +provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once( + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); +provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( + reason: turbo_tasks::util::StaticOrArc, + future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, +) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); +provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn start_once_process( + future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, +)); +provide_prod_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); + +// TurboTasksApi +provide_prod!(fn invalidate(task: turbo_tasks::TaskId)); +provide_prod!(fn invalidate_with_reason( + task: turbo_tasks::TaskId, + reason: turbo_tasks::util::StaticOrArc, +)); +provide_prod!(fn invalidate_serialization(task: turbo_tasks::TaskId)); +provide_prod!(fn try_read_task_output( + task: turbo_tasks::TaskId, + options: turbo_tasks::ReadOutputOptions, +) -> ::anyhow::Result<::core::result::Result>); +provide_prod!(fn try_read_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + options: turbo_tasks::ReadCellOptions, +) -> ::anyhow::Result<::core::result::Result>); +provide_prod!(fn try_read_local_output( + execution_id: turbo_tasks::ExecutionId, + local_task_id: turbo_tasks::LocalTaskId, +) -> ::anyhow::Result<::core::result::Result>); +provide_prod!(fn read_task_collectibles( + task: turbo_tasks::TaskId, + trait_id: turbo_tasks::TraitTypeId, +) -> turbo_tasks::backend::TaskCollectiblesMap); +provide_prod!(fn emit_collectible( + trait_type: turbo_tasks::TraitTypeId, + collectible: turbo_tasks::RawVc, +)); +provide_prod!(fn unemit_collectible( + trait_type: turbo_tasks::TraitTypeId, + collectible: turbo_tasks::RawVc, + count: u32, +)); +provide_prod!(fn unemit_collectibles( + trait_type: turbo_tasks::TraitTypeId, + collectibles: &turbo_tasks::backend::TaskCollectiblesMap, +)); +provide_prod!(fn try_read_own_task_cell( + current_task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, +) -> ::anyhow::Result); +provide_prod!(fn read_own_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, +) -> ::anyhow::Result); +provide_prod!(fn update_own_task_cell( + task: turbo_tasks::TaskId, + index: turbo_tasks::CellId, + content: turbo_tasks::backend::CellContent, + updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, + content_hash: ::core::option::Option, + verification_mode: turbo_tasks::backend::VerificationMode, +)); +provide_prod!(fn mark_own_task_as_finished(task: turbo_tasks::TaskId)); +provide_prod!(fn connect_task(task: turbo_tasks::TaskId)); +provide_prod!(fn spawn_detached_for_testing( + f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, +)); +provide_prod!(fn subscribe_to_compilation_events( + event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, +) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); +provide_prod!(fn is_tracking_dependencies() -> bool); + +// `task_statistics` is special: the trait method returns +// `&TaskStatisticsApi` borrowed from `&self`, but extern "Rust" can't +// carry that lifetime through a `*const ()` receiver. The provider +// returns a raw pointer; the handle wrapper in `turbo-tasks` re-binds +// the lifetime to `&self`. This is sound because the underlying Arc +// (held by the handle) keeps the `TaskStatisticsApi` alive. +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_task_statistics( + ptr: *const (), +) -> *const turbo_tasks::task_statistics::TaskStatisticsApi { + let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; + tt.task_statistics() as *const _ +} + +// ---- Arc clone / drop ----------------------------------------------------- + +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_clone_arc(ptr: *const ()) { + // Bump the refcount of the Arc whose data pointer is `ptr`. The caller + // (`::clone`) is responsible for reusing + // the same `ptr` value in the new handle, so we don't need to return + // anything. + unsafe { Arc::::increment_strong_count(ptr as *const ProdHandleConcrete) } +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_drop_arc(ptr: *const ()) { + // Decrement the refcount; runs the destructor when it reaches zero. + unsafe { Arc::::decrement_strong_count(ptr as *const ProdHandleConcrete) } +} + +// ---- Weak refcount providers --------------------------------------------- + +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const () { + // Reconstitute the Arc transiently to call `downgrade`, then leak the + // Arc back so its refcount is unchanged. The Weak we produce owns its + // own weak refcount. + let arc = unsafe { Arc::from_raw(arc_ptr as *const ProdHandleConcrete) }; + let weak = Arc::downgrade(&arc); + ::std::mem::forget(arc); + ::std::sync::Weak::into_raw(weak) as *const () +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const () { + // Reconstitute the Weak transiently to attempt upgrade, then leak it + // back so its refcount is unchanged. + let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; + let maybe_arc = weak.upgrade(); + ::std::mem::forget(weak); + match maybe_arc { + Some(arc) => Arc::into_raw(arc) as *const (), + None => ::std::ptr::null(), + } +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_clone_weak(weak_ptr: *const ()) { + // `Weak` has no `increment_weak_count` API, so we round-trip through + // `Weak::clone` and leak both copies. + let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; + let cloned = weak.clone(); + ::std::mem::forget(weak); + ::std::mem::forget(cloned); +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn __tt_prod_drop_weak(weak_ptr: *const ()) { + drop(unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }); +} diff --git a/turbopack/crates/turbo-tasks-backend/src/lib.rs b/turbopack/crates/turbo-tasks-backend/src/lib.rs index abd462881e07..7d21351f597d 100644 --- a/turbopack/crates/turbo-tasks-backend/src/lib.rs +++ b/turbopack/crates/turbo-tasks-backend/src/lib.rs @@ -1,11 +1,23 @@ #![feature(anonymous_lifetime_in_impl_trait)] #![feature(box_patterns)] +#![feature(macro_metavar_expr_concat)] + +// Force the linker to pull in `libturbo_tasks_testing.rlib` for the lib +// test binary, so the `__tt_test_*` extern "Rust" providers (which the +// lib references transitively through the dispatch in `turbo-tasks` — +// `test_handle` is feature-unified-on whenever a dev-dep activates it) +// are resolved at link time. Without this, cargo doesn't include the +// `turbo-tasks-testing` rlib in the test binary's link command because +// no Rust code references it by name. +#[cfg(test)] +extern crate turbo_tasks_testing; mod backend; mod backing_storage; mod data; mod database; mod error; +mod handle_providers; mod kv_backing_storage; mod utils; diff --git a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs index cd8e74bbf0b9..163c8cfd7805 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs @@ -2,6 +2,12 @@ #![feature(arbitrary_self_types_pointers)] #![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this +// Force linkage of `__tt_test_*` providers. This test doesn't `use` +// anything from `turbo_tasks_testing`, but the feature unification of +// `test_handle` on `turbo-tasks` (active because dev-deps bring it in +// through other means) makes the lib reference `__tt_test_*` symbols. +extern crate turbo_tasks_testing; + use std::sync::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, diff --git a/turbopack/crates/turbo-tasks-handle/Cargo.toml b/turbopack/crates/turbo-tasks-handle/Cargo.toml deleted file mode 100644 index 9643ece904b6..000000000000 --- a/turbopack/crates/turbo-tasks-handle/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "turbo-tasks-handle" -version = "0.1.0" -description = "Concrete dispatch providers for the turbo-tasks task-local. Implements `extern \"Rust\"` symbols whose forward declarations live in `turbo-tasks`, allowing thin-LTO devirtualization of the prod and test handles." -license = "MIT" -edition = "2024" - -[lib] -bench = false - -[lints] -workspace = true - -[features] -# Auto-activated by binaries / test crates that need the corresponding arm. -# - `prod`: links `turbo-tasks-backend` and emits `__tt_prod_*` providers. -# - `test`: links `turbo-tasks-testing` and emits `__tt_test_*` providers. -prod = ["dep:turbo-tasks-backend", "dep:either"] -test = ["dep:turbo-tasks-testing"] - -[dependencies] -anyhow = { workspace = true } -smallvec = { workspace = true } -tokio = { workspace = true, features = ["sync"] } -turbo-tasks = { workspace = true } - -either = { workspace = true, optional = true } -turbo-tasks-backend = { workspace = true, optional = true } -turbo-tasks-testing = { workspace = true, optional = true } diff --git a/turbopack/crates/turbo-tasks-handle/src/lib.rs b/turbopack/crates/turbo-tasks-handle/src/lib.rs deleted file mode 100644 index ca9b676da458..000000000000 --- a/turbopack/crates/turbo-tasks-handle/src/lib.rs +++ /dev/null @@ -1,505 +0,0 @@ -//! See the crate-level docs in `Cargo.toml`. -//! -//! This crate is intentionally tiny: it only emits `#[no_mangle] pub extern -//! "Rust" fn` provider symbols that match the forward declarations in -//! [`turbo_tasks::handle`]. Consumers should depend on this crate with the -//! `prod` and/or `test` features enabled to pull in the corresponding arm. -//! -//! Building this crate with neither feature compiles to an empty lib that -//! exports no symbols. This is intentional so that `cargo check --workspace` -//! works without explicit feature flags; binaries and test harnesses that -//! actually consume the handle dispatch must enable the appropriate feature(s). - -#![feature(macro_metavar_expr_concat)] - -use std::sync::Arc; - -use turbo_tasks::{HandleTag, TurboTasksHandle}; - -// ===================================================================== -// Prod arm -// ===================================================================== - -/// The concrete prod handle type — matches what `next-napi-bindings` uses. -/// -/// If a future binary needs a different `Backend`/storage combination, -/// this crate's prod arm would need to be re-parameterized (or a parallel -/// crate added). Today there is exactly one prod handle type so we -/// hardcode it. -#[cfg(feature = "prod")] -pub type ProdHandleConcrete = turbo_tasks::TurboTasks< - turbo_tasks_backend::TurboTasksBackend< - either::Either< - turbo_tasks_backend::TurboBackingStorage, - turbo_tasks_backend::NoopBackingStorage, - >, - >, ->; - -#[cfg(feature = "prod")] -mod prod { - use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; - - use super::*; - - /// Generates `#[no_mangle] pub extern "Rust" fn __tt_prod_(...)` - /// for a single dispatched method, dispatched via method call syntax. - /// This resolves to whichever trait method or inherent method has the - /// matching name; for methods that don't have a colliding inherent - /// method on the concrete type, this is fine. - macro_rules! provide_prod { - ( - fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? - ) => { - #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_prod_, $name)}( - ptr: *const () - $(, $arg : $ty)* - ) $(-> $ret)? { - let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; - tt.$name($($arg),*) - } - }; - } - - /// Same as `provide_prod!`, but forces UFCS dispatch to a specific - /// trait. Used for methods (`run`, `run_once`, `run_once_with_reason`, - /// `start_once_process`, `stop_and_wait`) where the concrete type has - /// an inherent method with the same name but a different return type - /// — without UFCS, the inherent method wins and the macro fails type - /// checking. - macro_rules! provide_prod_trait { - ( - $trait:path, - fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? - ) => { - #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_prod_, $name)}( - ptr: *const () - $(, $arg : $ty)* - ) $(-> $ret)? { - let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; - ::$name(tt $(, $arg)*) - } - }; - } - - // ---- dispatched methods ---------------------------------------------- - // - // Keep this list in sync with the matching `tt_decl_extern!` / - // `tt_decl_handle_method!` invocations in - // `turbopack/crates/turbo-tasks/src/handle.rs`. A future cleanup could - // generate both from a shared callback macro, but for now duplicating - // the list is the simplest source-of-truth. - - // TurboTasksCallApi - provide_prod!(fn dynamic_call( - native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, - this: Option, - arg: &mut dyn turbo_tasks::StackDynTaskInputs, - persistence: turbo_tasks::TaskPersistence, - ) -> turbo_tasks::RawVc); - provide_prod!(fn native_call( - native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, - this: Option, - arg: &mut dyn turbo_tasks::StackDynTaskInputs, - persistence: turbo_tasks::TaskPersistence, - ) -> turbo_tasks::RawVc); - provide_prod!(fn trait_call( - trait_method: &'static turbo_tasks::TraitMethod, - this: turbo_tasks::RawVc, - arg: &mut dyn turbo_tasks::StackDynTaskInputs, - persistence: turbo_tasks::TaskPersistence, - ) -> turbo_tasks::RawVc); - provide_prod!(fn send_compilation_event( - event: ::std::sync::Arc, - )); - provide_prod!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); - - provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( - reason: turbo_tasks::util::StaticOrArc, - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn start_once_process( - future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, - )); - provide_prod_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); - - // TurboTasksApi - provide_prod!(fn invalidate(task: turbo_tasks::TaskId)); - provide_prod!(fn invalidate_with_reason( - task: turbo_tasks::TaskId, - reason: turbo_tasks::util::StaticOrArc, - )); - provide_prod!(fn invalidate_serialization(task: turbo_tasks::TaskId)); - provide_prod!(fn try_read_task_output( - task: turbo_tasks::TaskId, - options: turbo_tasks::ReadOutputOptions, - ) -> ::anyhow::Result<::core::result::Result>); - provide_prod!(fn try_read_task_cell( - task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - options: turbo_tasks::ReadCellOptions, - ) -> ::anyhow::Result<::core::result::Result>); - provide_prod!(fn try_read_local_output( - execution_id: turbo_tasks::ExecutionId, - local_task_id: turbo_tasks::LocalTaskId, - ) -> ::anyhow::Result<::core::result::Result>); - provide_prod!(fn read_task_collectibles( - task: turbo_tasks::TaskId, - trait_id: turbo_tasks::TraitTypeId, - ) -> turbo_tasks::backend::TaskCollectiblesMap); - provide_prod!(fn emit_collectible( - trait_type: turbo_tasks::TraitTypeId, - collectible: turbo_tasks::RawVc, - )); - provide_prod!(fn unemit_collectible( - trait_type: turbo_tasks::TraitTypeId, - collectible: turbo_tasks::RawVc, - count: u32, - )); - provide_prod!(fn unemit_collectibles( - trait_type: turbo_tasks::TraitTypeId, - collectibles: &turbo_tasks::backend::TaskCollectiblesMap, - )); - provide_prod!(fn try_read_own_task_cell( - current_task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - ) -> ::anyhow::Result); - provide_prod!(fn read_own_task_cell( - task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - ) -> ::anyhow::Result); - provide_prod!(fn update_own_task_cell( - task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - content: turbo_tasks::backend::CellContent, - updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, - content_hash: ::core::option::Option, - verification_mode: turbo_tasks::backend::VerificationMode, - )); - provide_prod!(fn mark_own_task_as_finished(task: turbo_tasks::TaskId)); - provide_prod!(fn connect_task(task: turbo_tasks::TaskId)); - provide_prod!(fn spawn_detached_for_testing( - f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, - )); - provide_prod!(fn subscribe_to_compilation_events( - event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, - ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); - provide_prod!(fn is_tracking_dependencies() -> bool); - - // `task_statistics` is special: the trait method returns - // `&TaskStatisticsApi` borrowed from `&self`, but extern "Rust" can't - // carry that lifetime through a `*const ()` receiver. The provider - // returns a raw pointer; the handle wrapper in `turbo-tasks` re-binds - // the lifetime to `&self`. This is sound because the underlying Arc - // (held by the handle) keeps the `TaskStatisticsApi` alive. - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_task_statistics( - ptr: *const (), - ) -> *const turbo_tasks::task_statistics::TaskStatisticsApi { - let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; - tt.task_statistics() as *const _ - } - - // ---- Arc clone / drop ------------------------------------------------- - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_clone_arc(ptr: *const ()) { - // Bump the refcount of the Arc whose data pointer is `ptr`. The - // caller (`::clone`) is responsible for - // reusing the same `ptr` value in the new handle, so we don't need - // to return anything. - unsafe { - Arc::::increment_strong_count(ptr as *const ProdHandleConcrete) - } - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_drop_arc(ptr: *const ()) { - // Decrement the refcount; runs the destructor when it reaches zero. - unsafe { - Arc::::decrement_strong_count(ptr as *const ProdHandleConcrete) - } - } - - // ---- Weak refcount providers ----------------------------------------- - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const () { - // Reconstitute the Arc transiently to call `downgrade`, then leak - // the Arc back so its refcount is unchanged. The Weak we produce - // owns its own weak refcount. - let arc = unsafe { Arc::from_raw(arc_ptr as *const ProdHandleConcrete) }; - let weak = Arc::downgrade(&arc); - ::std::mem::forget(arc); - ::std::sync::Weak::into_raw(weak) as *const () - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const () { - // Reconstitute the Weak transiently to attempt upgrade, then leak - // it back so its refcount is unchanged. - let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; - let maybe_arc = weak.upgrade(); - ::std::mem::forget(weak); - match maybe_arc { - Some(arc) => Arc::into_raw(arc) as *const (), - None => ::std::ptr::null(), - } - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_clone_weak(weak_ptr: *const ()) { - // `Weak` has no `increment_weak_count` API, so we round-trip - // through `Weak::clone` and leak both copies. - let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; - let cloned = weak.clone(); - ::std::mem::forget(weak); - ::std::mem::forget(cloned); - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_prod_drop_weak(weak_ptr: *const ()) { - drop(unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }); - } - - /// Constructs a `TurboTasksHandle` pointing at the given prod Arc. - /// - /// The caller transfers ownership of one refcount into the handle. - pub fn from_prod(arc: Arc) -> TurboTasksHandle { - let ptr = Arc::into_raw(arc) as *mut (); - // Safety: ptr came from Arc::into_raw on a ProdHandleConcrete and - // the tag is consistent with that type. - unsafe { - TurboTasksHandle::from_raw_parts(HandleTag::Prod, std::ptr::NonNull::new_unchecked(ptr)) - } - } -} - -#[cfg(feature = "prod")] -pub use prod::from_prod; - -// ===================================================================== -// Test arm -// ===================================================================== - -#[cfg(feature = "test")] -mod test_arm { - use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; - - use super::*; - - pub type TestHandleConcrete = turbo_tasks_testing::VcStorage; - - macro_rules! provide_test { - ( - fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? - ) => { - #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_test_, $name)}( - ptr: *const () - $(, $arg : $ty)* - ) $(-> $ret)? { - let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; - tt.$name($($arg),*) - } - }; - } - - /// Mirrors `provide_prod_trait!` — see its docs. - macro_rules! provide_test_trait { - ( - $trait:path, - fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? - ) => { - #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_test_, $name)}( - ptr: *const () - $(, $arg : $ty)* - ) $(-> $ret)? { - let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; - ::$name(tt $(, $arg)*) - } - }; - } - - // ---- dispatched methods ---------------------------------------------- - // (Mirror of the `prod` block — keep in sync.) - - // TurboTasksCallApi - provide_test!(fn dynamic_call( - native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, - this: Option, - arg: &mut dyn turbo_tasks::StackDynTaskInputs, - persistence: turbo_tasks::TaskPersistence, - ) -> turbo_tasks::RawVc); - provide_test!(fn native_call( - native_fn: &'static turbo_tasks::macro_helpers::NativeFunction, - this: Option, - arg: &mut dyn turbo_tasks::StackDynTaskInputs, - persistence: turbo_tasks::TaskPersistence, - ) -> turbo_tasks::RawVc); - provide_test!(fn trait_call( - trait_method: &'static turbo_tasks::TraitMethod, - this: turbo_tasks::RawVc, - arg: &mut dyn turbo_tasks::StackDynTaskInputs, - persistence: turbo_tasks::TaskPersistence, - ) -> turbo_tasks::RawVc); - provide_test!(fn send_compilation_event( - event: ::std::sync::Arc, - )); - provide_test!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); - - provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn run( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn run_once( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( - reason: turbo_tasks::util::StaticOrArc, - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, - ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - provide_test_trait!(turbo_tasks::TurboTasksCallApi, fn start_once_process( - future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, - )); - provide_test_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); - - // TurboTasksApi - provide_test!(fn invalidate(task: turbo_tasks::TaskId)); - provide_test!(fn invalidate_with_reason( - task: turbo_tasks::TaskId, - reason: turbo_tasks::util::StaticOrArc, - )); - provide_test!(fn invalidate_serialization(task: turbo_tasks::TaskId)); - provide_test!(fn try_read_task_output( - task: turbo_tasks::TaskId, - options: turbo_tasks::ReadOutputOptions, - ) -> ::anyhow::Result<::core::result::Result>); - provide_test!(fn try_read_task_cell( - task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - options: turbo_tasks::ReadCellOptions, - ) -> ::anyhow::Result<::core::result::Result>); - provide_test!(fn try_read_local_output( - execution_id: turbo_tasks::ExecutionId, - local_task_id: turbo_tasks::LocalTaskId, - ) -> ::anyhow::Result<::core::result::Result>); - provide_test!(fn read_task_collectibles( - task: turbo_tasks::TaskId, - trait_id: turbo_tasks::TraitTypeId, - ) -> turbo_tasks::backend::TaskCollectiblesMap); - provide_test!(fn emit_collectible( - trait_type: turbo_tasks::TraitTypeId, - collectible: turbo_tasks::RawVc, - )); - provide_test!(fn unemit_collectible( - trait_type: turbo_tasks::TraitTypeId, - collectible: turbo_tasks::RawVc, - count: u32, - )); - provide_test!(fn unemit_collectibles( - trait_type: turbo_tasks::TraitTypeId, - collectibles: &turbo_tasks::backend::TaskCollectiblesMap, - )); - provide_test!(fn try_read_own_task_cell( - current_task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - ) -> ::anyhow::Result); - provide_test!(fn read_own_task_cell( - task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - ) -> ::anyhow::Result); - provide_test!(fn update_own_task_cell( - task: turbo_tasks::TaskId, - index: turbo_tasks::CellId, - content: turbo_tasks::backend::CellContent, - updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, - content_hash: ::core::option::Option, - verification_mode: turbo_tasks::backend::VerificationMode, - )); - provide_test!(fn mark_own_task_as_finished(task: turbo_tasks::TaskId)); - provide_test!(fn connect_task(task: turbo_tasks::TaskId)); - provide_test!(fn spawn_detached_for_testing( - f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, - )); - provide_test!(fn subscribe_to_compilation_events( - event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, - ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); - provide_test!(fn is_tracking_dependencies() -> bool); - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_task_statistics( - ptr: *const (), - ) -> *const turbo_tasks::task_statistics::TaskStatisticsApi { - let tt: &TestHandleConcrete = unsafe { &*(ptr as *const TestHandleConcrete) }; - tt.task_statistics() as *const _ - } - - // ---- Arc clone / drop ------------------------------------------------- - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_clone_arc(ptr: *const ()) { - unsafe { - Arc::::increment_strong_count(ptr as *const TestHandleConcrete) - } - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_drop_arc(ptr: *const ()) { - unsafe { - Arc::::decrement_strong_count(ptr as *const TestHandleConcrete) - } - } - - // ---- Weak refcount providers ----------------------------------------- - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_downgrade(arc_ptr: *const ()) -> *const () { - let arc = unsafe { Arc::from_raw(arc_ptr as *const TestHandleConcrete) }; - let weak = Arc::downgrade(&arc); - ::std::mem::forget(arc); - ::std::sync::Weak::into_raw(weak) as *const () - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_upgrade(weak_ptr: *const ()) -> *const () { - let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const TestHandleConcrete) }; - let maybe_arc = weak.upgrade(); - ::std::mem::forget(weak); - match maybe_arc { - Some(arc) => Arc::into_raw(arc) as *const (), - None => ::std::ptr::null(), - } - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_clone_weak(weak_ptr: *const ()) { - let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const TestHandleConcrete) }; - let cloned = weak.clone(); - ::std::mem::forget(weak); - ::std::mem::forget(cloned); - } - - #[unsafe(no_mangle)] - pub extern "Rust" fn __tt_test_drop_weak(weak_ptr: *const ()) { - drop(unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const TestHandleConcrete) }); - } - - pub fn from_test(arc: Arc) -> TurboTasksHandle { - let ptr = Arc::into_raw(arc) as *mut (); - unsafe { - TurboTasksHandle::from_raw_parts(HandleTag::Test, std::ptr::NonNull::new_unchecked(ptr)) - } - } -} - -#[cfg(feature = "test")] -pub use test_arm::from_test; diff --git a/turbopack/crates/turbo-tasks-testing/Cargo.toml b/turbopack/crates/turbo-tasks-testing/Cargo.toml index 7571896f4576..5fa0581db572 100644 --- a/turbopack/crates/turbo-tasks-testing/Cargo.toml +++ b/turbopack/crates/turbo-tasks-testing/Cargo.toml @@ -19,5 +19,5 @@ futures = { workspace = true } rustc-hash = { workspace = true } smallvec = { workspace = true } tokio = { workspace = true } -turbo-tasks = { workspace = true, features = ["non_operation_vc_strongly_consistent"] } +turbo-tasks = { workspace = true, features = ["non_operation_vc_strongly_consistent", "test_handle"] } turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbo-tasks/Cargo.toml b/turbopack/crates/turbo-tasks/Cargo.toml index 6f1733067135..9bdcf73a1067 100644 --- a/turbopack/crates/turbo-tasks/Cargo.toml +++ b/turbopack/crates/turbo-tasks/Cargo.toml @@ -15,6 +15,17 @@ hanging_detection = [] task_id_details = [] verify_determinism = [] +# Handle dispatch arms. These are activated by feature unification: +# turbo-tasks-backend deps on turbo-tasks [prod_handle] +# turbo-tasks-testing deps on turbo-tasks [test_handle] +# Anything that transitively links one or both of those crates gets the +# corresponding HandleTag variants, extern "Rust" forward decls, and +# `match` arms in TurboTasksHandle's forwarder methods. +# Pure-prod binaries (the napi binding) get a one-arm enum so LLVM +# collapses the match in each forwarder to a direct call. +prod_handle = [] +test_handle = [] + # TODO(bgw): This feature is here to unblock turning on local tasks by default. It's only turned on # in unit tests. This will be removed very soon. non_operation_vc_strongly_consistent = [] diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 7a30308d9d87..0f1e6e375598 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -31,35 +31,42 @@ //! two-arm enum it compiles to `cmp + b.ne + direct call` and LLVM can //! sometimes fuse the arms further. //! -//! Providers (the `__tt__` symbols) live in the -//! `turbo-tasks-handle` crate. That crate depends on both -//! `turbo-tasks-backend` (for the prod arm) and `turbo-tasks-testing` -//! (for the test arm); each arm is gated by a Cargo feature. By centralising -//! the providers in one crate, we have exactly one place that knows about -//! the dispatch contract. +//! ## Feature gating +//! +//! Two Cargo features on `turbo-tasks` control which dispatch arms exist: +//! +//! - `prod_handle` — activated by `turbo-tasks-backend` (which owns the concrete production handle +//! type and emits the `__tt_prod_*` providers). Pulled in transitively by anything that links the +//! backend, including the napi binding. +//! - `test_handle` — activated by `turbo-tasks-testing` (which owns `VcStorage` and emits the +//! `__tt_test_*` providers). +//! +//! A pure-prod binary (e.g. the napi binding) sees `HandleTag` with only +//! the `Prod` variant and the `match` in each forwarder collapses to a +//! direct call. A workspace test build sees both variants and gets the +//! two-arm match. Features unify across the dep graph; consumers do not +//! enable the features directly. use std::ptr::NonNull; -// Re-export the macro for use by `turbo-tasks-handle`'s provider macro, -// which needs to iterate over the same method list. -// -// TODO: when we add `turbo_tasks_weak()` to the dispatch surface, also -// generate `__tt__downgrade` / `upgrade` and a `TurboTasksWeakHandle` -// type. For now `turbo_tasks_weak` keeps returning `Weak` -// because its only consumer (`turbo_tasks_future_scope`) is not on a hot path. - /// Identifier for which concrete implementation a [`TurboTasksHandle`] points /// at. Used as the tag in the tagged-pointer dispatch. /// -/// New variants must coordinate with `turbo-tasks-handle`'s provider -/// emission and with every caller's `match` against this tag. +/// Variants are feature-gated. A pure-prod build (only `prod_handle` +/// active) has only the `Prod` variant, which makes every dispatch +/// `match` a one-arm collapse — LLVM emits a direct call to the +/// `__tt_prod_*` symbol with no comparison. The two-arm case (both +/// features active, e.g. workspace tests) compiles to `cmp + b.ne + +/// direct call`. #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum HandleTag { /// `TurboTasks>` — the production handle used by /// `next-napi-bindings`, benches, etc. + #[cfg(feature = "prod_handle")] Prod = 0, /// `VcStorage` — the test-only handle used by `turbo-tasks-testing`. + #[cfg(feature = "test_handle")] Test = 1, } @@ -127,21 +134,27 @@ impl TurboTasksHandle { // time. Thin LTO inlines them. // ===================================================================== -/// Generates an `unsafe extern "Rust" { fn __tt_prod_(...); fn -/// __tt_test_(...); }` declaration pair for one dispatched method. +/// Generates feature-gated `unsafe extern "Rust" { fn __tt__; }` +/// declarations. With only `prod_handle` active, only the prod decl is +/// emitted; with only `test_handle`, only the test decl. With both, both. macro_rules! tt_decl_extern { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { unsafe extern "Rust" { + #[cfg(feature = "prod_handle")] fn ${concat(__tt_prod_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; + #[cfg(feature = "test_handle")] fn ${concat(__tt_test_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; } }; } /// Generates an inherent method on `TurboTasksHandle` that dispatches over -/// `match self.tag` to the corresponding `extern "Rust"` symbol. +/// `match self.tag` to the corresponding `extern "Rust"` symbol. The match +/// arms are feature-gated alongside the [`HandleTag`] variants and the +/// extern decls, so single-variant builds get a one-arm match that LLVM +/// collapses to a direct call. macro_rules! tt_decl_handle_method { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? @@ -150,9 +163,11 @@ macro_rules! tt_decl_handle_method { #[inline] pub fn $name(&self $(, $arg : $ty)*) $(-> $ret)? { match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { ${concat(__tt_prod_, $name)}(self.ptr.as_ptr() $(, $arg)*) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { ${concat(__tt_test_, $name)}(self.ptr.as_ptr() $(, $arg)*) }, @@ -410,9 +425,11 @@ tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); // receiver, so the providers return `*const TaskStatisticsApi` and the // handle wrapper re-binds the lifetime to `&self`. unsafe extern "Rust" { + #[cfg(feature = "prod_handle")] fn __tt_prod_task_statistics( ptr: *const (), ) -> *const crate::task_statistics::TaskStatisticsApi; + #[cfg(feature = "test_handle")] fn __tt_test_task_statistics( ptr: *const (), ) -> *const crate::task_statistics::TaskStatisticsApi; @@ -426,7 +443,9 @@ impl TurboTasksHandle { // handle holds alive via its Arc. The returned reference is bound // to `&self`. let ptr = match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_task_statistics(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_task_statistics(self.ptr.as_ptr()) }, }; unsafe { &*ptr } @@ -438,9 +457,13 @@ impl TurboTasksHandle { // ===================================================================== unsafe extern "Rust" { + #[cfg(feature = "prod_handle")] fn __tt_prod_clone_arc(ptr: *const ()); + #[cfg(feature = "prod_handle")] fn __tt_prod_drop_arc(ptr: *const ()); + #[cfg(feature = "test_handle")] fn __tt_test_clone_arc(ptr: *const ()); + #[cfg(feature = "test_handle")] fn __tt_test_drop_arc(ptr: *const ()); // Weak-handle support. Each arm provides: @@ -450,13 +473,21 @@ unsafe extern "Rust" { // Arc is gone; otherwise transfers one strong refcount). // clone_weak: bumps the weak refcount. // drop_weak : drops the weak refcount. + #[cfg(feature = "prod_handle")] fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const (); + #[cfg(feature = "prod_handle")] fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const (); + #[cfg(feature = "prod_handle")] fn __tt_prod_clone_weak(weak_ptr: *const ()); + #[cfg(feature = "prod_handle")] fn __tt_prod_drop_weak(weak_ptr: *const ()); + #[cfg(feature = "test_handle")] fn __tt_test_downgrade(arc_ptr: *const ()) -> *const (); + #[cfg(feature = "test_handle")] fn __tt_test_upgrade(weak_ptr: *const ()) -> *const (); + #[cfg(feature = "test_handle")] fn __tt_test_clone_weak(weak_ptr: *const ()); + #[cfg(feature = "test_handle")] fn __tt_test_drop_weak(weak_ptr: *const ()); } @@ -464,7 +495,9 @@ impl Clone for TurboTasksHandle { #[inline] fn clone(&self) -> Self { match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_clone_arc(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_clone_arc(self.ptr.as_ptr()) }, } Self { @@ -478,7 +511,9 @@ impl Drop for TurboTasksHandle { #[inline] fn drop(&mut self) { match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_drop_arc(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_drop_arc(self.ptr.as_ptr()) }, } } @@ -489,7 +524,9 @@ impl TurboTasksHandle { #[inline] pub fn downgrade(&self) -> TurboTasksWeakHandle { let weak_ptr = match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_downgrade(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_downgrade(self.ptr.as_ptr()) }, }; TurboTasksWeakHandle { @@ -534,7 +571,9 @@ impl TurboTasksWeakHandle { #[inline] pub fn upgrade(&self) -> Option { let strong_ptr = match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_upgrade(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_upgrade(self.ptr.as_ptr()) }, }; let strong_ptr = NonNull::new(strong_ptr as *mut ())?; @@ -549,7 +588,9 @@ impl Clone for TurboTasksWeakHandle { #[inline] fn clone(&self) -> Self { match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_clone_weak(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_clone_weak(self.ptr.as_ptr()) }, } Self { @@ -563,7 +604,9 @@ impl Drop for TurboTasksWeakHandle { #[inline] fn drop(&mut self) { match self.tag { + #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_drop_weak(self.ptr.as_ptr()) }, + #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_drop_weak(self.ptr.as_ptr()) }, } } diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 5bed75ce0f98..89a3b08ea3ec 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -598,11 +598,16 @@ impl TurboTasks { /// Builds a [`TurboTasksHandle`] that points at this `TurboTasks`. /// Consumes one strong refcount from the given `Arc`; the handle /// will drop that refcount when itself dropped. + /// + /// Only available when the `prod_handle` feature is enabled (i.e. when + /// `turbo-tasks-backend` is in the dep graph). + #[cfg(feature = "prod_handle")] pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { let ptr = Arc::into_raw(self) as *mut (); // Safety: `ptr` came from `Arc::into_raw` on a `TurboTasks`, - // which `turbo-tasks-handle`'s `__tt_prod_*` providers know how to - // cast back to. Tag is consistent with the prod arm by definition. + // which the `__tt_prod_*` providers (in `turbo-tasks-backend`) + // know how to cast back to. Tag is consistent with the prod arm + // by definition. unsafe { crate::TurboTasksHandle::from_raw_parts( crate::HandleTag::Prod, @@ -614,6 +619,7 @@ impl TurboTasks { /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. /// Helper that clones the internal `Arc` first; equivalent to /// `self.pin().make_handle()`. + #[cfg(feature = "prod_handle")] pub fn make_handle_from_ref(&self) -> crate::TurboTasksHandle { self.pin().make_handle() } From dbe70d6df4357a7ab33fc2c75c14b9c5c9b4db07 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Mon, 18 May 2026 13:48:37 -0700 Subject: [PATCH 07/19] Move retry helpers from turbo-tasks-testing to turbopack-bench The retry/retry_async functions are generic Future utilities with no dependency on turbo-tasks. They previously lived in turbo-tasks-testing where they pulled the rest of that crate (and its test_handle feature activation) into anything that transitively needed retry. Specifically, turbopack-cli dev-depped turbopack-bench which depped turbo-tasks-testing solely for retry(). That dependency triggered turbo-tasks feature unification that enabled test_handle, generating extern decls for __tt_test_* in turbopack-cli's lib test binary, which then failed to link because cargo doesn't pull the unused turbo-tasks-testing rlib into the binary's link command. Moving the two retry helpers to turbopack-bench (their sole user) breaks that chain. turbopack-cli's lib tests now build cleanly. --- Cargo.lock | 1 - turbopack/crates/turbo-tasks-testing/src/lib.rs | 1 - turbopack/crates/turbopack-bench/Cargo.toml | 1 - turbopack/crates/turbopack-bench/src/util/mod.rs | 7 +++++-- .../src => turbopack-bench/src/util}/retry.rs | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename turbopack/crates/{turbo-tasks-testing/src => turbopack-bench/src/util}/retry.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 243dabd82acb..3f9d1c36ea58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10161,7 +10161,6 @@ dependencies = [ "tokio", "tungstenite 0.20.1", "turbo-tasks", - "turbo-tasks-testing", "turbopack-create-test-app", "url", ] diff --git a/turbopack/crates/turbo-tasks-testing/src/lib.rs b/turbopack/crates/turbo-tasks-testing/src/lib.rs index 64e9d7033b6a..6cda42b297bd 100644 --- a/turbopack/crates/turbo-tasks-testing/src/lib.rs +++ b/turbopack/crates/turbo-tasks-testing/src/lib.rs @@ -1,6 +1,5 @@ //! Testing utilities and macros for turbo-tasks and applications based on it. -pub mod retry; mod run; pub use crate::run::{ diff --git a/turbopack/crates/turbopack-bench/Cargo.toml b/turbopack/crates/turbopack-bench/Cargo.toml index 4432d8dacc94..dd67339bcfb1 100644 --- a/turbopack/crates/turbopack-bench/Cargo.toml +++ b/turbopack/crates/turbopack-bench/Cargo.toml @@ -34,7 +34,6 @@ tempfile = { workspace = true } tokio = { workspace = true, features = ["full"] } tungstenite = { workspace = true } turbo-tasks = { workspace = true } -turbo-tasks-testing = { workspace = true } turbopack-create-test-app = { workspace = true } url = { workspace = true } diff --git a/turbopack/crates/turbopack-bench/src/util/mod.rs b/turbopack/crates/turbopack-bench/src/util/mod.rs index 460f092f8860..30ae356e8fa2 100644 --- a/turbopack/crates/turbopack-bench/src/util/mod.rs +++ b/turbopack/crates/turbopack-bench/src/util/mod.rs @@ -20,12 +20,14 @@ pub use prepared_app::PreparedApp; use regex::Regex; use tungstenite::{Error::Protocol, error::ProtocolError::ResetWithoutClosingHandshake}; use turbo_tasks::util::FormatDuration; -use turbo_tasks_testing::retry::{retry, retry_async}; use turbopack_create_test_app::test_app_builder::{ EffectMode, PackageJsonConfig, TestApp, TestAppBuilder, }; -use self::env::read_env_bool; +use self::{ + env::read_env_bool, + retry::{retry, retry_async}, +}; use crate::bundlers::{Bundler, RenderType}; pub mod env; @@ -33,6 +35,7 @@ pub mod module_picker; pub mod npm; mod page_guard; mod prepared_app; +mod retry; pub const BINDING_NAME: &str = "__turbopackBenchBinding"; diff --git a/turbopack/crates/turbo-tasks-testing/src/retry.rs b/turbopack/crates/turbopack-bench/src/util/retry.rs similarity index 100% rename from turbopack/crates/turbo-tasks-testing/src/retry.rs rename to turbopack/crates/turbopack-bench/src/util/retry.rs From 154999e3d47cc6486867f0a319ea4fd71ebaa00f Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Mon, 18 May 2026 15:46:30 -0700 Subject: [PATCH 08/19] Add Unreachable variant to HandleTag for no-features builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When 'turbo-tasks' is built standalone with neither 'prod_handle' nor 'test_handle' active, 'HandleTag' previously became a zero-variant enum which is invalid under '#[repr(u8)]' — and downstream consumers (e.g. 'turbo-tasks-bytes') that only declare value types would fail to compile because TurboTasks::make_handle requires 'HandleTag::Prod'. This commit: - Adds 'HandleTag::Unreachable = 255', present only when neither feature is active. The variant is never constructed by consumer code; it just keeps the enum inhabited. - Adds '_ => unreachable!()' arms to every dispatch 'match' (TurboTasksHandle's forwarder macro, task_statistics, Clone, Drop, downgrade, and the same trio for TurboTasksWeakHandle), feature-gated to only exist when no real arm exists. - Ungates TurboTasks::make_handle / make_handle_from_ref so they always exist; in the no-features build they construct a handle with the Unreachable tag (any dispatch through it panics at runtime, but the binary builds). - Adds 'compile_error!' if a build activates 'test_handle' but not 'prod_handle' AND tries to call TurboTasks::make_handle — that combination is structurally inconsistent. The workspace 'cargo check' is green. 'cargo build --workspace --tests --benches' still has linker errors in crates that don't directly dep on turbo-tasks-backend or -testing but get their features unified on by other workspace members. Fixing those needs 'extern crate' anchors in the affected crates (next commit). --- turbopack/crates/turbo-tasks/src/handle.rs | 34 ++++++++++++++++++++- turbopack/crates/turbo-tasks/src/manager.rs | 30 +++++++++++++----- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 0f1e6e375598..afddc0038a0a 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -58,6 +58,13 @@ use std::ptr::NonNull; /// `__tt_prod_*` symbol with no comparison. The two-arm case (both /// features active, e.g. workspace tests) compiles to `cmp + b.ne + /// direct call`. +/// +/// When neither feature is active (e.g. building `turbo-tasks` standalone +/// or downstream consumers that only declare value types and don't link +/// any backend), the only variant is `Unreachable` — `turbo-tasks`'s lib +/// still compiles, but constructing or dispatching a handle is a +/// `unreachable!()` panic. This is intentional: code that builds without +/// either feature has no concrete implementation to dispatch to. #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum HandleTag { @@ -68,6 +75,11 @@ pub enum HandleTag { /// `VcStorage` — the test-only handle used by `turbo-tasks-testing`. #[cfg(feature = "test_handle")] Test = 1, + /// Placeholder variant kept only so the enum stays inhabited when + /// neither provider feature is active. Never constructed at runtime; + /// dispatch arms reach `unreachable!()`. + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + Unreachable = 255, } /// A type-erased reference to a concrete `TurboTasksApi` implementation. @@ -161,6 +173,7 @@ macro_rules! tt_decl_handle_method { ) => { impl TurboTasksHandle { #[inline] + #[allow(unused_variables)] pub fn $name(&self $(, $arg : $ty)*) $(-> $ret)? { match self.tag { #[cfg(feature = "prod_handle")] @@ -171,6 +184,11 @@ macro_rules! tt_decl_handle_method { HandleTag::Test => unsafe { ${concat(__tt_test_, $name)}(self.ptr.as_ptr() $(, $arg)*) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!( + "TurboTasksHandle dispatch with neither `prod_handle` nor `test_handle` \ + feature active on `turbo-tasks`" + ), } } } @@ -442,11 +460,13 @@ impl TurboTasksHandle { // owned by the underlying `TurboTasks` / `VcStorage`, which the // handle holds alive via its Arc. The returned reference is bound // to `&self`. - let ptr = match self.tag { + let ptr: *const crate::task_statistics::TaskStatisticsApi = match self.tag { #[cfg(feature = "prod_handle")] HandleTag::Prod => unsafe { __tt_prod_task_statistics(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_task_statistics(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), }; unsafe { &*ptr } } @@ -499,6 +519,8 @@ impl Clone for TurboTasksHandle { HandleTag::Prod => unsafe { __tt_prod_clone_arc(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_clone_arc(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), } Self { tag: self.tag, @@ -515,6 +537,8 @@ impl Drop for TurboTasksHandle { HandleTag::Prod => unsafe { __tt_prod_drop_arc(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_drop_arc(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), } } } @@ -528,6 +552,8 @@ impl TurboTasksHandle { HandleTag::Prod => unsafe { __tt_prod_downgrade(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_downgrade(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), }; TurboTasksWeakHandle { tag: self.tag, @@ -575,6 +601,8 @@ impl TurboTasksWeakHandle { HandleTag::Prod => unsafe { __tt_prod_upgrade(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_upgrade(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), }; let strong_ptr = NonNull::new(strong_ptr as *mut ())?; // Safety: the provider returned a non-null `Arc::into_raw` pointer @@ -592,6 +620,8 @@ impl Clone for TurboTasksWeakHandle { HandleTag::Prod => unsafe { __tt_prod_clone_weak(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_clone_weak(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), } Self { tag: self.tag, @@ -608,6 +638,8 @@ impl Drop for TurboTasksWeakHandle { HandleTag::Prod => unsafe { __tt_prod_drop_weak(self.ptr.as_ptr()) }, #[cfg(feature = "test_handle")] HandleTag::Test => unsafe { __tt_test_drop_weak(self.ptr.as_ptr()) }, + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + HandleTag::Unreachable => unreachable!(), } } } diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 89a3b08ea3ec..98271fe3887b 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -599,27 +599,41 @@ impl TurboTasks { /// Consumes one strong refcount from the given `Arc`; the handle /// will drop that refcount when itself dropped. /// - /// Only available when the `prod_handle` feature is enabled (i.e. when - /// `turbo-tasks-backend` is in the dep graph). - #[cfg(feature = "prod_handle")] + /// When the `prod_handle` feature is active, the returned handle + /// dispatches into the `__tt_prod_*` providers (in + /// `turbo-tasks-backend`). When neither provider feature is active, + /// this still compiles so consumers that merely declare value types + /// via `turbo-tasks` macros build, but any dispatch through the + /// returned handle will `unreachable!()` at runtime. pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { + // Pick the right tag for the current build: + // * `prod_handle` active (with or without `test_handle`) → Prod + // * neither active → Unreachable + // A `test_handle`-only build doesn't make sense here (`TurboTasks` + // is the prod handle's concrete type) and is not supported. + #[cfg(feature = "prod_handle")] + let tag = crate::HandleTag::Prod; + #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] + let tag = crate::HandleTag::Unreachable; + #[cfg(all(feature = "test_handle", not(feature = "prod_handle")))] + compile_error!( + "TurboTasks::make_handle is unreachable without the `prod_handle` feature active on \ + `turbo-tasks`. A `test_handle`-only build of `turbo-tasks` that still constructs a \ + `TurboTasks` is not supported." + ); let ptr = Arc::into_raw(self) as *mut (); // Safety: `ptr` came from `Arc::into_raw` on a `TurboTasks`, // which the `__tt_prod_*` providers (in `turbo-tasks-backend`) // know how to cast back to. Tag is consistent with the prod arm // by definition. unsafe { - crate::TurboTasksHandle::from_raw_parts( - crate::HandleTag::Prod, - std::ptr::NonNull::new_unchecked(ptr), - ) + crate::TurboTasksHandle::from_raw_parts(tag, std::ptr::NonNull::new_unchecked(ptr)) } } /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. /// Helper that clones the internal `Arc` first; equivalent to /// `self.pin().make_handle()`. - #[cfg(feature = "prod_handle")] pub fn make_handle_from_ref(&self) -> crate::TurboTasksHandle { self.pin().make_handle() } From 7c26802bee90a4098e7b25f792e51ae5fbdb8712 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Mon, 18 May 2026 23:53:47 -0700 Subject: [PATCH 09/19] Rename Prod/Test variants and prod_handle/test_handle features Renames the dispatch arms by mechanism (Static/Dynamic) rather than by intended user (Prod/Test). VcStorage from turbo-tasks-testing uses the Dynamic arm because we don't want to wire its concrete type into the static dispatch surface, not because dynamic dispatch is inherently a "test" concept. Also restructures features so static_handle is explicit opt-in (activated by turbo-tasks-backend's static_handle feature, which the napi binding enables) rather than being feature-unified workspace-wide. This avoids the linker error where test binaries that don't directly link turbo-tasks-backend would otherwise see unresolved __tt_static_* extern symbols. Drops the unused read_task_output free function. --- crates/next-napi-bindings/Cargo.toml | 2 +- .../crates/turbo-tasks-backend/Cargo.toml | 16 +- .../crates/turbo-tasks-backend/benches/mod.rs | 6 - .../src/handle_providers.rs | 24 +- .../crates/turbo-tasks-backend/src/lib.rs | 11 +- .../turbo-tasks-backend/tests/eviction.rs | 6 - .../crates/turbo-tasks-testing/Cargo.toml | 2 +- turbopack/crates/turbo-tasks/Cargo.toml | 30 +- turbopack/crates/turbo-tasks/src/handle.rs | 566 +++++++++--------- turbopack/crates/turbo-tasks/src/lib.rs | 2 +- turbopack/crates/turbo-tasks/src/manager.rs | 39 +- 11 files changed, 357 insertions(+), 347 deletions(-) diff --git a/crates/next-napi-bindings/Cargo.toml b/crates/next-napi-bindings/Cargo.toml index 5237da8bd97f..af34b8506341 100644 --- a/crates/next-napi-bindings/Cargo.toml +++ b/crates/next-napi-bindings/Cargo.toml @@ -94,7 +94,7 @@ swc_plugin_backend_wasmtime = { workspace = true } tokio = { workspace = true, features = ["full"] } turbo-rcstr = { workspace = true, features = ["napi"] } turbo-tasks = { workspace = true } -turbo-tasks-backend = { workspace = true } +turbo-tasks-backend = { workspace = true, features = ["static_handle"] } turbo-tasks-fs = { workspace = true } turbo-unix-path = { workspace = true } next-api = { workspace = true, features = ["worker_pool"] } diff --git a/turbopack/crates/turbo-tasks-backend/Cargo.toml b/turbopack/crates/turbo-tasks-backend/Cargo.toml index 2acbcb1d0874..339a5083092d 100644 --- a/turbopack/crates/turbo-tasks-backend/Cargo.toml +++ b/turbopack/crates/turbo-tasks-backend/Cargo.toml @@ -14,6 +14,20 @@ workspace = true [features] default = [] + +# Emits `#[no_mangle] pub extern "Rust" fn __tt_static_*` providers for +# the prod dispatch arm of `TurboTasksHandle` (see +# `turbo_tasks::handle`). Activates `turbo-tasks/static_handle` so the +# declarations and providers line up. +# +# Only the binary that actually runs prod turbo-tasks code (the +# `next-napi-bindings` napi addon) should enable this. Activating +# `static_handle` workspace-wide via feature unification would cause every +# linked test binary to need to resolve the providers, but most don't +# directly link `turbo-tasks-backend`. Explicit opt-in keeps the +# linker-symbol contract a binary-by-binary concern. +static_handle = ["turbo-tasks/static_handle"] + print_cache_item_size_with_compressed = ["print_cache_item_size", "dep:lzzzz"] print_cache_item_size = [] no_fast_stale = [] @@ -58,7 +72,7 @@ tracing = { workspace = true } turbo-bincode = { workspace = true } turbo-persistence = { workspace = true } turbo-rcstr = { workspace = true } -turbo-tasks = { workspace = true, features = ["prod_handle"] } +turbo-tasks = { workspace = true } turbo-tasks-hash = { workspace = true } turbo-tasks-malloc = { workspace = true, default-features = false } thread_local = { workspace = true } diff --git a/turbopack/crates/turbo-tasks-backend/benches/mod.rs b/turbopack/crates/turbo-tasks-backend/benches/mod.rs index dd4fca4940bf..537e93e49478 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/mod.rs @@ -1,12 +1,6 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] -// Force linkage of `__tt_test_*` providers. See similar comment in -// `tests/eviction.rs`. Benches don't use `turbo_tasks_testing` directly -// but the feature-unified `test_handle` decl exists, so the binary -// needs the test-arm providers linked. -extern crate turbo_tasks_testing; - use criterion::{Criterion, criterion_group, criterion_main}; pub(crate) mod overhead; diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 7ac93ae9ecee..27172107e66f 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -1,8 +1,8 @@ -//! `#[no_mangle] pub extern "Rust" fn __tt_prod_*` providers for the +//! `#[no_mangle] pub extern "Rust" fn __tt_static_*` providers for the //! production arm of the `TurboTasksHandle` dispatch. //! //! The forward declarations live in `turbo_tasks::handle` and are gated by -//! the `prod_handle` Cargo feature on `turbo-tasks`. `turbo-tasks-backend` +//! the `static_handle` Cargo feature on `turbo-tasks`. `turbo-tasks-backend` //! activates that feature in its dep entry, so these `#[no_mangle]` //! symbols are linked into any binary that pulls in `turbo-tasks-backend`. //! @@ -32,14 +32,14 @@ use crate::{NoopBackingStorage, TurboBackingStorage, TurboTasksBackend}; pub type ProdHandleConcrete = turbo_tasks::TurboTasks>>; -/// Generates `#[no_mangle] pub extern "Rust" fn __tt_prod_(...)` +/// Generates `#[no_mangle] pub extern "Rust" fn __tt_static_(...)` /// for a single dispatched method, dispatched via method call syntax. macro_rules! provide_prod { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_prod_, $name)}( + pub extern "Rust" fn ${concat(__tt_static_, $name)}( ptr: *const () $(, $arg : $ty)* ) $(-> $ret)? { @@ -61,7 +61,7 @@ macro_rules! provide_prod_trait { fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_prod_, $name)}( + pub extern "Rust" fn ${concat(__tt_static_, $name)}( ptr: *const () $(, $arg : $ty)* ) $(-> $ret)? { @@ -186,7 +186,7 @@ provide_prod!(fn is_tracking_dependencies() -> bool); // the lifetime to `&self`. This is sound because the underlying Arc // (held by the handle) keeps the `TaskStatisticsApi` alive. #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_task_statistics( +pub extern "Rust" fn __tt_static_task_statistics( ptr: *const (), ) -> *const turbo_tasks::task_statistics::TaskStatisticsApi { let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; @@ -196,7 +196,7 @@ pub extern "Rust" fn __tt_prod_task_statistics( // ---- Arc clone / drop ----------------------------------------------------- #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_clone_arc(ptr: *const ()) { +pub extern "Rust" fn __tt_static_clone_arc(ptr: *const ()) { // Bump the refcount of the Arc whose data pointer is `ptr`. The caller // (`::clone`) is responsible for reusing // the same `ptr` value in the new handle, so we don't need to return @@ -205,7 +205,7 @@ pub extern "Rust" fn __tt_prod_clone_arc(ptr: *const ()) { } #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_drop_arc(ptr: *const ()) { +pub extern "Rust" fn __tt_static_drop_arc(ptr: *const ()) { // Decrement the refcount; runs the destructor when it reaches zero. unsafe { Arc::::decrement_strong_count(ptr as *const ProdHandleConcrete) } } @@ -213,7 +213,7 @@ pub extern "Rust" fn __tt_prod_drop_arc(ptr: *const ()) { // ---- Weak refcount providers --------------------------------------------- #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const () { +pub extern "Rust" fn __tt_static_downgrade(arc_ptr: *const ()) -> *const () { // Reconstitute the Arc transiently to call `downgrade`, then leak the // Arc back so its refcount is unchanged. The Weak we produce owns its // own weak refcount. @@ -224,7 +224,7 @@ pub extern "Rust" fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const () { } #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const () { +pub extern "Rust" fn __tt_static_upgrade(weak_ptr: *const ()) -> *const () { // Reconstitute the Weak transiently to attempt upgrade, then leak it // back so its refcount is unchanged. let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; @@ -237,7 +237,7 @@ pub extern "Rust" fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const () { } #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_clone_weak(weak_ptr: *const ()) { +pub extern "Rust" fn __tt_static_clone_weak(weak_ptr: *const ()) { // `Weak` has no `increment_weak_count` API, so we round-trip through // `Weak::clone` and leak both copies. let weak = unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }; @@ -247,6 +247,6 @@ pub extern "Rust" fn __tt_prod_clone_weak(weak_ptr: *const ()) { } #[unsafe(no_mangle)] -pub extern "Rust" fn __tt_prod_drop_weak(weak_ptr: *const ()) { +pub extern "Rust" fn __tt_static_drop_weak(weak_ptr: *const ()) { drop(unsafe { ::std::sync::Weak::from_raw(weak_ptr as *const ProdHandleConcrete) }); } diff --git a/turbopack/crates/turbo-tasks-backend/src/lib.rs b/turbopack/crates/turbo-tasks-backend/src/lib.rs index 7d21351f597d..801857c893e8 100644 --- a/turbopack/crates/turbo-tasks-backend/src/lib.rs +++ b/turbopack/crates/turbo-tasks-backend/src/lib.rs @@ -2,21 +2,12 @@ #![feature(box_patterns)] #![feature(macro_metavar_expr_concat)] -// Force the linker to pull in `libturbo_tasks_testing.rlib` for the lib -// test binary, so the `__tt_test_*` extern "Rust" providers (which the -// lib references transitively through the dispatch in `turbo-tasks` — -// `test_handle` is feature-unified-on whenever a dev-dep activates it) -// are resolved at link time. Without this, cargo doesn't include the -// `turbo-tasks-testing` rlib in the test binary's link command because -// no Rust code references it by name. -#[cfg(test)] -extern crate turbo_tasks_testing; - mod backend; mod backing_storage; mod data; mod database; mod error; +#[cfg(feature = "static_handle")] mod handle_providers; mod kv_backing_storage; mod utils; diff --git a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs index 163c8cfd7805..cd8e74bbf0b9 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs @@ -2,12 +2,6 @@ #![feature(arbitrary_self_types_pointers)] #![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this -// Force linkage of `__tt_test_*` providers. This test doesn't `use` -// anything from `turbo_tasks_testing`, but the feature unification of -// `test_handle` on `turbo-tasks` (active because dev-deps bring it in -// through other means) makes the lib reference `__tt_test_*` symbols. -extern crate turbo_tasks_testing; - use std::sync::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, diff --git a/turbopack/crates/turbo-tasks-testing/Cargo.toml b/turbopack/crates/turbo-tasks-testing/Cargo.toml index 5fa0581db572..8790afbc55cb 100644 --- a/turbopack/crates/turbo-tasks-testing/Cargo.toml +++ b/turbopack/crates/turbo-tasks-testing/Cargo.toml @@ -19,5 +19,5 @@ futures = { workspace = true } rustc-hash = { workspace = true } smallvec = { workspace = true } tokio = { workspace = true } -turbo-tasks = { workspace = true, features = ["non_operation_vc_strongly_consistent", "test_handle"] } +turbo-tasks = { workspace = true, features = ["non_operation_vc_strongly_consistent", "dynamic_handle"] } turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbo-tasks/Cargo.toml b/turbopack/crates/turbo-tasks/Cargo.toml index 9bdcf73a1067..f608126995a2 100644 --- a/turbopack/crates/turbo-tasks/Cargo.toml +++ b/turbopack/crates/turbo-tasks/Cargo.toml @@ -15,16 +15,26 @@ hanging_detection = [] task_id_details = [] verify_determinism = [] -# Handle dispatch arms. These are activated by feature unification: -# turbo-tasks-backend deps on turbo-tasks [prod_handle] -# turbo-tasks-testing deps on turbo-tasks [test_handle] -# Anything that transitively links one or both of those crates gets the -# corresponding HandleTag variants, extern "Rust" forward decls, and -# `match` arms in TurboTasksHandle's forwarder methods. -# Pure-prod binaries (the napi binding) get a one-arm enum so LLVM -# collapses the match in each forwarder to a direct call. -prod_handle = [] -test_handle = [] +# Adds the devirtualized `Prod` arm to `TurboTasksHandle`, dispatching +# via `#[no_mangle] pub extern "Rust" fn __tt_static_*` symbols defined in +# `turbo-tasks-backend`. Activated by `turbo-tasks-backend`'s dep on +# `turbo-tasks`. Only binaries whose link graph includes +# `turbo-tasks-backend` can resolve the externs, which is also exactly +# the situation where the prod arm is meaningful to call. +# +# Crates that don't link `turbo-tasks-backend` (the workspace has many — +# `turbo-esregex`, `turbo-tasks-fs`'s lib-tests, etc.) compile without +# this feature: `TurboTasksHandle` falls back to `Arc` +# vtable dispatch through the `Test` variant. Since those crates never +# actually run prod tasks, the perf delta is moot. +static_handle = [] + +# Adds the `Test` variant to `TurboTasksHandle`, which dispatches via +# `Arc` vtable. Activated by `turbo-tasks-testing`'s +# dep on `turbo-tasks`. The test arm uses normal Rust vtable dispatch, +# so there's no linker-symbol footgun if the feature unifies on without +# the providing crate being directly linked. +dynamic_handle = [] # TODO(bgw): This feature is here to unblock turning on local tasks by default. It's only turned on # in unit tests. This will be removed very soon. diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index afddc0038a0a..ec52293c8cfc 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -1,172 +1,200 @@ -//! Tagged-pointer dispatch for the turbo-tasks task-local. +//! Devirtualized dispatch for the turbo-tasks task-local. //! -//! The task-local that holds the current `TurboTasksApi` implementation has -//! historically been an `Arc`. The `dyn` is necessary -//! because the prod handle (`TurboTasks`) is generic over a backend -//! type that the `task_local!` macro cannot name — but the cost is an -//! indirect vtable call on every dispatched method, and rustc currently does -//! not emit the LLVM metadata that `WholeProgramDevirt` needs to inline -//! through trait objects ([rust#68262], [rust#45774]). +//! The task-local that holds the current `TurboTasksApi` implementation +//! has historically been an `Arc`. The `dyn` is +//! necessary because the prod implementor (`TurboTasks`) is generic over a +//! backend type that the `task_local!` macro cannot name — but the cost +//! is an indirect vtable call on every dispatched method, and rustc +//! currently does not emit the LLVM metadata that `WholeProgramDevirt` +//! needs to inline through trait objects ([rust#68262], [rust#45774]). //! //! [rust#68262]: https://github.com/rust-lang/rust/issues/68262 //! [rust#45774]: https://github.com/rust-lang/rust/issues/45774 //! -//! This module replaces the `dyn` indirection with a tagged pointer that -//! dispatches through `extern "Rust"` forward declarations: +//! This module replaces the production path's `dyn` with `extern "Rust"` +//! direct calls. Under `lto = "thin"` + `codegen-units = 1` (this +//! workspace's release profile), the static-arm `extern "Rust"` call +//! inlines across the `turbo-tasks` → `turbo-tasks-backend` boundary, +//! collapsing the `match` arm to a direct call to the underlying backend +//! method. //! //! ```text -//! call site provider crate +//! call site (static feature active) turbo-tasks-backend //! ┌──────────────────────────┐ ┌─────────────────────────────────────┐ -//! │ tt.invalidate(task) ─────┼──────────────► #[no_mangle] pub extern "Rust" fn │ -//! │ match self.tag { … } │ │ __tt_prod_invalidate(ptr, task) { │ -//! │ │ │ let tt: &TurboTasks<…> = …; │ -//! │ │ │ tt.invalidate(task) │ -//! │ │ │ } │ +//! │ tt.invalidate(task) │ │ #[no_mangle] pub extern "Rust" fn │ +//! │ match self { │ │ __tt_static_invalidate(ptr, task) { │ +//! │ Static(ptr) => ───┐ │ │ let tt: &TurboTasks<…> = …; │ +//! │ Dynamic(arc) => … │ │ │ tt.invalidate(task) │ +//! │ } └─────────────► │ } │ //! └──────────────────────────┘ └─────────────────────────────────────┘ //! ``` //! -//! Under `lto = "thin"` + `codegen-units = 1` (this workspace's release -//! profile), the linker fully inlines the `extern "Rust"` call into the -//! caller. The `match` over a one-arm enum collapses entirely; over a -//! two-arm enum it compiles to `cmp + b.ne + direct call` and LLVM can -//! sometimes fuse the arms further. +//! For the dynamic arm: +//! ```text +//! call site +//! ┌──────────────────────────┐ +//! │ tt.invalidate(task) │ +//! │ Dynamic(arc) => │ +//! │ arc.invalidate(task) │ (normal vtable dispatch on Arc) +//! └──────────────────────────┘ +//! ``` //! //! ## Feature gating //! -//! Two Cargo features on `turbo-tasks` control which dispatch arms exist: +//! Each variant is feature-gated. The gates exist to avoid linker-symbol +//! breakage in crates that don't pull in the providing crate at link +//! time. Names describe the *dispatch mechanism*, not the intended user: +//! `VcStorage` from `turbo-tasks-testing` uses the dynamic arm because we +//! don't want to wire its concrete type into the static dispatch surface, +//! not because dynamic dispatch is inherently a "test" concept. +//! +//! - **`static_handle`** — activated by `turbo-tasks-backend`'s feature `static_handle` (which the +//! napi binding opts into). Adds the `Static` variant and the `extern "Rust"` forward +//! declarations. Any binary that activates `turbo-tasks-backend/static_handle` gets both the +//! feature on `turbo-tasks` and the `#[no_mangle]` provider definitions, so the externs resolve +//! cleanly at link time. +//! - **`dynamic_handle`** — activated by `turbo-tasks-testing`'s dep on `turbo-tasks`. Adds the +//! `Dynamic` variant. Uses normal `Arc` vtable dispatch, so there's no linker footgun if the +//! feature unifies on without the providing crate being linked. //! -//! - `prod_handle` — activated by `turbo-tasks-backend` (which owns the concrete production handle -//! type and emits the `__tt_prod_*` providers). Pulled in transitively by anything that links the -//! backend, including the napi binding. -//! - `test_handle` — activated by `turbo-tasks-testing` (which owns `VcStorage` and emits the -//! `__tt_test_*` providers). +//! With neither feature active, `HandleInner` has a single `Unreachable` +//! variant. This keeps the type inhabited (the `task_local!` declaration +//! requires that) but unconstructible. Crates in this situation +//! (`turbo-esregex`, `turbo-tasks-bytes` lib-tests, …) compile but cannot +//! actually run turbo-tasks code — which is fine because they don't. //! -//! A pure-prod binary (e.g. the napi binding) sees `HandleTag` with only -//! the `Prod` variant and the `match` in each forwarder collapses to a -//! direct call. A workspace test build sees both variants and gets the -//! two-arm match. Features unify across the dep graph; consumers do not -//! enable the features directly. - -use std::ptr::NonNull; - -/// Identifier for which concrete implementation a [`TurboTasksHandle`] points -/// at. Used as the tag in the tagged-pointer dispatch. -/// -/// Variants are feature-gated. A pure-prod build (only `prod_handle` -/// active) has only the `Prod` variant, which makes every dispatch -/// `match` a one-arm collapse — LLVM emits a direct call to the -/// `__tt_prod_*` symbol with no comparison. The two-arm case (both -/// features active, e.g. workspace tests) compiles to `cmp + b.ne + -/// direct call`. -/// -/// When neither feature is active (e.g. building `turbo-tasks` standalone -/// or downstream consumers that only declare value types and don't link -/// any backend), the only variant is `Unreachable` — `turbo-tasks`'s lib -/// still compiles, but constructing or dispatching a handle is a -/// `unreachable!()` panic. This is intentional: code that builds without -/// either feature has no concrete implementation to dispatch to. -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum HandleTag { - /// `TurboTasks>` — the production handle used by - /// `next-napi-bindings`, benches, etc. - #[cfg(feature = "prod_handle")] - Prod = 0, - /// `VcStorage` — the test-only handle used by `turbo-tasks-testing`. - #[cfg(feature = "test_handle")] - Test = 1, - /// Placeholder variant kept only so the enum stays inhabited when - /// neither provider feature is active. Never constructed at runtime; - /// dispatch arms reach `unreachable!()`. - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - Unreachable = 255, +//! ## Follow-ups +//! +//! - `task_statistics` is only used by `turbo-tasks-backend/tests/ task_statistics.rs`; if that +//! test calls it on a concrete `TurboTasks` instead of via the handle, it can move off the +//! dispatch surface (no extern, no method on `TurboTasksHandle`). +//! - `VcStorage` is the only reason the `dynamic_handle` feature exists. If `turbo-tasks-testing`'s +//! harness used a real `TurboTasks` with a noop backend, the `Dynamic` variant could be +//! deleted entirely, collapsing the dispatch to a single `match` arm. + +use std::{ptr::NonNull, sync::Arc}; + +#[cfg(any(feature = "static_handle", feature = "dynamic_handle"))] +use crate::TurboTasksApi; + +/// Type-erased reference to a `TurboTasksApi` implementation. See the +/// [module docs](self) for the dispatch design. +pub struct TurboTasksHandle(HandleInner); + +enum HandleInner { + /// Statically-dispatched handle. The pointer is the data pointer of + /// an `Arc::into_raw(arc)` where `arc: Arc>` for the + /// concrete backend type the `__tt_static_*` providers were + /// generated for. Dispatch goes through `extern "Rust"` symbols + /// defined in `turbo-tasks-backend`. + #[cfg(feature = "static_handle")] + Static(NonNull<()>), + /// Dynamically-dispatched handle. Normal `Arc` + /// vtable dispatch. Used by `VcStorage` and any other harness that + /// doesn't want to own a real `TurboTasks`. Not on the production + /// hot path. + #[cfg(feature = "dynamic_handle")] + Dynamic(Arc), + /// Placeholder variant kept so `HandleInner` stays inhabited when + /// neither feature is on. Constructing a handle requires a feature; + /// dispatch on this variant is `unreachable!()`. This lets + /// `turbo-tasks` compile standalone for crates that merely declare + /// value types and never construct a handle. + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + Unreachable, } -/// A type-erased reference to a concrete `TurboTasksApi` implementation. -/// -/// Logically equivalent to an `Arc`: the pointer is the -/// raw `Arc::into_raw(...)` of the concrete handle, the tag tells the -/// dispatch which provider to call. -/// -/// `Clone` and `Drop` route through `__tt__clone_arc` / -/// `__tt__drop_arc` so refcounting stays correct. -#[derive(Debug)] -pub struct TurboTasksHandle { - tag: HandleTag, - /// Points at the inner of an `Arc` owned via - /// `Arc::into_raw`. The lifetime is managed by `Clone` / `Drop` - /// dispatching through the provider crate. - ptr: NonNull<()>, +impl std::fmt::Debug for TurboTasksHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + #[cfg(feature = "static_handle")] + HandleInner::Static(ptr) => f + .debug_tuple("TurboTasksHandle::Static") + .field(ptr) + .finish(), + #[cfg(feature = "dynamic_handle")] + HandleInner::Dynamic(_) => f.debug_tuple("TurboTasksHandle::Dynamic").finish(), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + HandleInner::Unreachable => f.write_str("TurboTasksHandle::Unreachable"), + } + } } -// Safety: the underlying concrete handle types (`TurboTasks<…>` and -// `VcStorage`) are themselves `Send + Sync` and are reference-counted via -// `Arc`. The raw pointer is just an erased `Arc::into_raw` result; it does -// not introduce additional aliasing beyond what the Arc allowed. +// Safety: every concrete handle behind a `TurboTasksHandle` is itself +// `Send + Sync` and reference-counted via `Arc`. For the `Static` +// variant, the raw pointer is just an erased `Arc::into_raw`. The +// `Dynamic` variant is `Arc` (the trait +// bounds make it `Send + Sync`). unsafe impl Send for TurboTasksHandle {} unsafe impl Sync for TurboTasksHandle {} impl TurboTasksHandle { - /// Constructs a handle from raw parts. Intended to be called only by - /// `turbo-tasks-handle`'s `from_prod` / `from_test` constructors, which - /// own the safety contract that `ptr` is a valid `Arc::into_raw` pointer - /// for the concrete type associated with `tag`. + /// Construct a `Static` handle from an `Arc::into_raw` pointer. /// /// # Safety /// - /// `ptr` must be a pointer obtained from `Arc::into_raw` on the concrete - /// handle type whose tag matches `tag`. Ownership of the refcount - /// transfers into the new `TurboTasksHandle`. + /// `ptr` must come from `Arc::into_raw(arc)` where `arc` is an + /// `Arc>` for the concrete backend the + /// `__tt_static_*` providers (in `turbo-tasks-backend`) target. + /// Ownership of one strong refcount transfers into the handle. + #[cfg(feature = "static_handle")] #[inline] - pub unsafe fn from_raw_parts(tag: HandleTag, ptr: NonNull<()>) -> Self { - Self { tag, ptr } + pub unsafe fn from_static_raw(ptr: NonNull<()>) -> Self { + Self(HandleInner::Static(ptr)) } - /// The tag indicating which concrete implementation this handle points - /// at. Exposed for tests and diagnostics; the dispatch macro handles - /// the normal case. + /// Stub for `from_static_raw` when the `static_handle` feature is off. + /// Cannot actually be constructed and called at runtime because the + /// dispatch paths that produce a static handle are gated on the same + /// feature; this is kept as a `panic!` body so the `TurboTasks` + /// inherent `make_handle` (in `manager.rs`) can compile in builds + /// that don't activate `static_handle`. Linker dead-code elimination + /// removes both this and its callers from the final binary. + #[cfg(not(feature = "static_handle"))] #[inline] - pub fn tag(&self) -> HandleTag { - self.tag + pub unsafe fn from_static_raw(_ptr: NonNull<()>) -> Self { + unreachable!( + "TurboTasksHandle::from_static_raw called without `static_handle` feature on \ + `turbo-tasks`" + ) } - /// Raw pointer access, intended only for the dispatch macro and for - /// `turbo-tasks-handle`. Callers must respect the tag to interpret it. + /// Construct a `Dynamic` handle from any `TurboTasksApi` implementor. + #[cfg(feature = "dynamic_handle")] #[inline] - pub fn raw_ptr(&self) -> *const () { - self.ptr.as_ptr() + pub fn from_dynamic(arc: Arc) -> Self { + Self(HandleInner::Dynamic(arc)) } } // ===================================================================== -// `extern "Rust"` forward declarations. +// `extern "Rust"` forward declarations for the static arm. // -// Each dispatched `TurboTasksApi` method has two extern symbols — one per -// arm. The bodies are defined in `turbo-tasks-handle` and resolved at link -// time. Thin LTO inlines them. +// The bodies are defined `#[no_mangle]` in `turbo-tasks-backend` and +// resolved at link time. Thin LTO inlines them across the crate boundary. // ===================================================================== -/// Generates feature-gated `unsafe extern "Rust" { fn __tt__; }` -/// declarations. With only `prod_handle` active, only the prod decl is -/// emitted; with only `test_handle`, only the test decl. With both, both. +/// Generates `unsafe extern "Rust" { fn __tt_static_; }` declarations +/// for one dispatched method. Only emitted when the `static_handle` feature +/// is active — i.e. when `turbo-tasks-backend` is in the dep graph and +/// will provide the matching `#[no_mangle]` definitions. macro_rules! tt_decl_extern { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { + #[cfg(feature = "static_handle")] unsafe extern "Rust" { - #[cfg(feature = "prod_handle")] - fn ${concat(__tt_prod_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; - #[cfg(feature = "test_handle")] - fn ${concat(__tt_test_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; + fn ${concat(__tt_static_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; } }; } -/// Generates an inherent method on `TurboTasksHandle` that dispatches over -/// `match self.tag` to the corresponding `extern "Rust"` symbol. The match -/// arms are feature-gated alongside the [`HandleTag`] variants and the -/// extern decls, so single-variant builds get a one-arm match that LLVM -/// collapses to a direct call. +/// Generates an inherent method on `TurboTasksHandle` that dispatches via +/// `match self.0`. The `Static` arm calls the `__tt_static_` extern; +/// the `Dynamic` arm (if present) calls the method on the `Arc` (vtable dispatch). With no features active, the +/// `Unreachable` arm panics — but you can't construct such a handle, so +/// this is dead code that's kept only to make the type compile. macro_rules! tt_decl_handle_method { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? @@ -175,18 +203,20 @@ macro_rules! tt_decl_handle_method { #[inline] #[allow(unused_variables)] pub fn $name(&self $(, $arg : $ty)*) $(-> $ret)? { - match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { - ${concat(__tt_prod_, $name)}(self.ptr.as_ptr() $(, $arg)*) + match &self.0 { + #[cfg(feature = "static_handle")] + HandleInner::Static(ptr) => unsafe { + ${concat(__tt_static_, $name)}(ptr.as_ptr() $(, $arg)*) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { - ${concat(__tt_test_, $name)}(self.ptr.as_ptr() $(, $arg)*) - }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!( - "TurboTasksHandle dispatch with neither `prod_handle` nor `test_handle` \ + // The `TurboTasksApi` super-trait bound on the Arc + // makes every dispatched method (whether defined on + // `TurboTasksApi` itself or its supertrait + // `TurboTasksCallApi`) callable via method-call syntax. + #[cfg(feature = "dynamic_handle")] + HandleInner::Dynamic(arc) => arc.$name($($arg),*), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + HandleInner::Unreachable => unreachable!( + "TurboTasksHandle dispatch with neither `static_handle` nor `dynamic_handle` \ feature active on `turbo-tasks`" ), } @@ -197,11 +227,9 @@ macro_rules! tt_decl_handle_method { // ---- dispatched methods ------------------------------------------------- // -// Add new entries here when adding a method to the dispatch surface. Each -// entry must also be implemented in `turbo-tasks-handle`'s provider macro -// (which currently lists them explicitly — a future cleanup could share -// this list via a callback macro, but for now duplication is the cost of -// keeping both places easy to read). +// Keep this list in sync with the matching provider implementations in +// `turbo-tasks-backend/src/handle_providers.rs`. The list is duplicated; +// a missing static provider surfaces as a link error. // `TurboTasksCallApi` methods. tt_decl_extern!(fn dynamic_call( @@ -253,15 +281,6 @@ tt_decl_handle_method!(fn send_compilation_event( tt_decl_extern!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); tt_decl_handle_method!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); -// `run`, `run_once`, `run_once_with_reason`, `start_once_process`, and -// `stop_and_wait` need to be on the dispatch surface because the test -// harness in `turbo-tasks-testing` constructs a type-erased -// `TestInstance.tt: TurboTasksHandle` and passes it to the free -// `turbo_tasks::run_once` / `turbo_tasks::run` helpers. Without these on -// the handle, we'd have to either expose the concrete backend type -// through `TestInstance` (cascades into `Registration`) or duplicate the -// helpers per arm. Putting them on the dispatch surface is one extern -// symbol per method per arm — cheap. tt_decl_extern!(fn run( future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); @@ -440,15 +459,12 @@ tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); // `task_statistics` returns `&TaskStatisticsApi` borrowed from `&self`. // The macro can't express the lifetime relationship through a `*const ()` -// receiver, so the providers return `*const TaskStatisticsApi` and the -// handle wrapper re-binds the lifetime to `&self`. +// receiver, so the static provider returns `*const TaskStatisticsApi` and +// the handle wrapper re-binds the lifetime to `&self`. The dynamic arm +// just calls the trait method on the Arc. +#[cfg(feature = "static_handle")] unsafe extern "Rust" { - #[cfg(feature = "prod_handle")] - fn __tt_prod_task_statistics( - ptr: *const (), - ) -> *const crate::task_statistics::TaskStatisticsApi; - #[cfg(feature = "test_handle")] - fn __tt_test_task_statistics( + fn __tt_static_task_statistics( ptr: *const (), ) -> *const crate::task_statistics::TaskStatisticsApi; } @@ -456,75 +472,61 @@ unsafe extern "Rust" { impl TurboTasksHandle { #[inline] pub fn task_statistics(&self) -> &crate::task_statistics::TaskStatisticsApi { - // SAFETY: the provider returns a pointer to a `TaskStatisticsApi` - // owned by the underlying `TurboTasks` / `VcStorage`, which the - // handle holds alive via its Arc. The returned reference is bound - // to `&self`. - let ptr: *const crate::task_statistics::TaskStatisticsApi = match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_task_statistics(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_task_statistics(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), - }; - unsafe { &*ptr } + match &self.0 { + #[cfg(feature = "static_handle")] + HandleInner::Static(ptr) => { + // SAFETY: the provider returns a pointer to a + // `TaskStatisticsApi` owned by the underlying + // `TurboTasks`, which this handle keeps alive via its + // Arc. The returned reference is bound to `&self`. + let p = unsafe { __tt_static_task_statistics(ptr.as_ptr()) }; + unsafe { &*p } + } + #[cfg(feature = "dynamic_handle")] + HandleInner::Dynamic(arc) => arc.task_statistics(), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + HandleInner::Unreachable => unreachable!(), + } } } // ===================================================================== -// Clone / Drop dispatch — Arc-style refcounting through extern symbols. +// Clone / Drop — Arc refcounting through extern symbols (prod) or +// the std `Arc` impls (test). // ===================================================================== +#[cfg(feature = "static_handle")] unsafe extern "Rust" { - #[cfg(feature = "prod_handle")] - fn __tt_prod_clone_arc(ptr: *const ()); - #[cfg(feature = "prod_handle")] - fn __tt_prod_drop_arc(ptr: *const ()); - #[cfg(feature = "test_handle")] - fn __tt_test_clone_arc(ptr: *const ()); - #[cfg(feature = "test_handle")] - fn __tt_test_drop_arc(ptr: *const ()); - - // Weak-handle support. Each arm provides: - // downgrade : *const Arc -> *const Weak (transfers no refcount, - // creates a fresh Weak; caller owns the returned weak). - // upgrade : *const Weak -> *const Arc (returns null if the - // Arc is gone; otherwise transfers one strong refcount). + fn __tt_static_clone_arc(ptr: *const ()); + fn __tt_static_drop_arc(ptr: *const ()); + + // Weak-handle support for the static arm. Each provides: + // downgrade : *const Arc -> *const Weak (creates a fresh + // Weak; caller owns the returned weak). + // upgrade : *const Weak -> *const Arc (returns null if + // the Arc is gone; otherwise transfers one strong + // refcount). // clone_weak: bumps the weak refcount. // drop_weak : drops the weak refcount. - #[cfg(feature = "prod_handle")] - fn __tt_prod_downgrade(arc_ptr: *const ()) -> *const (); - #[cfg(feature = "prod_handle")] - fn __tt_prod_upgrade(weak_ptr: *const ()) -> *const (); - #[cfg(feature = "prod_handle")] - fn __tt_prod_clone_weak(weak_ptr: *const ()); - #[cfg(feature = "prod_handle")] - fn __tt_prod_drop_weak(weak_ptr: *const ()); - #[cfg(feature = "test_handle")] - fn __tt_test_downgrade(arc_ptr: *const ()) -> *const (); - #[cfg(feature = "test_handle")] - fn __tt_test_upgrade(weak_ptr: *const ()) -> *const (); - #[cfg(feature = "test_handle")] - fn __tt_test_clone_weak(weak_ptr: *const ()); - #[cfg(feature = "test_handle")] - fn __tt_test_drop_weak(weak_ptr: *const ()); + fn __tt_static_downgrade(arc_ptr: *const ()) -> *const (); + fn __tt_static_upgrade(weak_ptr: *const ()) -> *const (); + fn __tt_static_clone_weak(weak_ptr: *const ()); + fn __tt_static_drop_weak(weak_ptr: *const ()); } impl Clone for TurboTasksHandle { #[inline] fn clone(&self) -> Self { - match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_clone_arc(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_clone_arc(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), - } - Self { - tag: self.tag, - ptr: self.ptr, + match &self.0 { + #[cfg(feature = "static_handle")] + HandleInner::Static(ptr) => { + unsafe { __tt_static_clone_arc(ptr.as_ptr()) } + Self(HandleInner::Static(*ptr)) + } + #[cfg(feature = "dynamic_handle")] + HandleInner::Dynamic(arc) => Self(HandleInner::Dynamic(Arc::clone(arc))), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + HandleInner::Unreachable => unreachable!(), } } } @@ -532,13 +534,14 @@ impl Clone for TurboTasksHandle { impl Drop for TurboTasksHandle { #[inline] fn drop(&mut self) { - match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_drop_arc(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_drop_arc(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), + match &self.0 { + #[cfg(feature = "static_handle")] + HandleInner::Static(ptr) => unsafe { __tt_static_drop_arc(ptr.as_ptr()) }, + // Dynamic variant: the `Arc` field drops itself naturally. + #[cfg(feature = "dynamic_handle")] + HandleInner::Dynamic(_) => {} + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + HandleInner::Unreachable => {} } } } @@ -547,20 +550,22 @@ impl TurboTasksHandle { /// Downgrades to a weak handle, equivalent to `Arc::downgrade`. #[inline] pub fn downgrade(&self) -> TurboTasksWeakHandle { - let weak_ptr = match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_downgrade(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_downgrade(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), - }; - TurboTasksWeakHandle { - tag: self.tag, - // `downgrade` always produces a valid pointer (a `Weak` is never - // null even when the strong count is zero); we can safely - // `NonNull::new_unchecked` it. - ptr: unsafe { NonNull::new_unchecked(weak_ptr as *mut ()) }, + match &self.0 { + #[cfg(feature = "static_handle")] + HandleInner::Static(ptr) => { + let weak_ptr = unsafe { __tt_static_downgrade(ptr.as_ptr()) }; + // `Weak::into_raw` always produces a valid (non-null) + // pointer even when the strong count is zero. + TurboTasksWeakHandle(WeakInner::Static(unsafe { + NonNull::new_unchecked(weak_ptr as *mut ()) + })) + } + #[cfg(feature = "dynamic_handle")] + HandleInner::Dynamic(arc) => { + TurboTasksWeakHandle(WeakInner::Dynamic(Arc::downgrade(arc))) + } + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + HandleInner::Unreachable => unreachable!(), } } } @@ -568,26 +573,42 @@ impl TurboTasksHandle { // ===================================================================== // Weak-handle dispatch. // -// Mirrors the strong-handle dispatch but holds the data pointer of a -// `Weak`. Used by long-lived non-task contexts (e.g. the filesystem -// watcher in `turbo-tasks-fs`) that need to reach back into TurboTasks -// without keeping it alive. +// Used by long-lived non-task contexts (e.g. the filesystem watcher in +// `turbo-tasks-fs`) that need to reach back into TurboTasks without +// keeping it alive. // ===================================================================== /// Weak counterpart to [`TurboTasksHandle`]. Constructed via /// [`TurboTasksHandle::downgrade`]; upgraded via /// [`TurboTasksWeakHandle::upgrade`]. -#[derive(Debug)] -pub struct TurboTasksWeakHandle { - tag: HandleTag, - /// Points at the inner of a `Weak` owned via - /// `Weak::into_raw`. The strong count may be zero by the time we - /// try to upgrade. - ptr: NonNull<()>, +pub struct TurboTasksWeakHandle(WeakInner); + +enum WeakInner { + #[cfg(feature = "static_handle")] + Static(NonNull<()>), + #[cfg(feature = "dynamic_handle")] + Dynamic(std::sync::Weak), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + Unreachable, } -// Safety: as with `TurboTasksHandle`, the concrete weak pointer's data -// is `Send + Sync` for any `T: Send + Sync`. +impl std::fmt::Debug for TurboTasksWeakHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + #[cfg(feature = "static_handle")] + WeakInner::Static(ptr) => f + .debug_tuple("TurboTasksWeakHandle::Static") + .field(ptr) + .finish(), + #[cfg(feature = "dynamic_handle")] + WeakInner::Dynamic(_) => f.debug_tuple("TurboTasksWeakHandle::Dynamic").finish(), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + WeakInner::Unreachable => f.write_str("TurboTasksWeakHandle::Unreachable"), + } + } +} + +// Safety: same reasoning as for `TurboTasksHandle`. unsafe impl Send for TurboTasksWeakHandle {} unsafe impl Sync for TurboTasksWeakHandle {} @@ -596,36 +617,37 @@ impl TurboTasksWeakHandle { /// concrete handle has been dropped. #[inline] pub fn upgrade(&self) -> Option { - let strong_ptr = match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_upgrade(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_upgrade(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), - }; - let strong_ptr = NonNull::new(strong_ptr as *mut ())?; - // Safety: the provider returned a non-null `Arc::into_raw` pointer - // for the concrete type indicated by `self.tag`. Ownership of one - // strong refcount transfers in. - Some(unsafe { TurboTasksHandle::from_raw_parts(self.tag, strong_ptr) }) + match &self.0 { + #[cfg(feature = "static_handle")] + WeakInner::Static(ptr) => { + let strong = unsafe { __tt_static_upgrade(ptr.as_ptr()) }; + let strong = NonNull::new(strong as *mut ())?; + // SAFETY: provider returned a valid `Arc::into_raw` + // pointer for the static concrete type, transferring one + // strong refcount. + Some(unsafe { TurboTasksHandle::from_static_raw(strong) }) + } + #[cfg(feature = "dynamic_handle")] + WeakInner::Dynamic(weak) => weak.upgrade().map(TurboTasksHandle::from_dynamic), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + WeakInner::Unreachable => unreachable!(), + } } } impl Clone for TurboTasksWeakHandle { #[inline] fn clone(&self) -> Self { - match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_clone_weak(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_clone_weak(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), - } - Self { - tag: self.tag, - ptr: self.ptr, + match &self.0 { + #[cfg(feature = "static_handle")] + WeakInner::Static(ptr) => { + unsafe { __tt_static_clone_weak(ptr.as_ptr()) } + Self(WeakInner::Static(*ptr)) + } + #[cfg(feature = "dynamic_handle")] + WeakInner::Dynamic(weak) => Self(WeakInner::Dynamic(weak.clone())), + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + WeakInner::Unreachable => unreachable!(), } } } @@ -633,13 +655,13 @@ impl Clone for TurboTasksWeakHandle { impl Drop for TurboTasksWeakHandle { #[inline] fn drop(&mut self) { - match self.tag { - #[cfg(feature = "prod_handle")] - HandleTag::Prod => unsafe { __tt_prod_drop_weak(self.ptr.as_ptr()) }, - #[cfg(feature = "test_handle")] - HandleTag::Test => unsafe { __tt_test_drop_weak(self.ptr.as_ptr()) }, - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - HandleTag::Unreachable => unreachable!(), + match &self.0 { + #[cfg(feature = "static_handle")] + WeakInner::Static(ptr) => unsafe { __tt_static_drop_weak(ptr.as_ptr()) }, + #[cfg(feature = "dynamic_handle")] + WeakInner::Dynamic(_) => {} + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + WeakInner::Unreachable => {} } } } diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index ee9415fef1cb..0ec3c278e9e8 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -83,7 +83,7 @@ pub use crate::{ }, effect::{Effect, EffectError, EffectStateStorage, Effects, emit_effect, take_effects}, error::PrettyPrintError, - handle::{HandleTag, TurboTasksHandle, TurboTasksWeakHandle}, + handle::{TurboTasksHandle, TurboTasksWeakHandle}, id::{ExecutionId, LocalTaskId, TRANSIENT_TASK_BIT, TaskId, TraitTypeId, ValueTypeId}, invalidation::{ InvalidationReason, InvalidationReasonKind, InvalidationReasonSet, Invalidator, diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 98271fe3887b..e83dd6dba967 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -599,36 +599,21 @@ impl TurboTasks { /// Consumes one strong refcount from the given `Arc`; the handle /// will drop that refcount when itself dropped. /// - /// When the `prod_handle` feature is active, the returned handle - /// dispatches into the `__tt_prod_*` providers (in - /// `turbo-tasks-backend`). When neither provider feature is active, - /// this still compiles so consumers that merely declare value types - /// via `turbo-tasks` macros build, but any dispatch through the - /// returned handle will `unreachable!()` at runtime. + /// The handle is only *meaningfully* usable when the `static_handle` + /// feature is active on `turbo-tasks` (activated by + /// `turbo-tasks-backend`). Without that feature, calling any + /// dispatch method on the returned handle panics — but the body + /// stays defined so that other inherent methods on `TurboTasks` + /// (executor impls, foreground/background scheduling) compile in + /// builds that don't link a backend. pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { - // Pick the right tag for the current build: - // * `prod_handle` active (with or without `test_handle`) → Prod - // * neither active → Unreachable - // A `test_handle`-only build doesn't make sense here (`TurboTasks` - // is the prod handle's concrete type) and is not supported. - #[cfg(feature = "prod_handle")] - let tag = crate::HandleTag::Prod; - #[cfg(not(any(feature = "prod_handle", feature = "test_handle")))] - let tag = crate::HandleTag::Unreachable; - #[cfg(all(feature = "test_handle", not(feature = "prod_handle")))] - compile_error!( - "TurboTasks::make_handle is unreachable without the `prod_handle` feature active on \ - `turbo-tasks`. A `test_handle`-only build of `turbo-tasks` that still constructs a \ - `TurboTasks` is not supported." - ); let ptr = Arc::into_raw(self) as *mut (); // Safety: `ptr` came from `Arc::into_raw` on a `TurboTasks`, - // which the `__tt_prod_*` providers (in `turbo-tasks-backend`) - // know how to cast back to. Tag is consistent with the prod arm - // by definition. - unsafe { - crate::TurboTasksHandle::from_raw_parts(tag, std::ptr::NonNull::new_unchecked(ptr)) - } + // which the `__tt_static_*` providers (in `turbo-tasks-backend`) + // know how to cast back to. When `static_handle` is off, + // `from_static_raw` panics; that's fine because none of the code + // paths that call us actually run without a backend linked. + unsafe { crate::TurboTasksHandle::from_static_raw(std::ptr::NonNull::new_unchecked(ptr)) } } /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. From 57ee062bbeab57e4d674a2e4cf88062676992eb7 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Tue, 19 May 2026 08:57:33 -0700 Subject: [PATCH 10/19] Match test/bench TurboTasks construction to ProdHandleConcrete type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `__tt_static_*` providers in turbo-tasks-backend cast the opaque `*const ()` receiver to `&TurboTasks>>`. Tests and benches were constructing `TurboTasks::new(TurboTasksBackend::new(_, TurboBackingStorage))` (no Either wrapper), so the resulting `Arc>` had a different concrete type than what the providers expected — reinterpreting the pointer was undefined behavior and segfaulted under optimization. Adds `ProdBackingStorage` (the Either-wrapped type) and small `prod_backing_storage_turbo` / `prod_backing_storage_noop` helpers in turbo-tasks-backend, then updates every test_config.trs and the three bench harnesses to use them. Also gates the backend's own tests/benches to activate the `static_handle` feature on themselves via a dev-dep self-reference, so `TurboTasksHandle::from_static_raw` is wired up to the actual extern providers instead of the no-feature unreachable! stub. --- Cargo.lock | 1 + .../crates/turbo-tasks-backend/Cargo.toml | 6 ++++ .../turbo-tasks-backend/benches/overhead.rs | 8 +++-- .../benches/scope_stress.rs | 7 +++-- .../turbo-tasks-backend/benches/stress.rs | 7 +++-- .../src/handle_providers.rs | 19 ++++++------ .../crates/turbo-tasks-backend/src/lib.rs | 30 +++++++++++++++++++ .../turbo-tasks-backend/tests/test_config.trs | 9 ++++-- .../turbo-tasks-fetch/tests/test_config.trs | 7 +++-- .../tests/test_config.trs | 5 +++- .../turbopack-analyze/tests/test_config.trs | 7 +++-- .../turbopack-node/tests/test_config.trs | 7 +++-- 12 files changed, 88 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f9d1c36ea58..b07f1c85fad7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9892,6 +9892,7 @@ dependencies = [ "turbo-persistence", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-hash", "turbo-tasks-malloc", "turbo-tasks-testing", diff --git a/turbopack/crates/turbo-tasks-backend/Cargo.toml b/turbopack/crates/turbo-tasks-backend/Cargo.toml index 339a5083092d..6b61d91b5bc4 100644 --- a/turbopack/crates/turbo-tasks-backend/Cargo.toml +++ b/turbopack/crates/turbo-tasks-backend/Cargo.toml @@ -87,6 +87,12 @@ triomphe = { workspace = true } turbo-tasks-malloc = { workspace = true , features = ["custom_allocator"]} rstest = { workspace = true } turbo-tasks-testing = { workspace = true } +# Activate `static_handle` for this crate's own tests and benches. +# `TurboTasksHandle` dispatch through the dynamic arm would panic on the +# unreachable! arms in `VcStorage` (the test arm only implements the +# subset of `TurboTasksApi` that pure-storage tests need), so backend +# tests / benches that drive real work need the static arm wired up. +turbo-tasks-backend = { workspace = true, features = ["static_handle"] } [[bench]] diff --git a/turbopack/crates/turbo-tasks-backend/benches/overhead.rs b/turbopack/crates/turbo-tasks-backend/benches/overhead.rs index 0072c4f677cb..184c76a47ba0 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/overhead.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/overhead.rs @@ -4,7 +4,9 @@ use criterion::{BenchmarkId, Criterion, black_box}; use futures::{FutureExt, StreamExt, stream::FuturesUnordered}; use tokio::spawn; use turbo_tasks::{TurboTasks, unmark_top_level_task_may_leak_eventually_consistent_state}; -use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; +use turbo_tasks_backend::{ + BackendOptions, TurboTasksBackend, noop_backing_storage, prod_backing_storage_noop, +}; #[global_allocator] static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc; @@ -178,7 +180,9 @@ fn run_turbo( storage_mode: None, ..Default::default() }, - noop_backing_storage(), + // Wrapped to match `ProdBackingStorage`, the concrete type + // the `__tt_static_*` providers cast to. + prod_backing_storage_noop(noop_backing_storage()), )); async move { diff --git a/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs b/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs index 4487e431afa5..a40f8b4b5759 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs @@ -1,7 +1,9 @@ use anyhow::Result; use criterion::{BenchmarkId, Criterion}; use turbo_tasks::{Completion, TryJoinIterExt, TurboTasks, Vc}; -use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; +use turbo_tasks_backend::{ + BackendOptions, TurboTasksBackend, noop_backing_storage, prod_backing_storage_noop, +}; pub fn scope_stress(c: &mut Criterion) { if matches!( @@ -38,7 +40,8 @@ pub fn scope_stress(c: &mut Criterion) { storage_mode: None, ..Default::default() }, - noop_backing_storage(), + // Wrap to match `ProdBackingStorage`. + prod_backing_storage_noop(noop_backing_storage()), )); async move { (0..size) diff --git a/turbopack/crates/turbo-tasks-backend/benches/stress.rs b/turbopack/crates/turbo-tasks-backend/benches/stress.rs index 2e2cbf01f05f..1c6298850f4e 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/stress.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/stress.rs @@ -3,7 +3,9 @@ use criterion::{BenchmarkId, Criterion}; use turbo_tasks::{ TryJoinIterExt, TurboTasks, Vc, unmark_top_level_task_may_leak_eventually_consistent_state, }; -use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; +use turbo_tasks_backend::{ + BackendOptions, TurboTasksBackend, noop_backing_storage, prod_backing_storage_noop, +}; pub fn fibonacci(c: &mut Criterion) { if matches!( @@ -36,7 +38,8 @@ pub fn fibonacci(c: &mut Criterion) { storage_mode: None, ..Default::default() }, - noop_backing_storage(), + // Wrap to match `ProdBackingStorage`. + prod_backing_storage_noop(noop_backing_storage()), )); async move { tt.run(async move { diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 27172107e66f..20e7d363eff7 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -18,19 +18,18 @@ use std::sync::Arc; -use either::Either; use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; -use crate::{NoopBackingStorage, TurboBackingStorage, TurboTasksBackend}; +use crate::{ProdBackingStorage, TurboTasksBackend}; -/// The concrete prod handle type — matches what `next-napi-bindings` uses -/// (one of `Either`). -/// -/// If a future binary needs a different `Backend`/storage combination, -/// this provider crate would need a parallel module. Today there is -/// exactly one prod handle type so we hardcode it. -pub type ProdHandleConcrete = - turbo_tasks::TurboTasks>>; +/// The concrete prod handle type. The `__tt_static_*` providers below +/// cast each opaque `*const ()` receiver to `&ProdHandleConcrete`, so +/// any `Arc>>` that goes into a +/// [`turbo_tasks::TurboTasksHandle`] via `make_handle` MUST be this +/// exact type. Mismatch is undefined behavior. See +/// [`crate::ProdBackingStorage`] for why the storage type is wrapped in +/// `Either`. +pub type ProdHandleConcrete = turbo_tasks::TurboTasks>; /// Generates `#[no_mangle] pub extern "Rust" fn __tt_static_(...)` /// for a single dispatched method, dispatched via method call syntax. diff --git a/turbopack/crates/turbo-tasks-backend/src/lib.rs b/turbopack/crates/turbo-tasks-backend/src/lib.rs index 801857c893e8..43a0f0ea3a69 100644 --- a/turbopack/crates/turbo-tasks-backend/src/lib.rs +++ b/turbopack/crates/turbo-tasks-backend/src/lib.rs @@ -31,6 +31,36 @@ pub use crate::{ pub type TurboBackingStorage = KeyValueDatabaseBackingStorage; +/// Concrete `BackingStorage` type accepted by the prod dispatch arm. The +/// `__tt_static_*` providers in [`handle_providers`] are monomorphized +/// for `TurboTasks>`; any caller +/// that wants its `TurboTasks::new(...)` instance to be drivable through +/// `TurboTasksHandle` must produce this exact type. The `Either` lets +/// the same handle type cover both the real on-disk cache and the noop +/// in-memory variant used by tests and the napi cdylib's `no-cache` +/// mode. +pub type ProdBackingStorage = either::Either; + +// Re-exported so consumers (test config files, the napi binding) can +// build a `ProdBackingStorage` without needing a direct dep on `either`. +pub use either::Either; + +/// Wraps a [`TurboBackingStorage`] into [`ProdBackingStorage`] so callers +/// can hand the result to [`TurboTasksBackend::new`] and end up with the +/// concrete `TurboTasks>` type the +/// `__tt_static_*` dispatch providers cast to. Equivalent to writing +/// `Either::Left(storage)` but doesn't require turbofish for type +/// inference at the call site. +pub fn prod_backing_storage_turbo(storage: TurboBackingStorage) -> ProdBackingStorage { + Either::Left(storage) +} + +/// Wraps a [`NoopBackingStorage`] into [`ProdBackingStorage`]. See +/// [`prod_backing_storage_turbo`] for the rationale. +pub fn prod_backing_storage_noop(storage: NoopBackingStorage) -> ProdBackingStorage { + Either::Right(storage) +} + /// Creates a `BackingStorage` to be passed to [`TurboTasksBackend::new`]. /// /// Information about the state of the on-disk cache is returned using [`StartupCacheState`]. diff --git a/turbopack/crates/turbo-tasks-backend/tests/test_config.trs b/turbopack/crates/turbo-tasks-backend/tests/test_config.trs index 6ba3540fa199..714bcfe0e9e6 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/test_config.trs +++ b/turbopack/crates/turbo-tasks-backend/tests/test_config.trs @@ -14,7 +14,12 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - turbo_tasks_backend::turbo_backing_storage( + // Wrapped via `prod_backing_storage_turbo` to match + // `ProdBackingStorage` — the concrete type the `__tt_static_*` + // providers are monomorphized for. A bare `TurboBackingStorage` + // would produce a different `TurboTasks>` + // and the providers' pointer cast would be UB at runtime. + turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -23,7 +28,7 @@ false, true, false, - ).unwrap().0 + ).unwrap().0) ) ) } diff --git a/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs b/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs index e181c52b334a..7e3158b0e60b 100644 --- a/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs +++ b/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs @@ -13,7 +13,10 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - turbo_tasks_backend::turbo_backing_storage( + // Wrapped via `prod_backing_storage_turbo` to match + // `ProdBackingStorage` — see the rationale in + // `turbo-tasks-backend/tests/test_config.trs`. + turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -22,7 +25,7 @@ false, true, false, - ).unwrap().0 + ).unwrap().0) ) ) } diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs b/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs index 52ab7168aa4a..ac52e40b71a4 100644 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs +++ b/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs @@ -7,7 +7,10 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - turbo_tasks_backend::noop_backing_storage(), + // Wrapped via `prod_backing_storage_noop` to match + // `ProdBackingStorage` — see the rationale in + // `turbo-tasks-backend/tests/test_config.trs`. + turbo_tasks_backend::prod_backing_storage_noop(turbo_tasks_backend::noop_backing_storage()), ) ) } diff --git a/turbopack/crates/turbopack-analyze/tests/test_config.trs b/turbopack/crates/turbopack-analyze/tests/test_config.trs index e181c52b334a..7e3158b0e60b 100644 --- a/turbopack/crates/turbopack-analyze/tests/test_config.trs +++ b/turbopack/crates/turbopack-analyze/tests/test_config.trs @@ -13,7 +13,10 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - turbo_tasks_backend::turbo_backing_storage( + // Wrapped via `prod_backing_storage_turbo` to match + // `ProdBackingStorage` — see the rationale in + // `turbo-tasks-backend/tests/test_config.trs`. + turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -22,7 +25,7 @@ false, true, false, - ).unwrap().0 + ).unwrap().0) ) ) } diff --git a/turbopack/crates/turbopack-node/tests/test_config.trs b/turbopack/crates/turbopack-node/tests/test_config.trs index 6ba3540fa199..20826e13d1bd 100644 --- a/turbopack/crates/turbopack-node/tests/test_config.trs +++ b/turbopack/crates/turbopack-node/tests/test_config.trs @@ -14,7 +14,10 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - turbo_tasks_backend::turbo_backing_storage( + // Wrapped via `prod_backing_storage_turbo` to match + // `ProdBackingStorage` — see the rationale in + // `turbo-tasks-backend/tests/test_config.trs`. + turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -23,7 +26,7 @@ false, true, false, - ).unwrap().0 + ).unwrap().0) ) ) } From e315f81b9c8b7ae94d417f1a5d1ab3687293f3c5 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Tue, 19 May 2026 11:35:58 -0700 Subject: [PATCH 11/19] Fix `&*turbo_tasks::turbo_tasks()` in turbo-tasks-fetch; remove backend self-dev-dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `turbo_tasks()` returns a `TurboTasksHandle` by value, not a smart pointer that can be deref'd. The new caller in client.rs needs `&turbo_tasks()` not `&*turbo_tasks()` — bind to a local first so the temporary's drop order is explicit. Also reverts the self-dev-dep on turbo-tasks-backend that activated `static_handle` for its own tests/benches. That activation propagated across the workspace via Cargo feature unification and broke test bins that don't directly link `turbo-tasks-backend` (unresolved `__tt_static_*` extern symbols). Backend tests now panic at runtime without `static_handle`; will reactivate via per-crate dev-deps in a follow-up so only test bins that actually link the backend turn it on. --- Cargo.lock | 1 - turbopack/crates/turbo-tasks-backend/Cargo.toml | 6 ------ turbopack/crates/turbo-tasks-fetch/src/client.rs | 5 +++-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b07f1c85fad7..3f9d1c36ea58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9892,7 +9892,6 @@ dependencies = [ "turbo-persistence", "turbo-rcstr", "turbo-tasks", - "turbo-tasks-backend", "turbo-tasks-hash", "turbo-tasks-malloc", "turbo-tasks-testing", diff --git a/turbopack/crates/turbo-tasks-backend/Cargo.toml b/turbopack/crates/turbo-tasks-backend/Cargo.toml index 6b61d91b5bc4..339a5083092d 100644 --- a/turbopack/crates/turbo-tasks-backend/Cargo.toml +++ b/turbopack/crates/turbo-tasks-backend/Cargo.toml @@ -87,12 +87,6 @@ triomphe = { workspace = true } turbo-tasks-malloc = { workspace = true , features = ["custom_allocator"]} rstest = { workspace = true } turbo-tasks-testing = { workspace = true } -# Activate `static_handle` for this crate's own tests and benches. -# `TurboTasksHandle` dispatch through the dynamic arm would panic on the -# unreachable! arms in `VcStorage` (the test arm only implements the -# subset of `TurboTasksApi` that pure-storage tests need), so backend -# tests / benches that drive real work need the static arm wired up. -turbo-tasks-backend = { workspace = true, features = ["static_handle"] } [[bench]] diff --git a/turbopack/crates/turbo-tasks-fetch/src/client.rs b/turbopack/crates/turbo-tasks-fetch/src/client.rs index 2c36279a52e4..06f61107682e 100644 --- a/turbopack/crates/turbo-tasks-fetch/src/client.rs +++ b/turbopack/crates/turbo-tasks-fetch/src/client.rs @@ -264,7 +264,8 @@ impl FetchClientConfig { // // Skip when dependency tracking is disabled (e.g. one-shot `next build`) since // invalidation panics without dependency tracking and the timer would be wasted work. - if turbo_tasks::turbo_tasks().is_tracking_dependencies() + let tt = turbo_tasks::turbo_tasks(); + if tt.is_tracking_dependencies() && let (Some(deadline_secs), Some(invalidator)) = (deadline_secs, invalidator) { // transform absolute deadline back to a relative duration for the sleep call @@ -285,7 +286,7 @@ impl FetchClientConfig { // FetchClientConfig to track outstanding timers and cancel them. turbo_tasks::spawn(async move { tokio::time::sleep(remaining).await; - invalidator.invalidate_with_reason(&turbo_tasks::turbo_tasks(), HttpTimeout {}); + invalidator.invalidate_with_reason(&tt, HttpTimeout {}); }); } } From 9bbc76cbf139e2d86a32aedb262c6d7d6931640b Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Tue, 19 May 2026 12:34:25 -0700 Subject: [PATCH 12/19] Fall back to dynamic dispatch in `make_handle` when static_handle is off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the bench-job panic without forcing `static_handle` on workspace-wide (which would spread `__tt_static_*` extern decls into test bins that don't link `turbo-tasks-backend`). When `static_handle` is on (napi binding), `make_handle` returns the Static handle that dispatches through `extern "Rust"` symbols — devirtualized under thin LTO, as before. When `static_handle` is off but `dynamic_handle` is on (workspace tests/benches, which transitively pull in `turbo-tasks-testing`), `make_handle` upcasts the `Arc>` to `Arc` and returns a Dynamic handle. One vtable indirection per dispatched call — slower than static, but correct, and only the test/bench path pays it. The third arm (neither feature on) is a runtime panic stub so the generic `impl TurboTasks` block still compiles for crates that only depend on `turbo-tasks` for types. --- turbopack/crates/turbo-tasks/src/manager.rs | 46 ++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index e83dd6dba967..6d2a275cc524 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -599,23 +599,49 @@ impl TurboTasks { /// Consumes one strong refcount from the given `Arc`; the handle /// will drop that refcount when itself dropped. /// - /// The handle is only *meaningfully* usable when the `static_handle` - /// feature is active on `turbo-tasks` (activated by - /// `turbo-tasks-backend`). Without that feature, calling any - /// dispatch method on the returned handle panics — but the body - /// stays defined so that other inherent methods on `TurboTasks` - /// (executor impls, foreground/background scheduling) compile in - /// builds that don't link a backend. + /// When the `static_handle` feature is active on `turbo-tasks` (set + /// by binaries that link `turbo-tasks-backend` with its + /// `static_handle` feature on — i.e. the napi binding), this builds + /// a `Static` handle that dispatches through `extern "Rust"` + /// providers — fully devirtualized under thin LTO. + /// + /// Otherwise (typical workspace test/bench builds, where backend's + /// `static_handle` is off to avoid spreading the externs through + /// feature unification), this falls back to a `Dynamic` handle — + /// `Arc` vtable dispatch. The fallback works + /// because `TurboTasks` itself implements `TurboTasksApi`. Slower + /// (one vtable indirection per dispatched call) but correct. + #[cfg(feature = "static_handle")] pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { let ptr = Arc::into_raw(self) as *mut (); // Safety: `ptr` came from `Arc::into_raw` on a `TurboTasks`, // which the `__tt_static_*` providers (in `turbo-tasks-backend`) - // know how to cast back to. When `static_handle` is off, - // `from_static_raw` panics; that's fine because none of the code - // paths that call us actually run without a backend linked. + // know how to cast back to. unsafe { crate::TurboTasksHandle::from_static_raw(std::ptr::NonNull::new_unchecked(ptr)) } } + /// Fallback dynamic-dispatch variant of `make_handle` when the + /// `static_handle` feature is off. Requires the `dynamic_handle` + /// feature on `turbo-tasks` (activated by `turbo-tasks-testing`, + /// which everything that uses `TurboTasks` in test mode pulls in + /// transitively). + #[cfg(all(not(feature = "static_handle"), feature = "dynamic_handle"))] + pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { + crate::TurboTasksHandle::from_dynamic(self as Arc) + } + + /// Stub for `make_handle` when neither dispatch feature is on. Lets + /// the `impl TurboTasks` block compile for crates that link + /// `turbo-tasks` for types alone (no test/bench harness involved). + /// Calling it at runtime panics. + #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] + pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { + unreachable!( + "TurboTasks::make_handle requires either the `static_handle` or `dynamic_handle` \ + feature on `turbo-tasks`" + ) + } + /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. /// Helper that clones the internal `Arc` first; equivalent to /// `self.pin().make_handle()`. From 4c17037edfc235b94c4c7edbcd83eaac8278b3f9 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Tue, 19 May 2026 23:54:02 -0700 Subject: [PATCH 13/19] Drop static_handle/dynamic_handle features, make dispatch always-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes both the `static_handle` (on `turbo-tasks`) and `dynamic_handle` (now dead since VcStorage was deleted) Cargo features. `TurboTasksHandle` is now a single-variant `NonNull<()>`-wrapping type that always dispatches through `extern "Rust"` symbols defined in `turbo-tasks-backend`. This means every binary that links `libturbo_tasks.rlib` must also link `libturbo_tasks_backend.rlib`. Most workspace crates that depend on `turbo-tasks` for runtime use already do. For type-declaring leaf crates (`turbo-tasks-bytes`, `turbo-esregex`, `turbopack-css`, etc.) whose lib-test binaries link the rlib but otherwise have no need for the backend, the convention is a `[dev-dependencies] turbo-tasks-backend = { workspace = true }` plus `#[cfg(test)] extern crate turbo_tasks_backend;` in `src/lib.rs` — cargo won't include the rlib in the link command unless Rust code references it. A follow-up could replace the unconditional extern decls with weak symbols (`#[linkage = "extern_weak"]`), letting the dispatch site resolve to a null function pointer for crates that never actually construct a `TurboTasks`. Under thin LTO the resulting null-check should constant-fold away in callers that do link the backend. Not done here because it would require nightly-only `linkage` attribute support and `Option` plumbing. --- Cargo.lock | 22 ++ crates/next-build/Cargo.toml | 6 + crates/next-build/src/lib.rs | 5 + crates/next-core/Cargo.toml | 6 + crates/next-core/src/lib.rs | 5 + crates/next-napi-bindings/Cargo.toml | 2 +- turbopack/crates/turbo-esregex/Cargo.toml | 8 + turbopack/crates/turbo-esregex/src/lib.rs | 6 + .../crates/turbo-tasks-backend/Cargo.toml | 13 - .../src/handle_providers.rs | 12 +- .../crates/turbo-tasks-backend/src/lib.rs | 1 - turbopack/crates/turbo-tasks-bytes/Cargo.toml | 7 + turbopack/crates/turbo-tasks-bytes/src/lib.rs | 5 + turbopack/crates/turbo-tasks-env/Cargo.toml | 6 + turbopack/crates/turbo-tasks-env/src/lib.rs | 5 + turbopack/crates/turbo-tasks-fetch/src/lib.rs | 6 + .../crates/turbo-tasks-fs/benches/mod.rs | 4 + .../crates/turbo-tasks-testing/Cargo.toml | 5 +- turbopack/crates/turbo-tasks/Cargo.toml | 28 +- turbopack/crates/turbo-tasks/benches/mod.rs | 4 + turbopack/crates/turbo-tasks/src/handle.rs | 355 +++++------------- turbopack/crates/turbo-tasks/src/lib.rs | 7 + turbopack/crates/turbo-tasks/src/manager.rs | 36 -- turbopack/crates/turbopack-analyze/src/lib.rs | 6 + turbopack/crates/turbopack-bench/Cargo.toml | 6 + .../crates/turbopack-bench/benches/mod.rs | 4 + turbopack/crates/turbopack-bench/src/lib.rs | 5 + turbopack/crates/turbopack-browser/Cargo.toml | 6 + turbopack/crates/turbopack-browser/src/lib.rs | 5 + .../crates/turbopack-cli-utils/Cargo.toml | 6 + .../crates/turbopack-cli-utils/src/lib.rs | 5 + turbopack/crates/turbopack-cli/benches/mod.rs | 4 + turbopack/crates/turbopack-css/Cargo.toml | 6 + turbopack/crates/turbopack-css/src/lib.rs | 5 + .../Cargo.toml | 6 + .../src/lib.rs | 5 + .../turbopack-ecmascript-plugins/Cargo.toml | 6 + .../turbopack-ecmascript-plugins/src/lib.rs | 5 + .../turbopack-ecmascript-runtime/Cargo.toml | 6 + .../turbopack-ecmascript-runtime/src/lib.rs | 5 + turbopack/crates/turbopack-env/Cargo.toml | 6 + turbopack/crates/turbopack-env/src/lib.rs | 5 + turbopack/crates/turbopack-image/Cargo.toml | 6 + turbopack/crates/turbopack-image/src/lib.rs | 5 + turbopack/crates/turbopack-mdx/Cargo.toml | 6 + turbopack/crates/turbopack-mdx/src/lib.rs | 5 + turbopack/crates/turbopack-nft/src/lib.rs | 6 + turbopack/crates/turbopack-node/src/lib.rs | 6 + turbopack/crates/turbopack-nodejs/Cargo.toml | 6 + turbopack/crates/turbopack-nodejs/src/lib.rs | 5 + turbopack/crates/turbopack-resolve/Cargo.toml | 6 + turbopack/crates/turbopack-resolve/src/lib.rs | 5 + turbopack/crates/turbopack-static/Cargo.toml | 6 + turbopack/crates/turbopack-static/src/lib.rs | 5 + .../crates/turbopack-swc-utils/Cargo.toml | 6 + .../crates/turbopack-swc-utils/src/lib.rs | 5 + .../crates/turbopack-test-utils/Cargo.toml | 6 + .../crates/turbopack-test-utils/src/lib.rs | 5 + turbopack/crates/turbopack-wasm/Cargo.toml | 6 + turbopack/crates/turbopack-wasm/src/lib.rs | 5 + 60 files changed, 406 insertions(+), 350 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f9d1c36ea58..0d4d1f125481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4765,6 +4765,7 @@ version = "0.1.0" dependencies = [ "next-core", "turbo-rcstr", + "turbo-tasks-backend", "turbopack-core", ] @@ -4847,6 +4848,7 @@ dependencies = [ "turbo-esregex", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-bytes", "turbo-tasks-env", "turbo-tasks-fetch", @@ -9735,6 +9737,7 @@ dependencies = [ "regex", "regress", "turbo-tasks", + "turbo-tasks-backend", ] [[package]] @@ -9850,6 +9853,7 @@ dependencies = [ "turbo-dyn-eq-hash", "turbo-frozenmap", "turbo-rcstr", + "turbo-tasks-backend", "turbo-tasks-hash", "turbo-tasks-macros", "turbo-tasks-malloc", @@ -9923,6 +9927,7 @@ dependencies = [ "futures", "serde", "turbo-tasks", + "turbo-tasks-backend", ] [[package]] @@ -9934,6 +9939,7 @@ dependencies = [ "turbo-bincode", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", ] @@ -10161,6 +10167,7 @@ dependencies = [ "tokio", "tungstenite 0.20.1", "turbo-tasks", + "turbo-tasks-backend", "turbopack-create-test-app", "url", ] @@ -10181,6 +10188,7 @@ dependencies = [ "turbo-bincode", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbo-tasks-hash", "turbopack", @@ -10247,6 +10255,7 @@ dependencies = [ "serde", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", "turbopack-resolve", @@ -10332,6 +10341,7 @@ dependencies = [ "turbo-bincode", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", "turbopack-ecmascript", @@ -10433,6 +10443,7 @@ dependencies = [ "serde", "serde_json", "turbo-rcstr", + "turbo-tasks-backend", "turbopack-cli-utils", "turbopack-core", ] @@ -10458,6 +10469,7 @@ dependencies = [ "tracing", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", "turbopack-ecmascript", @@ -10472,6 +10484,7 @@ dependencies = [ "serde", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", "turbopack-ecmascript", @@ -10485,6 +10498,7 @@ dependencies = [ "async-trait", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-env", "turbo-tasks-fs", "turbopack-core", @@ -10507,6 +10521,7 @@ dependencies = [ "turbo-bincode", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", ] @@ -10522,6 +10537,7 @@ dependencies = [ "serde", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", "turbopack-ecmascript", @@ -10611,6 +10627,7 @@ dependencies = [ "turbo-bincode", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbo-tasks-hash", "turbopack", @@ -10634,6 +10651,7 @@ dependencies = [ "tracing", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbopack-core", ] @@ -10645,6 +10663,7 @@ dependencies = [ "anyhow", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbo-tasks-hash", "turbopack-core", @@ -10661,6 +10680,7 @@ dependencies = [ "swc_core", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbopack-core", ] @@ -10676,6 +10696,7 @@ dependencies = [ "similar", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbo-tasks-hash", "turbopack-cli-utils", @@ -10795,6 +10816,7 @@ dependencies = [ "serde", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-fs", "turbo-tasks-hash", "turbopack-core", diff --git a/crates/next-build/Cargo.toml b/crates/next-build/Cargo.toml index b8fccb635dd8..7ae730886b35 100644 --- a/crates/next-build/Cargo.toml +++ b/crates/next-build/Cargo.toml @@ -17,3 +17,9 @@ next-core = { workspace = true } turbopack-core = { workspace = true } turbo-rcstr = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/crates/next-build/src/lib.rs b/crates/next-build/src/lib.rs index a324accba792..81f35c5769c7 100644 --- a/crates/next-build/src/lib.rs +++ b/crates/next-build/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod build_options; pub use self::build_options::BuildOptions; diff --git a/crates/next-core/Cargo.toml b/crates/next-core/Cargo.toml index 26cad2622132..fb8327363558 100644 --- a/crates/next-core/Cargo.toml +++ b/crates/next-core/Cargo.toml @@ -87,6 +87,12 @@ turbopack-resolve = { workspace = true } turbopack-static = { workspace = true } turbopack-trace-utils = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + [features] default = ["process_pool"] process_pool = ["turbopack-node/process_pool"] diff --git a/crates/next-core/src/lib.rs b/crates/next-core/src/lib.rs index a157fe7521b4..3d3111296313 100644 --- a/crates/next-core/src/lib.rs +++ b/crates/next-core/src/lib.rs @@ -2,6 +2,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + mod app_page_loader_tree; pub mod app_structure; mod base_loader_tree; diff --git a/crates/next-napi-bindings/Cargo.toml b/crates/next-napi-bindings/Cargo.toml index af34b8506341..5237da8bd97f 100644 --- a/crates/next-napi-bindings/Cargo.toml +++ b/crates/next-napi-bindings/Cargo.toml @@ -94,7 +94,7 @@ swc_plugin_backend_wasmtime = { workspace = true } tokio = { workspace = true, features = ["full"] } turbo-rcstr = { workspace = true, features = ["napi"] } turbo-tasks = { workspace = true } -turbo-tasks-backend = { workspace = true, features = ["static_handle"] } +turbo-tasks-backend = { workspace = true } turbo-tasks-fs = { workspace = true } turbo-unix-path = { workspace = true } next-api = { workspace = true, features = ["worker_pool"] } diff --git a/turbopack/crates/turbo-esregex/Cargo.toml b/turbopack/crates/turbo-esregex/Cargo.toml index 90dcff8d844e..a5b7d98fdd89 100644 --- a/turbopack/crates/turbo-esregex/Cargo.toml +++ b/turbopack/crates/turbo-esregex/Cargo.toml @@ -11,5 +11,13 @@ regex = { workspace = true } regress = { workspace = true } turbo-tasks = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols in `libturbo_tasks.rlib` resolve in this crate's test binary. +# Without something in this crate referencing `turbo-tasks-backend` from +# Rust, rustc wouldn't put its rlib in the link command. See the +# matching `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + [lints] workspace = true diff --git a/turbopack/crates/turbo-esregex/src/lib.rs b/turbopack/crates/turbo-esregex/src/lib.rs index 917ef282574a..71709134f6cd 100644 --- a/turbopack/crates/turbo-esregex/src/lib.rs +++ b/turbopack/crates/turbo-esregex/src/lib.rs @@ -1,5 +1,11 @@ #![feature(arbitrary_self_types_pointers)] +// Force the linker to pull in `turbo-tasks-backend`'s `__tt_static_*` +// providers for this crate's test binary. See the matching dev-dep in +// `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + use std::vec; use anyhow::{Result, bail}; diff --git a/turbopack/crates/turbo-tasks-backend/Cargo.toml b/turbopack/crates/turbo-tasks-backend/Cargo.toml index 339a5083092d..cc0e65db23d0 100644 --- a/turbopack/crates/turbo-tasks-backend/Cargo.toml +++ b/turbopack/crates/turbo-tasks-backend/Cargo.toml @@ -15,19 +15,6 @@ workspace = true [features] default = [] -# Emits `#[no_mangle] pub extern "Rust" fn __tt_static_*` providers for -# the prod dispatch arm of `TurboTasksHandle` (see -# `turbo_tasks::handle`). Activates `turbo-tasks/static_handle` so the -# declarations and providers line up. -# -# Only the binary that actually runs prod turbo-tasks code (the -# `next-napi-bindings` napi addon) should enable this. Activating -# `static_handle` workspace-wide via feature unification would cause every -# linked test binary to need to resolve the providers, but most don't -# directly link `turbo-tasks-backend`. Explicit opt-in keeps the -# linker-symbol contract a binary-by-binary concern. -static_handle = ["turbo-tasks/static_handle"] - print_cache_item_size_with_compressed = ["print_cache_item_size", "dep:lzzzz"] print_cache_item_size = [] no_fast_stale = [] diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 20e7d363eff7..94bbbe9665a9 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -1,10 +1,10 @@ //! `#[no_mangle] pub extern "Rust" fn __tt_static_*` providers for the -//! production arm of the `TurboTasksHandle` dispatch. +//! `TurboTasksHandle` dispatch. //! -//! The forward declarations live in `turbo_tasks::handle` and are gated by -//! the `static_handle` Cargo feature on `turbo-tasks`. `turbo-tasks-backend` -//! activates that feature in its dep entry, so these `#[no_mangle]` -//! symbols are linked into any binary that pulls in `turbo-tasks-backend`. +//! The forward declarations live in `turbo_tasks::handle`. Both sides +//! are unconditional — any binary that links `libturbo_tasks.rlib` +//! must also link `libturbo_tasks_backend.rlib` so the linker can +//! resolve the externs. //! //! Each provider: //! 1. Casts the opaque `*const ()` receiver back to `&ProdHandleConcrete` (the production handle @@ -12,7 +12,7 @@ //! 2. Calls the trait method on the concrete type. //! //! Under thin LTO + `codegen-units = 1`, every step inlines into the -//! caller and the dispatch shape is `match tag => direct call` with no +//! caller and the dispatch shape is `direct call` with no //! indirect calls. See `turbo_tasks::handle` for the experiment that //! verified this. diff --git a/turbopack/crates/turbo-tasks-backend/src/lib.rs b/turbopack/crates/turbo-tasks-backend/src/lib.rs index 43a0f0ea3a69..b0d0ef7750a8 100644 --- a/turbopack/crates/turbo-tasks-backend/src/lib.rs +++ b/turbopack/crates/turbo-tasks-backend/src/lib.rs @@ -7,7 +7,6 @@ mod backing_storage; mod data; mod database; mod error; -#[cfg(feature = "static_handle")] mod handle_providers; mod kv_backing_storage; mod utils; diff --git a/turbopack/crates/turbo-tasks-bytes/Cargo.toml b/turbopack/crates/turbo-tasks-bytes/Cargo.toml index 2c92b6c8fcc5..e45052ad5a3d 100644 --- a/turbopack/crates/turbo-tasks-bytes/Cargo.toml +++ b/turbopack/crates/turbo-tasks-bytes/Cargo.toml @@ -18,3 +18,10 @@ bytes = { workspace = true } futures = { workspace = true } serde = { workspace = true } turbo-tasks = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols in `libturbo_tasks.rlib` resolve in this crate's test binary. +# See the matching `extern crate` in `src/lib.rs` and the explanation in +# `turbo-esregex/Cargo.toml`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbo-tasks-bytes/src/lib.rs b/turbopack/crates/turbo-tasks-bytes/src/lib.rs index 43b55018100a..1ef8b691df31 100644 --- a/turbopack/crates/turbo-tasks-bytes/src/lib.rs +++ b/turbopack/crates/turbo-tasks-bytes/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod bytes; pub mod stream; diff --git a/turbopack/crates/turbo-tasks-env/Cargo.toml b/turbopack/crates/turbo-tasks-env/Cargo.toml index 9d1ea1d677bf..15b383c91e76 100644 --- a/turbopack/crates/turbo-tasks-env/Cargo.toml +++ b/turbopack/crates/turbo-tasks-env/Cargo.toml @@ -19,3 +19,9 @@ turbo-rcstr = { workspace = true } turbo-tasks = { workspace = true } turbo-tasks-fs = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + diff --git a/turbopack/crates/turbo-tasks-env/src/lib.rs b/turbopack/crates/turbo-tasks-env/src/lib.rs index 54e941ef7c9c..28c0ef3e37bd 100644 --- a/turbopack/crates/turbo-tasks-env/src/lib.rs +++ b/turbopack/crates/turbo-tasks-env/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + mod command_line; mod custom; mod dotenv; diff --git a/turbopack/crates/turbo-tasks-fetch/src/lib.rs b/turbopack/crates/turbo-tasks-fetch/src/lib.rs index 8b037cf9c00e..988a3f25d010 100644 --- a/turbopack/crates/turbo-tasks-fetch/src/lib.rs +++ b/turbopack/crates/turbo-tasks-fetch/src/lib.rs @@ -2,6 +2,12 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary. The backend is a regular dep, but rustc +// only adds rlibs to the link command for crates Rust code references. +#[cfg(test)] +extern crate turbo_tasks_backend; + mod client; mod error; mod response; diff --git a/turbopack/crates/turbo-tasks-fs/benches/mod.rs b/turbopack/crates/turbo-tasks-fs/benches/mod.rs index 1ab8a9293bbb..0b8db7ab3543 100644 --- a/turbopack/crates/turbo-tasks-fs/benches/mod.rs +++ b/turbopack/crates/turbo-tasks-fs/benches/mod.rs @@ -1,3 +1,7 @@ +// Force linking the static-dispatch providers — see the matching dep +// declaration on `turbo-tasks-backend` in `Cargo.toml`. +extern crate turbo_tasks_backend; + use std::{ fs, sync::{Arc, mpsc::channel}, diff --git a/turbopack/crates/turbo-tasks-testing/Cargo.toml b/turbopack/crates/turbo-tasks-testing/Cargo.toml index 8790afbc55cb..9aa035b03a8f 100644 --- a/turbopack/crates/turbo-tasks-testing/Cargo.toml +++ b/turbopack/crates/turbo-tasks-testing/Cargo.toml @@ -19,5 +19,8 @@ futures = { workspace = true } rustc-hash = { workspace = true } smallvec = { workspace = true } tokio = { workspace = true } -turbo-tasks = { workspace = true, features = ["non_operation_vc_strongly_consistent", "dynamic_handle"] } +turbo-tasks = { workspace = true, features = ["non_operation_vc_strongly_consistent"] } +# The `test_instance` factory goes through `make_handle`, so binaries +# that depend on this crate transitively pull in the +# `turbo-tasks-backend` rlib for the `__tt_static_*` providers. turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbo-tasks/Cargo.toml b/turbopack/crates/turbo-tasks/Cargo.toml index f608126995a2..7beddaf865be 100644 --- a/turbopack/crates/turbo-tasks/Cargo.toml +++ b/turbopack/crates/turbo-tasks/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" description = "TBD" license = "MIT" edition = "2024" +# Disable auto-discovery of bench targets so `benches/scope.rs` is +# treated as a module of `benches/mod.rs` rather than a separate bench. +autobenches = false [lib] bench = false @@ -15,27 +18,6 @@ hanging_detection = [] task_id_details = [] verify_determinism = [] -# Adds the devirtualized `Prod` arm to `TurboTasksHandle`, dispatching -# via `#[no_mangle] pub extern "Rust" fn __tt_static_*` symbols defined in -# `turbo-tasks-backend`. Activated by `turbo-tasks-backend`'s dep on -# `turbo-tasks`. Only binaries whose link graph includes -# `turbo-tasks-backend` can resolve the externs, which is also exactly -# the situation where the prod arm is meaningful to call. -# -# Crates that don't link `turbo-tasks-backend` (the workspace has many — -# `turbo-esregex`, `turbo-tasks-fs`'s lib-tests, etc.) compile without -# this feature: `TurboTasksHandle` falls back to `Arc` -# vtable dispatch through the `Test` variant. Since those crates never -# actually run prod tasks, the perf delta is moot. -static_handle = [] - -# Adds the `Test` variant to `TurboTasksHandle`, which dispatches via -# `Arc` vtable. Activated by `turbo-tasks-testing`'s -# dep on `turbo-tasks`. The test arm uses normal Rust vtable dispatch, -# so there's no linker-symbol footgun if the feature unifies on without -# the providing crate being directly linked. -dynamic_handle = [] - # TODO(bgw): This feature is here to unblock turning on local tasks by default. It's only turned on # in unit tests. This will be removed very soon. non_operation_vc_strongly_consistent = [] @@ -82,6 +64,10 @@ unsize = { workspace = true } [dev-dependencies] criterion = { workspace = true, features = ["async_tokio"] } +# Circular dev-dep — Cargo allows it. Pulls in the `__tt_static_*` +# providers so this crate's test/bench binaries can link. See the +# matching `extern crate` in `src/lib.rs` and `benches/mod.rs`. +turbo-tasks-backend = { workspace = true } [[bench]] name = "mod" diff --git a/turbopack/crates/turbo-tasks/benches/mod.rs b/turbopack/crates/turbo-tasks/benches/mod.rs index 60e504c700d9..2da476d0c692 100644 --- a/turbopack/crates/turbo-tasks/benches/mod.rs +++ b/turbopack/crates/turbo-tasks/benches/mod.rs @@ -1,5 +1,9 @@ #![feature(arbitrary_self_types)] +// Force linking the static-dispatch providers — see the matching +// `extern crate` and dev-dep declaration on `turbo-tasks-backend`. +extern crate turbo_tasks_backend; + use criterion::{Criterion, criterion_group, criterion_main}; pub(crate) mod scope; diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index ec52293c8cfc..1f5f5d409884 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -2,134 +2,81 @@ //! //! The task-local that holds the current `TurboTasksApi` implementation //! has historically been an `Arc`. The `dyn` is -//! necessary because the prod implementor (`TurboTasks`) is generic over a -//! backend type that the `task_local!` macro cannot name — but the cost -//! is an indirect vtable call on every dispatched method, and rustc -//! currently does not emit the LLVM metadata that `WholeProgramDevirt` -//! needs to inline through trait objects ([rust#68262], [rust#45774]). +//! necessary because the prod implementor (`TurboTasks`) is generic +//! over a backend type that the `task_local!` macro cannot name — but +//! the cost is an indirect vtable call on every dispatched method, and +//! rustc currently does not emit the LLVM metadata that +//! `WholeProgramDevirt` needs to inline through trait objects +//! ([rust#68262], [rust#45774]). //! //! [rust#68262]: https://github.com/rust-lang/rust/issues/68262 //! [rust#45774]: https://github.com/rust-lang/rust/issues/45774 //! -//! This module replaces the production path's `dyn` with `extern "Rust"` -//! direct calls. Under `lto = "thin"` + `codegen-units = 1` (this -//! workspace's release profile), the static-arm `extern "Rust"` call -//! inlines across the `turbo-tasks` → `turbo-tasks-backend` boundary, -//! collapsing the `match` arm to a direct call to the underlying backend -//! method. +//! This module replaces that `dyn` with `extern "Rust"` direct calls. +//! Under `lto = "thin"` + `codegen-units = 1` (this workspace's release +//! profile), the `extern "Rust"` call inlines across the +//! `turbo-tasks` → `turbo-tasks-backend` boundary, collapsing the +//! dispatch site to a direct call into the underlying backend method. //! //! ```text -//! call site (static feature active) turbo-tasks-backend +//! call site turbo-tasks-backend //! ┌──────────────────────────┐ ┌─────────────────────────────────────┐ -//! │ tt.invalidate(task) │ │ #[no_mangle] pub extern "Rust" fn │ -//! │ match self { │ │ __tt_static_invalidate(ptr, task) { │ -//! │ Static(ptr) => ───┐ │ │ let tt: &TurboTasks<…> = …; │ -//! │ Dynamic(arc) => … │ │ │ tt.invalidate(task) │ -//! │ } └─────────────► │ } │ -//! └──────────────────────────┘ └─────────────────────────────────────┘ +//! │ tt.invalidate(task) │ ───────────► │ #[no_mangle] pub extern "Rust" fn │ +//! │ __tt_static_invalidate(ptr, task) │ __tt_static_invalidate(ptr, task) { │ +//! │ │ │ let tt: &TurboTasks<…> = …; │ +//! │ │ │ tt.invalidate(task) │ +//! └──────────────────────────┘ │ } │ +//! └─────────────────────────────────────┘ //! ``` //! -//! For the dynamic arm: -//! ```text -//! call site -//! ┌──────────────────────────┐ -//! │ tt.invalidate(task) │ -//! │ Dynamic(arc) => │ -//! │ arc.invalidate(task) │ (normal vtable dispatch on Arc) -//! └──────────────────────────┘ -//! ``` -//! -//! ## Feature gating -//! -//! Each variant is feature-gated. The gates exist to avoid linker-symbol -//! breakage in crates that don't pull in the providing crate at link -//! time. Names describe the *dispatch mechanism*, not the intended user: -//! `VcStorage` from `turbo-tasks-testing` uses the dynamic arm because we -//! don't want to wire its concrete type into the static dispatch surface, -//! not because dynamic dispatch is inherently a "test" concept. +//! ## Linkage contract //! -//! - **`static_handle`** — activated by `turbo-tasks-backend`'s feature `static_handle` (which the -//! napi binding opts into). Adds the `Static` variant and the `extern "Rust"` forward -//! declarations. Any binary that activates `turbo-tasks-backend/static_handle` gets both the -//! feature on `turbo-tasks` and the `#[no_mangle]` provider definitions, so the externs resolve -//! cleanly at link time. -//! - **`dynamic_handle`** — activated by `turbo-tasks-testing`'s dep on `turbo-tasks`. Adds the -//! `Dynamic` variant. Uses normal `Arc` vtable dispatch, so there's no linker footgun if the -//! feature unifies on without the providing crate being linked. +//! `turbo-tasks` declares the `__tt_static_*` symbols as `extern "Rust"` +//! forward declarations unconditionally. `turbo-tasks-backend` defines +//! them as `#[no_mangle] pub extern "Rust" fn`. Every binary that +//! links `libturbo_tasks.rlib` MUST also link +//! `libturbo_tasks_backend.rlib` so the linker can resolve the symbols +//! — there's no feature flag to gate this off. //! -//! With neither feature active, `HandleInner` has a single `Unreachable` -//! variant. This keeps the type inhabited (the `task_local!` declaration -//! requires that) but unconstructible. Crates in this situation -//! (`turbo-esregex`, `turbo-tasks-bytes` lib-tests, …) compile but cannot -//! actually run turbo-tasks code — which is fine because they don't. +//! For library crates that depend on `turbo-tasks` for the proc-macro +//! types (`turbo_tasks::value`, `turbo_tasks::function`) but never +//! construct a `TurboTasks`, that's still required because their +//! test binaries link the rlib. The convention is a dev-dep on +//! `turbo-tasks-backend` plus `#[cfg(test)] extern crate +//! turbo_tasks_backend;` in `src/lib.rs` — see the existing examples in +//! `turbo-tasks-bytes`, `turbo-esregex`, etc. //! //! ## Follow-ups //! -//! - `task_statistics` is only used by `turbo-tasks-backend/tests/ task_statistics.rs`; if that -//! test calls it on a concrete `TurboTasks` instead of via the handle, it can move off the -//! dispatch surface (no extern, no method on `TurboTasksHandle`). -//! - `VcStorage` is the only reason the `dynamic_handle` feature exists. If `turbo-tasks-testing`'s -//! harness used a real `TurboTasks` with a noop backend, the `Dynamic` variant could be -//! deleted entirely, collapsing the dispatch to a single `match` arm. +//! - `task_statistics` is only used by `turbo-tasks-backend/tests/task_statistics.rs`; if that test +//! calls it on a concrete `TurboTasks` instead of via the handle, it can move off the dispatch +//! surface (no extern, no method on `TurboTasksHandle`). -use std::{ptr::NonNull, sync::Arc}; - -#[cfg(any(feature = "static_handle", feature = "dynamic_handle"))] -use crate::TurboTasksApi; +use std::ptr::NonNull; /// Type-erased reference to a `TurboTasksApi` implementation. See the /// [module docs](self) for the dispatch design. -pub struct TurboTasksHandle(HandleInner); - -enum HandleInner { - /// Statically-dispatched handle. The pointer is the data pointer of - /// an `Arc::into_raw(arc)` where `arc: Arc>` for the - /// concrete backend type the `__tt_static_*` providers were - /// generated for. Dispatch goes through `extern "Rust"` symbols - /// defined in `turbo-tasks-backend`. - #[cfg(feature = "static_handle")] - Static(NonNull<()>), - /// Dynamically-dispatched handle. Normal `Arc` - /// vtable dispatch. Used by `VcStorage` and any other harness that - /// doesn't want to own a real `TurboTasks`. Not on the production - /// hot path. - #[cfg(feature = "dynamic_handle")] - Dynamic(Arc), - /// Placeholder variant kept so `HandleInner` stays inhabited when - /// neither feature is on. Constructing a handle requires a feature; - /// dispatch on this variant is `unreachable!()`. This lets - /// `turbo-tasks` compile standalone for crates that merely declare - /// value types and never construct a handle. - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - Unreachable, -} +/// +/// The pointer is the data pointer of an `Arc::into_raw(arc)` where +/// `arc: Arc>` for the concrete backend type the +/// `__tt_static_*` providers were generated for. Dispatch goes through +/// `extern "Rust"` symbols defined in `turbo-tasks-backend`. +pub struct TurboTasksHandle(NonNull<()>); impl std::fmt::Debug for TurboTasksHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - #[cfg(feature = "static_handle")] - HandleInner::Static(ptr) => f - .debug_tuple("TurboTasksHandle::Static") - .field(ptr) - .finish(), - #[cfg(feature = "dynamic_handle")] - HandleInner::Dynamic(_) => f.debug_tuple("TurboTasksHandle::Dynamic").finish(), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - HandleInner::Unreachable => f.write_str("TurboTasksHandle::Unreachable"), - } + f.debug_tuple("TurboTasksHandle").field(&self.0).finish() } } -// Safety: every concrete handle behind a `TurboTasksHandle` is itself -// `Send + Sync` and reference-counted via `Arc`. For the `Static` -// variant, the raw pointer is just an erased `Arc::into_raw`. The -// `Dynamic` variant is `Arc` (the trait -// bounds make it `Send + Sync`). +// Safety: the concrete `Arc>` behind the pointer is +// `Send + Sync`. We strip the type via `*const ()` but keep the +// underlying ref-counted ownership intact. unsafe impl Send for TurboTasksHandle {} unsafe impl Sync for TurboTasksHandle {} impl TurboTasksHandle { - /// Construct a `Static` handle from an `Arc::into_raw` pointer. + /// Construct a handle from an `Arc::into_raw` pointer. /// /// # Safety /// @@ -137,89 +84,41 @@ impl TurboTasksHandle { /// `Arc>` for the concrete backend the /// `__tt_static_*` providers (in `turbo-tasks-backend`) target. /// Ownership of one strong refcount transfers into the handle. - #[cfg(feature = "static_handle")] #[inline] pub unsafe fn from_static_raw(ptr: NonNull<()>) -> Self { - Self(HandleInner::Static(ptr)) - } - - /// Stub for `from_static_raw` when the `static_handle` feature is off. - /// Cannot actually be constructed and called at runtime because the - /// dispatch paths that produce a static handle are gated on the same - /// feature; this is kept as a `panic!` body so the `TurboTasks` - /// inherent `make_handle` (in `manager.rs`) can compile in builds - /// that don't activate `static_handle`. Linker dead-code elimination - /// removes both this and its callers from the final binary. - #[cfg(not(feature = "static_handle"))] - #[inline] - pub unsafe fn from_static_raw(_ptr: NonNull<()>) -> Self { - unreachable!( - "TurboTasksHandle::from_static_raw called without `static_handle` feature on \ - `turbo-tasks`" - ) - } - - /// Construct a `Dynamic` handle from any `TurboTasksApi` implementor. - #[cfg(feature = "dynamic_handle")] - #[inline] - pub fn from_dynamic(arc: Arc) -> Self { - Self(HandleInner::Dynamic(arc)) + Self(ptr) } } // ===================================================================== -// `extern "Rust"` forward declarations for the static arm. +// `extern "Rust"` forward declarations. // // The bodies are defined `#[no_mangle]` in `turbo-tasks-backend` and // resolved at link time. Thin LTO inlines them across the crate boundary. // ===================================================================== -/// Generates `unsafe extern "Rust" { fn __tt_static_; }` declarations -/// for one dispatched method. Only emitted when the `static_handle` feature -/// is active — i.e. when `turbo-tasks-backend` is in the dep graph and -/// will provide the matching `#[no_mangle]` definitions. +/// Generates `unsafe extern "Rust" { fn __tt_static_; }` +/// declarations for one dispatched method. macro_rules! tt_decl_extern { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { - #[cfg(feature = "static_handle")] unsafe extern "Rust" { fn ${concat(__tt_static_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; } }; } -/// Generates an inherent method on `TurboTasksHandle` that dispatches via -/// `match self.0`. The `Static` arm calls the `__tt_static_` extern; -/// the `Dynamic` arm (if present) calls the method on the `Arc` (vtable dispatch). With no features active, the -/// `Unreachable` arm panics — but you can't construct such a handle, so -/// this is dead code that's kept only to make the type compile. +/// Generates an inherent method on `TurboTasksHandle` that dispatches +/// via the matching `__tt_static_` extern. macro_rules! tt_decl_handle_method { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { impl TurboTasksHandle { #[inline] - #[allow(unused_variables)] pub fn $name(&self $(, $arg : $ty)*) $(-> $ret)? { - match &self.0 { - #[cfg(feature = "static_handle")] - HandleInner::Static(ptr) => unsafe { - ${concat(__tt_static_, $name)}(ptr.as_ptr() $(, $arg)*) - }, - // The `TurboTasksApi` super-trait bound on the Arc - // makes every dispatched method (whether defined on - // `TurboTasksApi` itself or its supertrait - // `TurboTasksCallApi`) callable via method-call syntax. - #[cfg(feature = "dynamic_handle")] - HandleInner::Dynamic(arc) => arc.$name($($arg),*), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - HandleInner::Unreachable => unreachable!( - "TurboTasksHandle dispatch with neither `static_handle` nor `dynamic_handle` \ - feature active on `turbo-tasks`" - ), - } + unsafe { ${concat(__tt_static_, $name)}(self.0.as_ptr() $(, $arg)*) } } } }; @@ -229,7 +128,7 @@ macro_rules! tt_decl_handle_method { // // Keep this list in sync with the matching provider implementations in // `turbo-tasks-backend/src/handle_providers.rs`. The list is duplicated; -// a missing static provider surfaces as a link error. +// a missing provider surfaces as a link error. // `TurboTasksCallApi` methods. tt_decl_extern!(fn dynamic_call( @@ -459,10 +358,8 @@ tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); // `task_statistics` returns `&TaskStatisticsApi` borrowed from `&self`. // The macro can't express the lifetime relationship through a `*const ()` -// receiver, so the static provider returns `*const TaskStatisticsApi` and -// the handle wrapper re-binds the lifetime to `&self`. The dynamic arm -// just calls the trait method on the Arc. -#[cfg(feature = "static_handle")] +// receiver, so the provider returns `*const TaskStatisticsApi` and the +// handle wrapper re-binds the lifetime to `&self`. unsafe extern "Rust" { fn __tt_static_task_statistics( ptr: *const (), @@ -472,35 +369,24 @@ unsafe extern "Rust" { impl TurboTasksHandle { #[inline] pub fn task_statistics(&self) -> &crate::task_statistics::TaskStatisticsApi { - match &self.0 { - #[cfg(feature = "static_handle")] - HandleInner::Static(ptr) => { - // SAFETY: the provider returns a pointer to a - // `TaskStatisticsApi` owned by the underlying - // `TurboTasks`, which this handle keeps alive via its - // Arc. The returned reference is bound to `&self`. - let p = unsafe { __tt_static_task_statistics(ptr.as_ptr()) }; - unsafe { &*p } - } - #[cfg(feature = "dynamic_handle")] - HandleInner::Dynamic(arc) => arc.task_statistics(), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - HandleInner::Unreachable => unreachable!(), - } + // SAFETY: the provider returns a pointer to a + // `TaskStatisticsApi` owned by the underlying `TurboTasks`, + // which this handle keeps alive via its Arc. The returned + // reference is bound to `&self`. + let p = unsafe { __tt_static_task_statistics(self.0.as_ptr()) }; + unsafe { &*p } } } // ===================================================================== -// Clone / Drop — Arc refcounting through extern symbols (prod) or -// the std `Arc` impls (test). +// Clone / Drop — Arc refcounting through extern symbols. // ===================================================================== -#[cfg(feature = "static_handle")] unsafe extern "Rust" { fn __tt_static_clone_arc(ptr: *const ()); fn __tt_static_drop_arc(ptr: *const ()); - // Weak-handle support for the static arm. Each provides: + // Weak-handle support. Each provides: // downgrade : *const Arc -> *const Weak (creates a fresh // Weak; caller owns the returned weak). // upgrade : *const Weak -> *const Arc (returns null if @@ -517,32 +403,15 @@ unsafe extern "Rust" { impl Clone for TurboTasksHandle { #[inline] fn clone(&self) -> Self { - match &self.0 { - #[cfg(feature = "static_handle")] - HandleInner::Static(ptr) => { - unsafe { __tt_static_clone_arc(ptr.as_ptr()) } - Self(HandleInner::Static(*ptr)) - } - #[cfg(feature = "dynamic_handle")] - HandleInner::Dynamic(arc) => Self(HandleInner::Dynamic(Arc::clone(arc))), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - HandleInner::Unreachable => unreachable!(), - } + unsafe { __tt_static_clone_arc(self.0.as_ptr()) } + Self(self.0) } } impl Drop for TurboTasksHandle { #[inline] fn drop(&mut self) { - match &self.0 { - #[cfg(feature = "static_handle")] - HandleInner::Static(ptr) => unsafe { __tt_static_drop_arc(ptr.as_ptr()) }, - // Dynamic variant: the `Arc` field drops itself naturally. - #[cfg(feature = "dynamic_handle")] - HandleInner::Dynamic(_) => {} - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - HandleInner::Unreachable => {} - } + unsafe { __tt_static_drop_arc(self.0.as_ptr()) } } } @@ -550,23 +419,10 @@ impl TurboTasksHandle { /// Downgrades to a weak handle, equivalent to `Arc::downgrade`. #[inline] pub fn downgrade(&self) -> TurboTasksWeakHandle { - match &self.0 { - #[cfg(feature = "static_handle")] - HandleInner::Static(ptr) => { - let weak_ptr = unsafe { __tt_static_downgrade(ptr.as_ptr()) }; - // `Weak::into_raw` always produces a valid (non-null) - // pointer even when the strong count is zero. - TurboTasksWeakHandle(WeakInner::Static(unsafe { - NonNull::new_unchecked(weak_ptr as *mut ()) - })) - } - #[cfg(feature = "dynamic_handle")] - HandleInner::Dynamic(arc) => { - TurboTasksWeakHandle(WeakInner::Dynamic(Arc::downgrade(arc))) - } - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - HandleInner::Unreachable => unreachable!(), - } + let weak_ptr = unsafe { __tt_static_downgrade(self.0.as_ptr()) }; + // `Weak::into_raw` always produces a valid (non-null) pointer + // even when the strong count is zero. + TurboTasksWeakHandle(unsafe { NonNull::new_unchecked(weak_ptr as *mut ()) }) } } @@ -581,30 +437,13 @@ impl TurboTasksHandle { /// Weak counterpart to [`TurboTasksHandle`]. Constructed via /// [`TurboTasksHandle::downgrade`]; upgraded via /// [`TurboTasksWeakHandle::upgrade`]. -pub struct TurboTasksWeakHandle(WeakInner); - -enum WeakInner { - #[cfg(feature = "static_handle")] - Static(NonNull<()>), - #[cfg(feature = "dynamic_handle")] - Dynamic(std::sync::Weak), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - Unreachable, -} +pub struct TurboTasksWeakHandle(NonNull<()>); impl std::fmt::Debug for TurboTasksWeakHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - #[cfg(feature = "static_handle")] - WeakInner::Static(ptr) => f - .debug_tuple("TurboTasksWeakHandle::Static") - .field(ptr) - .finish(), - #[cfg(feature = "dynamic_handle")] - WeakInner::Dynamic(_) => f.debug_tuple("TurboTasksWeakHandle::Dynamic").finish(), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - WeakInner::Unreachable => f.write_str("TurboTasksWeakHandle::Unreachable"), - } + f.debug_tuple("TurboTasksWeakHandle") + .field(&self.0) + .finish() } } @@ -617,51 +456,25 @@ impl TurboTasksWeakHandle { /// concrete handle has been dropped. #[inline] pub fn upgrade(&self) -> Option { - match &self.0 { - #[cfg(feature = "static_handle")] - WeakInner::Static(ptr) => { - let strong = unsafe { __tt_static_upgrade(ptr.as_ptr()) }; - let strong = NonNull::new(strong as *mut ())?; - // SAFETY: provider returned a valid `Arc::into_raw` - // pointer for the static concrete type, transferring one - // strong refcount. - Some(unsafe { TurboTasksHandle::from_static_raw(strong) }) - } - #[cfg(feature = "dynamic_handle")] - WeakInner::Dynamic(weak) => weak.upgrade().map(TurboTasksHandle::from_dynamic), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - WeakInner::Unreachable => unreachable!(), - } + let strong = unsafe { __tt_static_upgrade(self.0.as_ptr()) }; + let strong = NonNull::new(strong as *mut ())?; + // SAFETY: provider returned a valid `Arc::into_raw` pointer for + // the concrete type, transferring one strong refcount. + Some(unsafe { TurboTasksHandle::from_static_raw(strong) }) } } impl Clone for TurboTasksWeakHandle { #[inline] fn clone(&self) -> Self { - match &self.0 { - #[cfg(feature = "static_handle")] - WeakInner::Static(ptr) => { - unsafe { __tt_static_clone_weak(ptr.as_ptr()) } - Self(WeakInner::Static(*ptr)) - } - #[cfg(feature = "dynamic_handle")] - WeakInner::Dynamic(weak) => Self(WeakInner::Dynamic(weak.clone())), - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - WeakInner::Unreachable => unreachable!(), - } + unsafe { __tt_static_clone_weak(self.0.as_ptr()) } + Self(self.0) } } impl Drop for TurboTasksWeakHandle { #[inline] fn drop(&mut self) { - match &self.0 { - #[cfg(feature = "static_handle")] - WeakInner::Static(ptr) => unsafe { __tt_static_drop_weak(ptr.as_ptr()) }, - #[cfg(feature = "dynamic_handle")] - WeakInner::Dynamic(_) => {} - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - WeakInner::Unreachable => {} - } + unsafe { __tt_static_drop_weak(self.0.as_ptr()) } } } diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index 0ec3c278e9e8..5872ec11f3c3 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -12,6 +12,13 @@ #![feature(const_type_name)] #![feature(macro_metavar_expr_concat)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test/bench binaries (the circular dev-dep is declared in +// `Cargo.toml`). rustc only adds rlibs to the link command for crates +// Rust code references — this `extern crate` is the reference. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod backend; mod capture_future; mod collectibles; diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 6d2a275cc524..0b700f453742 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -598,20 +598,6 @@ impl TurboTasks { /// Builds a [`TurboTasksHandle`] that points at this `TurboTasks`. /// Consumes one strong refcount from the given `Arc`; the handle /// will drop that refcount when itself dropped. - /// - /// When the `static_handle` feature is active on `turbo-tasks` (set - /// by binaries that link `turbo-tasks-backend` with its - /// `static_handle` feature on — i.e. the napi binding), this builds - /// a `Static` handle that dispatches through `extern "Rust"` - /// providers — fully devirtualized under thin LTO. - /// - /// Otherwise (typical workspace test/bench builds, where backend's - /// `static_handle` is off to avoid spreading the externs through - /// feature unification), this falls back to a `Dynamic` handle — - /// `Arc` vtable dispatch. The fallback works - /// because `TurboTasks` itself implements `TurboTasksApi`. Slower - /// (one vtable indirection per dispatched call) but correct. - #[cfg(feature = "static_handle")] pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { let ptr = Arc::into_raw(self) as *mut (); // Safety: `ptr` came from `Arc::into_raw` on a `TurboTasks`, @@ -620,28 +606,6 @@ impl TurboTasks { unsafe { crate::TurboTasksHandle::from_static_raw(std::ptr::NonNull::new_unchecked(ptr)) } } - /// Fallback dynamic-dispatch variant of `make_handle` when the - /// `static_handle` feature is off. Requires the `dynamic_handle` - /// feature on `turbo-tasks` (activated by `turbo-tasks-testing`, - /// which everything that uses `TurboTasks` in test mode pulls in - /// transitively). - #[cfg(all(not(feature = "static_handle"), feature = "dynamic_handle"))] - pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { - crate::TurboTasksHandle::from_dynamic(self as Arc) - } - - /// Stub for `make_handle` when neither dispatch feature is on. Lets - /// the `impl TurboTasks` block compile for crates that link - /// `turbo-tasks` for types alone (no test/bench harness involved). - /// Calling it at runtime panics. - #[cfg(not(any(feature = "static_handle", feature = "dynamic_handle")))] - pub fn make_handle(self: Arc) -> crate::TurboTasksHandle { - unreachable!( - "TurboTasks::make_handle requires either the `static_handle` or `dynamic_handle` \ - feature on `turbo-tasks`" - ) - } - /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. /// Helper that clones the internal `Arc` first; equivalent to /// `self.pin().make_handle()`. diff --git a/turbopack/crates/turbopack-analyze/src/lib.rs b/turbopack/crates/turbopack-analyze/src/lib.rs index 69d13321b81a..f8b23e922899 100644 --- a/turbopack/crates/turbopack-analyze/src/lib.rs +++ b/turbopack/crates/turbopack-analyze/src/lib.rs @@ -1,4 +1,10 @@ #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary. The backend is a regular dep but rustc +// only adds rlibs to the link command for crates Rust code references. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod compressed_size; pub mod split_chunk; diff --git a/turbopack/crates/turbopack-bench/Cargo.toml b/turbopack/crates/turbopack-bench/Cargo.toml index dd67339bcfb1..db283ec74cf6 100644 --- a/turbopack/crates/turbopack-bench/Cargo.toml +++ b/turbopack/crates/turbopack-bench/Cargo.toml @@ -37,5 +37,11 @@ turbo-tasks = { workspace = true } turbopack-create-test-app = { workspace = true } url = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test/bench binaries. See the matching +# `extern crate` references in `src/lib.rs` and `benches/mod.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + [target.'cfg(unix)'.dependencies] nix = { version="0.30.1", features=["signal"] } diff --git a/turbopack/crates/turbopack-bench/benches/mod.rs b/turbopack/crates/turbopack-bench/benches/mod.rs index 56b6c57d3ac5..1eb652006398 100644 --- a/turbopack/crates/turbopack-bench/benches/mod.rs +++ b/turbopack/crates/turbopack-bench/benches/mod.rs @@ -1,3 +1,7 @@ +// Force linking the static-dispatch providers — see the matching +// dev-dep in `Cargo.toml`. +extern crate turbo_tasks_backend; + use criterion::{Criterion, criterion_group, criterion_main}; use turbopack_bench::bundlers::Bundler; diff --git a/turbopack/crates/turbopack-bench/src/lib.rs b/turbopack/crates/turbopack-bench/src/lib.rs index 8f27940201ab..f77482f80242 100644 --- a/turbopack/crates/turbopack-bench/src/lib.rs +++ b/turbopack/crates/turbopack-bench/src/lib.rs @@ -1,3 +1,8 @@ +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + use std::{ fs::{self}, panic::AssertUnwindSafe, diff --git a/turbopack/crates/turbopack-browser/Cargo.toml b/turbopack/crates/turbopack-browser/Cargo.toml index c4a6fb22ffdc..eb453d206500 100644 --- a/turbopack/crates/turbopack-browser/Cargo.toml +++ b/turbopack/crates/turbopack-browser/Cargo.toml @@ -41,3 +41,9 @@ turbopack-ecmascript = { workspace = true } turbopack-ecmascript-runtime = { workspace = true } turbopack-resolve = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-browser/src/lib.rs b/turbopack/crates/turbopack-browser/src/lib.rs index 4f8f9b765828..f228fbe1507c 100644 --- a/turbopack/crates/turbopack-browser/src/lib.rs +++ b/turbopack/crates/turbopack-browser/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub(crate) mod chunking_context; pub mod ecmascript; pub mod react_refresh; diff --git a/turbopack/crates/turbopack-cli-utils/Cargo.toml b/turbopack/crates/turbopack-cli-utils/Cargo.toml index 2cae973901ef..66e75cc7b4fd 100644 --- a/turbopack/crates/turbopack-cli-utils/Cargo.toml +++ b/turbopack/crates/turbopack-cli-utils/Cargo.toml @@ -26,3 +26,9 @@ turbo-tasks-fs = { workspace = true } turbopack-core = { workspace = true } turbopack-resolve = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + diff --git a/turbopack/crates/turbopack-cli-utils/src/lib.rs b/turbopack/crates/turbopack-cli-utils/src/lib.rs index 96b679fe1bb1..daad4390d855 100644 --- a/turbopack/crates/turbopack-cli-utils/src/lib.rs +++ b/turbopack/crates/turbopack-cli-utils/src/lib.rs @@ -2,6 +2,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod issue; pub mod runtime_entry; pub mod source_context; diff --git a/turbopack/crates/turbopack-cli/benches/mod.rs b/turbopack/crates/turbopack-cli/benches/mod.rs index 0fc2595b6158..55dd085ec363 100644 --- a/turbopack/crates/turbopack-cli/benches/mod.rs +++ b/turbopack/crates/turbopack-cli/benches/mod.rs @@ -1,3 +1,7 @@ +// Force linking the static-dispatch providers — see the matching +// dep declaration on `turbo-tasks-backend` in `Cargo.toml`. +extern crate turbo_tasks_backend; + use criterion::{Criterion, criterion_group, criterion_main}; use turbopack_bench::bundlers::Bundler; diff --git a/turbopack/crates/turbopack-css/Cargo.toml b/turbopack/crates/turbopack-css/Cargo.toml index 9efa0bf57683..44cb124da386 100644 --- a/turbopack/crates/turbopack-css/Cargo.toml +++ b/turbopack/crates/turbopack-css/Cargo.toml @@ -38,3 +38,9 @@ turbopack-core = { workspace = true } turbopack-ecmascript = { workspace = true } urlencoding = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-css/src/lib.rs b/turbopack/crates/turbopack-css/src/lib.rs index 513fb17a146f..ed65e4955da8 100644 --- a/turbopack/crates/turbopack-css/src/lib.rs +++ b/turbopack/crates/turbopack-css/src/lib.rs @@ -2,6 +2,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + mod asset; pub mod chunk; mod code_gen; diff --git a/turbopack/crates/turbopack-ecmascript-hmr-protocol/Cargo.toml b/turbopack/crates/turbopack-ecmascript-hmr-protocol/Cargo.toml index 08a5893c1cee..8602cfbc359b 100644 --- a/turbopack/crates/turbopack-ecmascript-hmr-protocol/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript-hmr-protocol/Cargo.toml @@ -19,3 +19,9 @@ serde_json = { workspace = true } turbo-rcstr = { workspace = true } turbopack-cli-utils = { workspace = true } turbopack-core = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript-hmr-protocol/src/lib.rs b/turbopack/crates/turbopack-ecmascript-hmr-protocol/src/lib.rs index 95fc8213b4e1..bba61e47df57 100644 --- a/turbopack/crates/turbopack-ecmascript-hmr-protocol/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript-hmr-protocol/src/lib.rs @@ -1,3 +1,8 @@ +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; use serde::{Deserialize, Serialize}; diff --git a/turbopack/crates/turbopack-ecmascript-plugins/Cargo.toml b/turbopack/crates/turbopack-ecmascript-plugins/Cargo.toml index 66621d88574a..ef92d79becb1 100644 --- a/turbopack/crates/turbopack-ecmascript-plugins/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript-plugins/Cargo.toml @@ -45,3 +45,9 @@ swc_plugin_backend_wasmtime = { workspace = true } swc_emotion = { workspace = true } swc_relay = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript-plugins/src/lib.rs b/turbopack/crates/turbopack-ecmascript-plugins/src/lib.rs index 728803123364..fcafb6688dc7 100644 --- a/turbopack/crates/turbopack-ecmascript-plugins/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript-plugins/src/lib.rs @@ -1,4 +1,9 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod transform; diff --git a/turbopack/crates/turbopack-ecmascript-runtime/Cargo.toml b/turbopack/crates/turbopack-ecmascript-runtime/Cargo.toml index 092b611ec866..454494ef2255 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript-runtime/Cargo.toml @@ -28,3 +28,9 @@ turbo-tasks = { workspace = true } turbo-tasks-fs = { workspace = true } turbopack-core = { workspace = true } turbopack-ecmascript = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-ecmascript-runtime/src/lib.rs b/turbopack/crates/turbopack-ecmascript-runtime/src/lib.rs index 13c5b544a6fa..175488d5b64a 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript-runtime/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub(crate) mod browser_runtime; #[cfg(feature = "test")] pub(crate) mod dummy_runtime; diff --git a/turbopack/crates/turbopack-env/Cargo.toml b/turbopack/crates/turbopack-env/Cargo.toml index ccec06b934ab..970fa0dcecec 100644 --- a/turbopack/crates/turbopack-env/Cargo.toml +++ b/turbopack/crates/turbopack-env/Cargo.toml @@ -22,3 +22,9 @@ turbo-tasks-fs = { workspace = true } turbopack-core = { workspace = true } turbopack-ecmascript = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-env/src/lib.rs b/turbopack/crates/turbopack-env/src/lib.rs index 89d1cc6424be..ebdb48fddacf 100644 --- a/turbopack/crates/turbopack-env/src/lib.rs +++ b/turbopack/crates/turbopack-env/src/lib.rs @@ -13,6 +13,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + mod asset; pub mod dotenv; mod embeddable; diff --git a/turbopack/crates/turbopack-image/Cargo.toml b/turbopack/crates/turbopack-image/Cargo.toml index ebdab94db4af..bc8d0c0b9499 100644 --- a/turbopack/crates/turbopack-image/Cargo.toml +++ b/turbopack/crates/turbopack-image/Cargo.toml @@ -41,3 +41,9 @@ turbo-tasks = { workspace = true } turbo-tasks-fs = { workspace = true } turbopack-core = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + diff --git a/turbopack/crates/turbopack-image/src/lib.rs b/turbopack/crates/turbopack-image/src/lib.rs index 4177d349e030..26b83fc23a21 100644 --- a/turbopack/crates/turbopack-image/src/lib.rs +++ b/turbopack/crates/turbopack-image/src/lib.rs @@ -1,4 +1,9 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod process; diff --git a/turbopack/crates/turbopack-mdx/Cargo.toml b/turbopack/crates/turbopack-mdx/Cargo.toml index 029d5ab6f771..29c0574e7719 100644 --- a/turbopack/crates/turbopack-mdx/Cargo.toml +++ b/turbopack/crates/turbopack-mdx/Cargo.toml @@ -23,3 +23,9 @@ turbo-tasks = { workspace = true } turbo-tasks-fs = { workspace = true } turbopack-core = { workspace = true } turbopack-ecmascript = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-mdx/src/lib.rs b/turbopack/crates/turbopack-mdx/src/lib.rs index 9e1d955f5e18..4d4fbccf4d30 100644 --- a/turbopack/crates/turbopack-mdx/src/lib.rs +++ b/turbopack/crates/turbopack-mdx/src/lib.rs @@ -2,6 +2,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + use anyhow::Result; use async_trait::async_trait; use mdxjs::{MdxParseOptions, Options, compile}; diff --git a/turbopack/crates/turbopack-nft/src/lib.rs b/turbopack/crates/turbopack-nft/src/lib.rs index 53f5ca6c1703..7fa61daa2779 100644 --- a/turbopack/crates/turbopack-nft/src/lib.rs +++ b/turbopack/crates/turbopack-nft/src/lib.rs @@ -1,3 +1,9 @@ #![feature(arbitrary_self_types)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary. The backend is a regular dep but rustc +// only adds rlibs to the link command for crates Rust code references. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod nft; diff --git a/turbopack/crates/turbopack-node/src/lib.rs b/turbopack/crates/turbopack-node/src/lib.rs index bead5baf633e..428f6d266602 100644 --- a/turbopack/crates/turbopack-node/src/lib.rs +++ b/turbopack/crates/turbopack-node/src/lib.rs @@ -2,6 +2,12 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary. The backend is a regular dep but rustc +// only adds rlibs to the link command for crates Rust code references. +#[cfg(test)] +extern crate turbo_tasks_backend; + use anyhow::Result; use rustc_hash::FxHashMap; use turbo_tasks::{ResolvedVc, TryFlatJoinIterExt, Vc}; diff --git a/turbopack/crates/turbopack-nodejs/Cargo.toml b/turbopack/crates/turbopack-nodejs/Cargo.toml index 4e1b876dcfbe..b891b1762f1f 100644 --- a/turbopack/crates/turbopack-nodejs/Cargo.toml +++ b/turbopack/crates/turbopack-nodejs/Cargo.toml @@ -39,3 +39,9 @@ turbopack-core = { workspace = true } turbopack-ecmascript = { workspace = true } turbopack-ecmascript-runtime = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-nodejs/src/lib.rs b/turbopack/crates/turbopack-nodejs/src/lib.rs index 70f72ef4ec57..80d0144fd2f5 100644 --- a/turbopack/crates/turbopack-nodejs/src/lib.rs +++ b/turbopack/crates/turbopack-nodejs/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub(crate) mod chunking_context; pub(crate) mod ecmascript; diff --git a/turbopack/crates/turbopack-resolve/Cargo.toml b/turbopack/crates/turbopack-resolve/Cargo.toml index 244bb74a41ba..f43fdf2f962b 100644 --- a/turbopack/crates/turbopack-resolve/Cargo.toml +++ b/turbopack/crates/turbopack-resolve/Cargo.toml @@ -26,3 +26,9 @@ turbo-tasks-fs = { workspace = true } turbopack-core = { workspace = true } next-taskless = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + diff --git a/turbopack/crates/turbopack-resolve/src/lib.rs b/turbopack/crates/turbopack-resolve/src/lib.rs index 93a3a21f1185..586fc02f3703 100644 --- a/turbopack/crates/turbopack-resolve/src/lib.rs +++ b/turbopack/crates/turbopack-resolve/src/lib.rs @@ -1,6 +1,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod ecmascript; pub mod node_native_binding; pub mod resolve; diff --git a/turbopack/crates/turbopack-static/Cargo.toml b/turbopack/crates/turbopack-static/Cargo.toml index 8fc528b84d3e..f132fd657aa5 100644 --- a/turbopack/crates/turbopack-static/Cargo.toml +++ b/turbopack/crates/turbopack-static/Cargo.toml @@ -23,3 +23,9 @@ turbopack-core = { workspace = true } turbopack-css = { workspace = true } turbopack-ecmascript = { workspace = true } + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-static/src/lib.rs b/turbopack/crates/turbopack-static/src/lib.rs index ac57de132f2d..7096a289cbd2 100644 --- a/turbopack/crates/turbopack-static/src/lib.rs +++ b/turbopack/crates/turbopack-static/src/lib.rs @@ -12,6 +12,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod css; pub mod ecma; pub mod fixed; diff --git a/turbopack/crates/turbopack-swc-utils/Cargo.toml b/turbopack/crates/turbopack-swc-utils/Cargo.toml index 3a9a8bfe68aa..65335b4681e9 100644 --- a/turbopack/crates/turbopack-swc-utils/Cargo.toml +++ b/turbopack/crates/turbopack-swc-utils/Cargo.toml @@ -26,3 +26,9 @@ swc_core = { workspace = true, features = [ "common_sourcemap", ] } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + diff --git a/turbopack/crates/turbopack-swc-utils/src/lib.rs b/turbopack/crates/turbopack-swc-utils/src/lib.rs index 3f961f3b4c4c..59a4cb8eb54c 100644 --- a/turbopack/crates/turbopack-swc-utils/src/lib.rs +++ b/turbopack/crates/turbopack-swc-utils/src/lib.rs @@ -1,4 +1,9 @@ #![feature(min_specialization)] #![feature(str_split_remainder)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod emitter; diff --git a/turbopack/crates/turbopack-test-utils/Cargo.toml b/turbopack/crates/turbopack-test-utils/Cargo.toml index ce4cbba4346f..b664f6b26ec3 100644 --- a/turbopack/crates/turbopack-test-utils/Cargo.toml +++ b/turbopack/crates/turbopack-test-utils/Cargo.toml @@ -26,3 +26,9 @@ turbopack-cli-utils = { workspace = true } turbopack-core = { workspace = true } rustc-hash = { workspace = true } +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching `extern +# crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } + diff --git a/turbopack/crates/turbopack-test-utils/src/lib.rs b/turbopack/crates/turbopack-test-utils/src/lib.rs index 3aead34dd5e4..ddb7d914d532 100644 --- a/turbopack/crates/turbopack-test-utils/src/lib.rs +++ b/turbopack/crates/turbopack-test-utils/src/lib.rs @@ -2,6 +2,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + pub mod jest; pub mod noop_asset_context; pub mod snapshot; diff --git a/turbopack/crates/turbopack-wasm/Cargo.toml b/turbopack/crates/turbopack-wasm/Cargo.toml index 5156042b7689..346cff980c36 100644 --- a/turbopack/crates/turbopack-wasm/Cargo.toml +++ b/turbopack/crates/turbopack-wasm/Cargo.toml @@ -26,3 +26,9 @@ turbopack-ecmascript = { workspace = true } wasmparser = "0.235.0" wat = "1.0.69" + +# Dev-only link of `turbo-tasks-backend` so the `__tt_static_*` extern +# symbols resolve in this crate's test binary. See the matching +# `extern crate` in `src/lib.rs`. +[dev-dependencies] +turbo-tasks-backend = { workspace = true } diff --git a/turbopack/crates/turbopack-wasm/src/lib.rs b/turbopack/crates/turbopack-wasm/src/lib.rs index df2e799be8bb..134824d3a1d1 100644 --- a/turbopack/crates/turbopack-wasm/src/lib.rs +++ b/turbopack/crates/turbopack-wasm/src/lib.rs @@ -9,6 +9,11 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] +// Force linking `turbo-tasks-backend`'s `__tt_static_*` providers into +// this crate's test binary; see the matching dev-dep in `Cargo.toml`. +#[cfg(test)] +extern crate turbo_tasks_backend; + use anyhow::{Context, Result}; use turbo_rcstr::RcStr; use turbo_tasks::Vc; From 80cf6e1db1bb2a7c34c0e30a6dc3140fc2a4f40a Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Wed, 20 May 2026 10:49:37 -0700 Subject: [PATCH 14/19] Remove references to the noopstorage implemetnation --- .../turbo-tasks-backend/benches/overhead.rs | 8 ++--- .../benches/scope_stress.rs | 6 ++-- .../turbo-tasks-backend/benches/stress.rs | 7 ++--- .../src/handle_providers.rs | 8 +++-- .../crates/turbo-tasks-backend/src/lib.rs | 30 ------------------- .../turbo-tasks-backend/tests/test_config.trs | 9 ++---- .../turbo-tasks-fetch/tests/test_config.trs | 7 ++--- .../tests/test_config.trs | 5 +--- .../turbopack-analyze/tests/test_config.trs | 7 ++--- .../turbopack-node/tests/test_config.trs | 7 ++--- 10 files changed, 20 insertions(+), 74 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/benches/overhead.rs b/turbopack/crates/turbo-tasks-backend/benches/overhead.rs index 184c76a47ba0..0072c4f677cb 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/overhead.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/overhead.rs @@ -4,9 +4,7 @@ use criterion::{BenchmarkId, Criterion, black_box}; use futures::{FutureExt, StreamExt, stream::FuturesUnordered}; use tokio::spawn; use turbo_tasks::{TurboTasks, unmark_top_level_task_may_leak_eventually_consistent_state}; -use turbo_tasks_backend::{ - BackendOptions, TurboTasksBackend, noop_backing_storage, prod_backing_storage_noop, -}; +use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; #[global_allocator] static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc; @@ -180,9 +178,7 @@ fn run_turbo( storage_mode: None, ..Default::default() }, - // Wrapped to match `ProdBackingStorage`, the concrete type - // the `__tt_static_*` providers cast to. - prod_backing_storage_noop(noop_backing_storage()), + noop_backing_storage(), )); async move { diff --git a/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs b/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs index a40f8b4b5759..acedc15a9a9f 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/scope_stress.rs @@ -1,9 +1,7 @@ use anyhow::Result; use criterion::{BenchmarkId, Criterion}; use turbo_tasks::{Completion, TryJoinIterExt, TurboTasks, Vc}; -use turbo_tasks_backend::{ - BackendOptions, TurboTasksBackend, noop_backing_storage, prod_backing_storage_noop, -}; +use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; pub fn scope_stress(c: &mut Criterion) { if matches!( @@ -41,7 +39,7 @@ pub fn scope_stress(c: &mut Criterion) { ..Default::default() }, // Wrap to match `ProdBackingStorage`. - prod_backing_storage_noop(noop_backing_storage()), + noop_backing_storage(), )); async move { (0..size) diff --git a/turbopack/crates/turbo-tasks-backend/benches/stress.rs b/turbopack/crates/turbo-tasks-backend/benches/stress.rs index 1c6298850f4e..2e2cbf01f05f 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/stress.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/stress.rs @@ -3,9 +3,7 @@ use criterion::{BenchmarkId, Criterion}; use turbo_tasks::{ TryJoinIterExt, TurboTasks, Vc, unmark_top_level_task_may_leak_eventually_consistent_state, }; -use turbo_tasks_backend::{ - BackendOptions, TurboTasksBackend, noop_backing_storage, prod_backing_storage_noop, -}; +use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage}; pub fn fibonacci(c: &mut Criterion) { if matches!( @@ -38,8 +36,7 @@ pub fn fibonacci(c: &mut Criterion) { storage_mode: None, ..Default::default() }, - // Wrap to match `ProdBackingStorage`. - prod_backing_storage_noop(noop_backing_storage()), + noop_backing_storage(), )); async move { tt.run(async move { diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 94bbbe9665a9..66827c4b6cc7 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -20,8 +20,6 @@ use std::sync::Arc; use turbo_tasks::{TurboTasksApi as _, TurboTasksCallApi as _}; -use crate::{ProdBackingStorage, TurboTasksBackend}; - /// The concrete prod handle type. The `__tt_static_*` providers below /// cast each opaque `*const ()` receiver to `&ProdHandleConcrete`, so /// any `Arc>>` that goes into a @@ -29,7 +27,11 @@ use crate::{ProdBackingStorage, TurboTasksBackend}; /// exact type. Mismatch is undefined behavior. See /// [`crate::ProdBackingStorage`] for why the storage type is wrapped in /// `Either`. -pub type ProdHandleConcrete = turbo_tasks::TurboTasks>; +pub type ProdHandleConcrete = turbo_tasks::TurboTasks< + crate::TurboTasksBackend< + crate::KeyValueDatabaseBackingStorage, + >, +>; /// Generates `#[no_mangle] pub extern "Rust" fn __tt_static_(...)` /// for a single dispatched method, dispatched via method call syntax. diff --git a/turbopack/crates/turbo-tasks-backend/src/lib.rs b/turbopack/crates/turbo-tasks-backend/src/lib.rs index b0d0ef7750a8..4d3fece29d09 100644 --- a/turbopack/crates/turbo-tasks-backend/src/lib.rs +++ b/turbopack/crates/turbo-tasks-backend/src/lib.rs @@ -30,36 +30,6 @@ pub use crate::{ pub type TurboBackingStorage = KeyValueDatabaseBackingStorage; -/// Concrete `BackingStorage` type accepted by the prod dispatch arm. The -/// `__tt_static_*` providers in [`handle_providers`] are monomorphized -/// for `TurboTasks>`; any caller -/// that wants its `TurboTasks::new(...)` instance to be drivable through -/// `TurboTasksHandle` must produce this exact type. The `Either` lets -/// the same handle type cover both the real on-disk cache and the noop -/// in-memory variant used by tests and the napi cdylib's `no-cache` -/// mode. -pub type ProdBackingStorage = either::Either; - -// Re-exported so consumers (test config files, the napi binding) can -// build a `ProdBackingStorage` without needing a direct dep on `either`. -pub use either::Either; - -/// Wraps a [`TurboBackingStorage`] into [`ProdBackingStorage`] so callers -/// can hand the result to [`TurboTasksBackend::new`] and end up with the -/// concrete `TurboTasks>` type the -/// `__tt_static_*` dispatch providers cast to. Equivalent to writing -/// `Either::Left(storage)` but doesn't require turbofish for type -/// inference at the call site. -pub fn prod_backing_storage_turbo(storage: TurboBackingStorage) -> ProdBackingStorage { - Either::Left(storage) -} - -/// Wraps a [`NoopBackingStorage`] into [`ProdBackingStorage`]. See -/// [`prod_backing_storage_turbo`] for the rationale. -pub fn prod_backing_storage_noop(storage: NoopBackingStorage) -> ProdBackingStorage { - Either::Right(storage) -} - /// Creates a `BackingStorage` to be passed to [`TurboTasksBackend::new`]. /// /// Information about the state of the on-disk cache is returned using [`StartupCacheState`]. diff --git a/turbopack/crates/turbo-tasks-backend/tests/test_config.trs b/turbopack/crates/turbo-tasks-backend/tests/test_config.trs index 714bcfe0e9e6..6ba3540fa199 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/test_config.trs +++ b/turbopack/crates/turbo-tasks-backend/tests/test_config.trs @@ -14,12 +14,7 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - // Wrapped via `prod_backing_storage_turbo` to match - // `ProdBackingStorage` — the concrete type the `__tt_static_*` - // providers are monomorphized for. A bare `TurboBackingStorage` - // would produce a different `TurboTasks>` - // and the providers' pointer cast would be UB at runtime. - turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( + turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -28,7 +23,7 @@ false, true, false, - ).unwrap().0) + ).unwrap().0 ) ) } diff --git a/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs b/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs index 7e3158b0e60b..e181c52b334a 100644 --- a/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs +++ b/turbopack/crates/turbo-tasks-fetch/tests/test_config.trs @@ -13,10 +13,7 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - // Wrapped via `prod_backing_storage_turbo` to match - // `ProdBackingStorage` — see the rationale in - // `turbo-tasks-backend/tests/test_config.trs`. - turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( + turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -25,7 +22,7 @@ false, true, false, - ).unwrap().0) + ).unwrap().0 ) ) } diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs b/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs index ac52e40b71a4..52ab7168aa4a 100644 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs +++ b/turbopack/crates/turbo-tasks-macros-tests/tests/test_config.trs @@ -7,10 +7,7 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - // Wrapped via `prod_backing_storage_noop` to match - // `ProdBackingStorage` — see the rationale in - // `turbo-tasks-backend/tests/test_config.trs`. - turbo_tasks_backend::prod_backing_storage_noop(turbo_tasks_backend::noop_backing_storage()), + turbo_tasks_backend::noop_backing_storage(), ) ) } diff --git a/turbopack/crates/turbopack-analyze/tests/test_config.trs b/turbopack/crates/turbopack-analyze/tests/test_config.trs index 7e3158b0e60b..e181c52b334a 100644 --- a/turbopack/crates/turbopack-analyze/tests/test_config.trs +++ b/turbopack/crates/turbopack-analyze/tests/test_config.trs @@ -13,10 +13,7 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - // Wrapped via `prod_backing_storage_turbo` to match - // `ProdBackingStorage` — see the rationale in - // `turbo-tasks-backend/tests/test_config.trs`. - turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( + turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -25,7 +22,7 @@ false, true, false, - ).unwrap().0) + ).unwrap().0 ) ) } diff --git a/turbopack/crates/turbopack-node/tests/test_config.trs b/turbopack/crates/turbopack-node/tests/test_config.trs index 20826e13d1bd..6ba3540fa199 100644 --- a/turbopack/crates/turbopack-node/tests/test_config.trs +++ b/turbopack/crates/turbopack-node/tests/test_config.trs @@ -14,10 +14,7 @@ storage_mode: Some(turbo_tasks_backend::StorageMode::ReadWriteOnShutdown), ..Default::default() }, - // Wrapped via `prod_backing_storage_turbo` to match - // `ProdBackingStorage` — see the rationale in - // `turbo-tasks-backend/tests/test_config.trs`. - turbo_tasks_backend::prod_backing_storage_turbo(turbo_tasks_backend::turbo_backing_storage( + turbo_tasks_backend::turbo_backing_storage( path.as_path(), &turbo_tasks_backend::GitVersionInfo { describe: "test-unversioned", @@ -26,7 +23,7 @@ false, true, false, - ).unwrap().0) + ).unwrap().0 ) ) } From 18d522a049fd65086ab903b89473e1311b348eae Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Wed, 20 May 2026 11:57:57 -0700 Subject: [PATCH 15/19] Merge tt_decl_extern + tt_decl_handle_method into one tt_decl macro Each dispatched method previously required two macro invocations that duplicated the signature: tt_decl_extern! for the extern declaration and tt_decl_handle_method! for the matching inherent method on TurboTasksHandle. Merge them into a single tt_decl! that emits both. --- .../src/handle_providers.rs | 3 +- turbopack/crates/turbo-tasks/src/handle.rs | 170 ++++-------------- 2 files changed, 33 insertions(+), 140 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 66827c4b6cc7..99216d772f9d 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -74,8 +74,7 @@ macro_rules! provide_prod_trait { // ---- dispatched methods --------------------------------------------------- // -// Keep this list in sync with the matching `tt_decl_extern!` / -// `tt_decl_handle_method!` invocations in +// Keep this list in sync with the matching `tt_decl!` invocations in // `turbopack/crates/turbo-tasks/src/handle.rs`. // TurboTasksCallApi diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 1f5f5d409884..c0d9f121c9f2 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -97,24 +97,16 @@ impl TurboTasksHandle { // resolved at link time. Thin LTO inlines them across the crate boundary. // ===================================================================== -/// Generates `unsafe extern "Rust" { fn __tt_static_; }` -/// declarations for one dispatched method. -macro_rules! tt_decl_extern { +/// For each dispatched method, generates both the `extern "Rust"` forward +/// declaration of `__tt_static_` and the matching inherent method on +/// `TurboTasksHandle` that calls it. +macro_rules! tt_decl { ( fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? ) => { unsafe extern "Rust" { fn ${concat(__tt_static_, $name)}(ptr: *const () $(, $arg : $ty)*) $(-> $ret)?; } - }; -} - -/// Generates an inherent method on `TurboTasksHandle` that dispatches -/// via the matching `__tt_static_` extern. -macro_rules! tt_decl_handle_method { - ( - fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? - ) => { impl TurboTasksHandle { #[inline] pub fn $name(&self $(, $arg : $ty)*) $(-> $ret)? { @@ -131,200 +123,111 @@ macro_rules! tt_decl_handle_method { // a missing provider surfaces as a link error. // `TurboTasksCallApi` methods. -tt_decl_extern!(fn dynamic_call( - native_fn: &'static crate::native_function::NativeFunction, - this: Option, - arg: &mut dyn crate::StackDynTaskInputs, - persistence: crate::TaskPersistence, -) -> crate::RawVc); -tt_decl_handle_method!(fn dynamic_call( +tt_decl!(fn dynamic_call( native_fn: &'static crate::native_function::NativeFunction, this: Option, arg: &mut dyn crate::StackDynTaskInputs, persistence: crate::TaskPersistence, ) -> crate::RawVc); -tt_decl_extern!(fn native_call( - native_fn: &'static crate::native_function::NativeFunction, - this: Option, - arg: &mut dyn crate::StackDynTaskInputs, - persistence: crate::TaskPersistence, -) -> crate::RawVc); -tt_decl_handle_method!(fn native_call( +tt_decl!(fn native_call( native_fn: &'static crate::native_function::NativeFunction, this: Option, arg: &mut dyn crate::StackDynTaskInputs, persistence: crate::TaskPersistence, ) -> crate::RawVc); -tt_decl_extern!(fn trait_call( - trait_method: &'static crate::TraitMethod, - this: crate::RawVc, - arg: &mut dyn crate::StackDynTaskInputs, - persistence: crate::TaskPersistence, -) -> crate::RawVc); -tt_decl_handle_method!(fn trait_call( +tt_decl!(fn trait_call( trait_method: &'static crate::TraitMethod, this: crate::RawVc, arg: &mut dyn crate::StackDynTaskInputs, persistence: crate::TaskPersistence, ) -> crate::RawVc); -tt_decl_extern!(fn send_compilation_event( - event: ::std::sync::Arc, -)); -tt_decl_handle_method!(fn send_compilation_event( +tt_decl!(fn send_compilation_event( event: ::std::sync::Arc, )); -tt_decl_extern!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); -tt_decl_handle_method!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); +tt_decl!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); -tt_decl_extern!(fn run( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl_handle_method!(fn run( +tt_decl!(fn run( future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl_extern!(fn run_once( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl_handle_method!(fn run_once( +tt_decl!(fn run_once( future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl_extern!(fn run_once_with_reason( - reason: crate::util::StaticOrArc, - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl_handle_method!(fn run_once_with_reason( +tt_decl!(fn run_once_with_reason( reason: crate::util::StaticOrArc, future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl_extern!(fn start_once_process( - future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, -)); -tt_decl_handle_method!(fn start_once_process( +tt_decl!(fn start_once_process( future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, )); -tt_decl_extern!(fn stop_and_wait() - -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); -tt_decl_handle_method!(fn stop_and_wait() +tt_decl!(fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); // `TurboTasksApi` methods (inherits TurboTasksCallApi above). -tt_decl_extern!(fn invalidate(task: crate::TaskId)); -tt_decl_handle_method!(fn invalidate(task: crate::TaskId)); +tt_decl!(fn invalidate(task: crate::TaskId)); -tt_decl_extern!(fn invalidate_with_reason( - task: crate::TaskId, - reason: crate::util::StaticOrArc, -)); -tt_decl_handle_method!(fn invalidate_with_reason( +tt_decl!(fn invalidate_with_reason( task: crate::TaskId, reason: crate::util::StaticOrArc, )); -tt_decl_extern!(fn invalidate_serialization(task: crate::TaskId)); -tt_decl_handle_method!(fn invalidate_serialization(task: crate::TaskId)); +tt_decl!(fn invalidate_serialization(task: crate::TaskId)); -tt_decl_extern!(fn try_read_task_output( - task: crate::TaskId, - options: crate::ReadOutputOptions, -) -> ::anyhow::Result<::core::result::Result>); -tt_decl_handle_method!(fn try_read_task_output( +tt_decl!(fn try_read_task_output( task: crate::TaskId, options: crate::ReadOutputOptions, ) -> ::anyhow::Result<::core::result::Result>); -tt_decl_extern!(fn try_read_task_cell( - task: crate::TaskId, - index: crate::CellId, - options: crate::ReadCellOptions, -) -> ::anyhow::Result<::core::result::Result>); -tt_decl_handle_method!(fn try_read_task_cell( +tt_decl!(fn try_read_task_cell( task: crate::TaskId, index: crate::CellId, options: crate::ReadCellOptions, ) -> ::anyhow::Result<::core::result::Result>); -tt_decl_extern!(fn try_read_local_output( - execution_id: crate::ExecutionId, - local_task_id: crate::LocalTaskId, -) -> ::anyhow::Result<::core::result::Result>); -tt_decl_handle_method!(fn try_read_local_output( +tt_decl!(fn try_read_local_output( execution_id: crate::ExecutionId, local_task_id: crate::LocalTaskId, ) -> ::anyhow::Result<::core::result::Result>); -tt_decl_extern!(fn read_task_collectibles( - task: crate::TaskId, - trait_id: crate::TraitTypeId, -) -> crate::backend::TaskCollectiblesMap); -tt_decl_handle_method!(fn read_task_collectibles( +tt_decl!(fn read_task_collectibles( task: crate::TaskId, trait_id: crate::TraitTypeId, ) -> crate::backend::TaskCollectiblesMap); -tt_decl_extern!(fn emit_collectible( - trait_type: crate::TraitTypeId, - collectible: crate::RawVc, -)); -tt_decl_handle_method!(fn emit_collectible( +tt_decl!(fn emit_collectible( trait_type: crate::TraitTypeId, collectible: crate::RawVc, )); -tt_decl_extern!(fn unemit_collectible( - trait_type: crate::TraitTypeId, - collectible: crate::RawVc, - count: u32, -)); -tt_decl_handle_method!(fn unemit_collectible( +tt_decl!(fn unemit_collectible( trait_type: crate::TraitTypeId, collectible: crate::RawVc, count: u32, )); -tt_decl_extern!(fn unemit_collectibles( - trait_type: crate::TraitTypeId, - collectibles: &crate::backend::TaskCollectiblesMap, -)); -tt_decl_handle_method!(fn unemit_collectibles( +tt_decl!(fn unemit_collectibles( trait_type: crate::TraitTypeId, collectibles: &crate::backend::TaskCollectiblesMap, )); -tt_decl_extern!(fn try_read_own_task_cell( - current_task: crate::TaskId, - index: crate::CellId, -) -> ::anyhow::Result); -tt_decl_handle_method!(fn try_read_own_task_cell( +tt_decl!(fn try_read_own_task_cell( current_task: crate::TaskId, index: crate::CellId, ) -> ::anyhow::Result); -tt_decl_extern!(fn read_own_task_cell( - task: crate::TaskId, - index: crate::CellId, -) -> ::anyhow::Result); -tt_decl_handle_method!(fn read_own_task_cell( +tt_decl!(fn read_own_task_cell( task: crate::TaskId, index: crate::CellId, ) -> ::anyhow::Result); -tt_decl_extern!(fn update_own_task_cell( - task: crate::TaskId, - index: crate::CellId, - content: crate::backend::CellContent, - updated_key_hashes: ::core::option::Option<::smallvec::SmallVec<[u64; 2]>>, - content_hash: ::core::option::Option, - verification_mode: crate::backend::VerificationMode, -)); -tt_decl_handle_method!(fn update_own_task_cell( +tt_decl!(fn update_own_task_cell( task: crate::TaskId, index: crate::CellId, content: crate::backend::CellContent, @@ -333,28 +236,19 @@ tt_decl_handle_method!(fn update_own_task_cell( verification_mode: crate::backend::VerificationMode, )); -tt_decl_extern!(fn mark_own_task_as_finished(task: crate::TaskId)); -tt_decl_handle_method!(fn mark_own_task_as_finished(task: crate::TaskId)); +tt_decl!(fn mark_own_task_as_finished(task: crate::TaskId)); -tt_decl_extern!(fn connect_task(task: crate::TaskId)); -tt_decl_handle_method!(fn connect_task(task: crate::TaskId)); +tt_decl!(fn connect_task(task: crate::TaskId)); -tt_decl_extern!(fn spawn_detached_for_testing( - f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, -)); -tt_decl_handle_method!(fn spawn_detached_for_testing( +tt_decl!(fn spawn_detached_for_testing( f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, )); -tt_decl_extern!(fn subscribe_to_compilation_events( - event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, -) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); -tt_decl_handle_method!(fn subscribe_to_compilation_events( +tt_decl!(fn subscribe_to_compilation_events( event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, ) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); -tt_decl_extern!(fn is_tracking_dependencies() -> bool); -tt_decl_handle_method!(fn is_tracking_dependencies() -> bool); +tt_decl!(fn is_tracking_dependencies() -> bool); // `task_statistics` returns `&TaskStatisticsApi` borrowed from `&self`. // The macro can't express the lifetime relationship through a `*const ()` From 1bef8d62610e7828d32364fd81e42ddc343bc9d2 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Wed, 20 May 2026 11:58:43 -0700 Subject: [PATCH 16/19] Drop subscribe_to_compilation_events from TurboTasksHandle dispatch The only caller (next-napi-bindings/src/next_api/project.rs:2142) holds the concrete `Arc>>` and goes through the trait method via static dispatch. No call site reaches it via `TurboTasksHandle`, so the extern + handle wrapper were dead weight. --- turbopack/crates/turbo-tasks-backend/src/handle_providers.rs | 3 --- turbopack/crates/turbo-tasks/src/handle.rs | 4 ---- 2 files changed, 7 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 99216d772f9d..846acdc66802 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -174,9 +174,6 @@ provide_prod!(fn connect_task(task: turbo_tasks::TaskId)); provide_prod!(fn spawn_detached_for_testing( f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, )); -provide_prod!(fn subscribe_to_compilation_events( - event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, -) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); provide_prod!(fn is_tracking_dependencies() -> bool); // `task_statistics` is special: the trait method returns diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index c0d9f121c9f2..3e14caddda95 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -244,10 +244,6 @@ tt_decl!(fn spawn_detached_for_testing( f: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, )); -tt_decl!(fn subscribe_to_compilation_events( - event_types: ::core::option::Option<::std::vec::Vec<::std::string::String>>, -) -> ::tokio::sync::mpsc::Receiver<::std::sync::Arc>); - tt_decl!(fn is_tracking_dependencies() -> bool); // `task_statistics` returns `&TaskStatisticsApi` borrowed from `&self`. From 747e5f9ccb8bb3616b292eecb564d061161ea9cf Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Wed, 20 May 2026 12:26:09 -0700 Subject: [PATCH 17/19] Genericize DevServerBuilder::serve and UpdateServer::run Both APIs previously took a `TurboTasksHandle`, forcing callers like turbopack-cli to erase their concrete `Arc>` through the extern-dispatch handle. Make them generic over `T: TurboTasksApi` and `T: TurboTasksCallApi` respectively, so the concrete backend type flows through and trait methods monomorphize directly. The two `run_once_with_reason` free-helper calls in dev-server are inlined: the request path uses a oneshot channel to ferry the response out of the `Result<()>` trait method, and the side-effects path spawns its closure inside `tokio::spawn(async move { tt.run_once_with_reason(...) })` so the boxed future owns its `Arc` clone (the trait method's future isn't `'static`). --- turbopack/crates/turbo-tasks/src/manager.rs | 4 +- turbopack/crates/turbopack-cli/src/dev/mod.rs | 2 +- .../crates/turbopack-dev-server/src/lib.rs | 219 ++++++++++-------- .../turbopack-dev-server/src/update/server.rs | 10 +- 4 files changed, 135 insertions(+), 100 deletions(-) diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 0b700f453742..0be0085ea96d 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -607,9 +607,7 @@ impl TurboTasks { } /// Builds a [`TurboTasksHandle`] for this `TurboTasks` instance. - /// Helper that clones the internal `Arc` first; equivalent to - /// `self.pin().make_handle()`. - pub fn make_handle_from_ref(&self) -> crate::TurboTasksHandle { + fn make_handle_from_ref(&self) -> crate::TurboTasksHandle { self.pin().make_handle() } diff --git a/turbopack/crates/turbopack-cli/src/dev/mod.rs b/turbopack/crates/turbopack-cli/src/dev/mod.rs index 6f914c1e7314..5db53d3b8bb7 100644 --- a/turbopack/crates/turbopack-cli/src/dev/mod.rs +++ b/turbopack/crates/turbopack-cli/src/dev/mod.rs @@ -247,7 +247,7 @@ impl TurbopackDevServerBuilder { }; let issue_reporter_arc = Arc::new(move || issue_provider.get_issue_reporter()); - Ok(server.serve(tasks.make_handle(), source, issue_reporter_arc)) + Ok(server.serve(tasks, source, issue_reporter_arc)) } } diff --git a/turbopack/crates/turbopack-dev-server/src/lib.rs b/turbopack/crates/turbopack-dev-server/src/lib.rs index 542b6b256186..46e06823a053 100644 --- a/turbopack/crates/turbopack-dev-server/src/lib.rs +++ b/turbopack/crates/turbopack-dev-server/src/lib.rs @@ -19,7 +19,7 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, anyhow}; use hyper::{ Request, Response, Server, server::{Builder, conn::AddrIncoming}, @@ -30,8 +30,8 @@ use socket2::{Domain, Protocol, Socket, Type}; use tokio::task::JoinHandle; use tracing::{Instrument, Level, Span, event, info_span}; use turbo_tasks::{ - Effects, NonLocalValue, OperationVc, PrettyPrintError, Vc, run_once_with_reason, take_effects, - trace::TraceRawVcs, util::FormatDuration, + Effects, InvalidationReason, NonLocalValue, OperationVc, PrettyPrintError, TurboTasksApi, Vc, + take_effects, trace::TraceRawVcs, util::FormatDuration, }; use turbopack_core::issue::{IssueReporter, IssueSeverity, handle_issues}; @@ -122,12 +122,15 @@ impl DevServer { } impl DevServerBuilder { - pub fn serve( + pub fn serve( self, - turbo_tasks: turbo_tasks::TurboTasksHandle, + turbo_tasks: Arc, source_provider: impl SourceProvider + NonLocalValue + TraceRawVcs + Sync, get_issue_reporter: Arc Vc> + Send + Sync>, - ) -> DevServer { + ) -> DevServer + where + T: TurboTasksApi + ?Sized + 'static, + { let ongoing_side_effects = Arc::new(Mutex::new(VecDeque::< Arc>>>>, >::with_capacity(16))); @@ -181,103 +184,133 @@ impl DevServerBuilder { method: request.method().clone(), uri: request.uri().clone(), }; - run_once_with_reason(tt.clone(), reason, async move { - // TODO: `get_issue_reporter` should be an `OperationVc`, as there's a - // risk it could be a task-local Vc, which is not safe for us to await. - let issue_reporter = get_issue_reporter(); + let (response_tx, response_rx) = + tokio::sync::oneshot::channel::>(); + let request_tt = tt.clone(); + let request_inner = async move { + let result: Result> = async move { + // TODO: `get_issue_reporter` should be an `OperationVc`, as there's + // a risk it could be a task-local + // Vc, which is not safe for us to await. + let issue_reporter = get_issue_reporter(); - if hyper_tungstenite::is_upgrade_request(&request) { - let uri = request.uri(); - let path = uri.path(); + if hyper_tungstenite::is_upgrade_request(&request) { + let uri = request.uri(); + let path = uri.path(); - if path == "/turbopack-hmr" { - let (response, websocket) = - hyper_tungstenite::upgrade(request, None)?; - let update_server = - UpdateServer::new(source_provider, issue_reporter); - update_server.run(&tt, websocket); - return Ok(response); - } + if path == "/turbopack-hmr" { + let (response, websocket) = + hyper_tungstenite::upgrade(request, None)?; + let update_server = + UpdateServer::new(source_provider, issue_reporter); + update_server.run(&tt, websocket); + return Ok(response); + } - println!("[404] {path} (WebSocket)"); - if path == "/_next/hmr" { - // Special-case requests to hmr as these are made by - // Next.js clients built - // without turbopack, which may be making requests in - // development. - println!( - "A non-turbopack next.js client is trying to connect." - ); - println!( - "Make sure to reload/close any browser window which has \ - been opened without --turbo." - ); - } + println!("[404] {path} (WebSocket)"); + if path == "/_next/hmr" { + // Special-case requests to hmr as these are made by + // Next.js clients built + // without turbopack, which may be making requests in + // development. + println!( + "A non-turbopack next.js client is trying to connect." + ); + println!( + "Make sure to reload/close any browser window which \ + has been opened without --turbo." + ); + } - return Ok(Response::builder() - .status(404) - .body(hyper::Body::empty())?); - } + return Ok(Response::builder() + .status(404) + .body(hyper::Body::empty())?); + } - let uri = request.uri(); - let path = uri.path().to_string(); - let source_with_issues_op = - get_source_with_issues_operation(source_provider.get_source()); - let ContentSourceWithIssues { source_op, effects } = - &*source_with_issues_op.read_strongly_consistent().await?; - effects.apply().await?; - handle_issues( - source_with_issues_op, - issue_reporter, - IssueSeverity::Fatal, - Some(&path), - Some("get source"), - ) - .await?; - let (response, side_effects) = - http::process_request_with_content_source( - // HACK: pass `source` here (instead of `resolved_source` - // because the underlying API wants to do it's own - // `.resolve().strongly_consistent()` call. - // - // It's unlikely (the calls happen one-after-another), but this - // could cause inconsistency between the reported issues and - // the generated HTTP response. - *source_op, - request, + let uri = request.uri(); + let path = uri.path().to_string(); + let source_with_issues_op = + get_source_with_issues_operation(source_provider.get_source()); + let ContentSourceWithIssues { source_op, effects } = + &*source_with_issues_op.read_strongly_consistent().await?; + effects.apply().await?; + handle_issues( + source_with_issues_op, issue_reporter, + IssueSeverity::Fatal, + Some(&path), + Some("get source"), ) .await?; - let status = response.status().as_u16(); - let is_error = response.status().is_client_error() - || response.status().is_server_error(); - let elapsed = start.elapsed(); - if is_error - || (cfg!(feature = "log_request_stats") - && elapsed > Duration::from_secs(1)) - { - println!( - "[{status}] {path} ({duration})", - duration = FormatDuration(elapsed) - ); + let (response, side_effects) = + http::process_request_with_content_source( + // HACK: pass `source` here (instead of `resolved_source` + // because the underlying API wants to do it's own + // `.resolve().strongly_consistent()` call. + // + // It's unlikely (the calls happen one-after-another), but + // this could cause + // inconsistency between the reported issues and + // the generated HTTP response. + *source_op, + request, + issue_reporter, + ) + .await?; + let status = response.status().as_u16(); + let is_error = response.status().is_client_error() + || response.status().is_server_error(); + let elapsed = start.elapsed(); + if is_error + || (cfg!(feature = "log_request_stats") + && elapsed > Duration::from_secs(1)) + { + println!( + "[{status}] {path} ({duration})", + duration = FormatDuration(elapsed) + ); + } + if !side_effects.is_empty() { + let side_effects_tt = tt.clone(); + let join_handle = tokio::spawn(async move { + side_effects_tt + .run_once_with_reason( + (Arc::new(side_effects_reason) + as Arc) + .into(), + Box::pin(async move { + for side_effect in side_effects { + side_effect.apply().await?; + } + Ok(()) + }), + ) + .await + }); + ongoing_side_effects.lock().push_back(Arc::new( + tokio::sync::Mutex::new(Some(join_handle)), + )); + } + Ok(response) } - if !side_effects.is_empty() { - let join_handle = tokio::spawn(run_once_with_reason( - tt.clone(), - side_effects_reason, - async move { - for side_effect in side_effects { - side_effect.apply().await?; - } + .await; + result + }; + async move { + request_tt + .run_once_with_reason( + (Arc::new(reason) as Arc).into(), + Box::pin(async move { + let response = request_inner.await?; + let _ = response_tx.send(response); Ok(()) - }, - )); - ongoing_side_effects.lock().push_back(Arc::new( - tokio::sync::Mutex::new(Some(join_handle)), - )); - } - Ok(response) - }) + }), + ) + .await?; + response_rx + .await + .map_err(|_| anyhow!("response channel closed")) + } .await }; async move { diff --git a/turbopack/crates/turbopack-dev-server/src/update/server.rs b/turbopack/crates/turbopack-dev-server/src/update/server.rs index 545d25040cc4..49b1b87ff316 100644 --- a/turbopack/crates/turbopack-dev-server/src/update/server.rs +++ b/turbopack/crates/turbopack-dev-server/src/update/server.rs @@ -1,6 +1,7 @@ use std::{ ops::ControlFlow, pin::Pin, + sync::Arc, task::{Context, Poll}, }; @@ -13,8 +14,8 @@ use tokio::select; use tokio_stream::StreamMap; use tracing::{Level, instrument}; use turbo_tasks::{ - NonLocalValue, OperationVc, PrettyPrintError, ReadRef, TransientInstance, Vc, - trace::TraceRawVcs, + NonLocalValue, OperationVc, PrettyPrintError, ReadRef, TransientInstance, TurboTasksCallApi, + Vc, trace::TraceRawVcs, }; use turbo_tasks_fs::json::parse_json_with_source_context; use turbopack_core::{issue::IssueReporter, version::Update}; @@ -52,7 +53,10 @@ where } /// Run the update server loop. - pub fn run(self, tt: &turbo_tasks::TurboTasksHandle, ws: HyperWebsocket) { + pub fn run(self, tt: &Arc, ws: HyperWebsocket) + where + T: TurboTasksCallApi + ?Sized + 'static, + { tt.start_once_process(Box::pin(async move { if let Err(err) = self.run_internal(ws).await { println!("[UpdateServer]: error {err:#}"); From 50878b88a2c960e5e140b998bc2095a3dab9d41b Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Wed, 20 May 2026 16:48:27 -0700 Subject: [PATCH 18/19] drop start_once_process` from the handle interface, it is dead --- turbopack/crates/turbo-tasks-backend/src/handle_providers.rs | 5 +---- turbopack/crates/turbo-tasks/src/handle.rs | 4 ---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 846acdc66802..68a701373a5b 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -52,7 +52,7 @@ macro_rules! provide_prod { /// Same as `provide_prod!`, but forces UFCS dispatch to a specific /// trait. Used for methods (`run`, `run_once`, `run_once_with_reason`, -/// `start_once_process`, `stop_and_wait`) where the concrete type has +/// `stop_and_wait`) where the concrete type has /// an inherent method with the same name but a different return type — /// without UFCS, the inherent method wins and the macro fails type /// checking. @@ -111,9 +111,6 @@ provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( reason: turbo_tasks::util::StaticOrArc, future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn start_once_process( - future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, -)); provide_prod_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); // TurboTasksApi diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 3e14caddda95..72daff050ec5 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -163,10 +163,6 @@ tt_decl!(fn run_once_with_reason( future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, ) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -tt_decl!(fn start_once_process( - future: ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send + 'static>>, -)); - tt_decl!(fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); From ee7f1dc65a915845cce83d48e62a7103d8ce5f3c Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Wed, 20 May 2026 18:14:55 -0700 Subject: [PATCH 19/19] Drop run/run_once/run_once_with_reason/stop_and_wait from TurboTasksHandle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These four methods were only routed through the type-erased TurboTasksHandle so test code could hold them. Every production caller already has the concrete Arc>> and uses the inherent methods directly. Make TestInstance carry the concrete arc (Arc>>) instead of a TurboTasksHandle — all .trs files produce that exact type, so Registration / TestInstance stay non-generic. Migrate the test-only turbo_tasks::run / run_once / run_once_with_reason free-fn shims into turbo-tasks-testing, where they can call the inherent methods (which already return Result directly, so the oneshot-channel wrapper is unnecessary). Then delete: the four tt_decl entries, their providers in handle_providers.rs, the now-unused provide_prod_trait! macro, the three free-fn shims in manager.rs, and the matching trait methods on TurboTasksCallApi (run, run_once) and TurboTasksApi (stop_and_wait). TurboTasksCallApi::run_once_with_reason stays — the dev-server uses it through Arc. --- .../src/handle_providers.rs | 34 - .../turbo-tasks-backend/tests/eviction.rs | 619 +++++++++--------- .../turbo-tasks-backend/tests/scope_stress.rs | 4 +- .../crates/turbo-tasks-fetch/tests/fetch.rs | 116 ++-- .../crates/turbo-tasks-testing/src/run.rs | 62 +- turbopack/crates/turbo-tasks/src/handle.rs | 16 - turbopack/crates/turbo-tasks/src/lib.rs | 6 +- turbopack/crates/turbo-tasks/src/manager.rs | 90 --- 8 files changed, 412 insertions(+), 535 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs index 68a701373a5b..95e1cb461a84 100644 --- a/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs +++ b/turbopack/crates/turbo-tasks-backend/src/handle_providers.rs @@ -50,28 +50,6 @@ macro_rules! provide_prod { }; } -/// Same as `provide_prod!`, but forces UFCS dispatch to a specific -/// trait. Used for methods (`run`, `run_once`, `run_once_with_reason`, -/// `stop_and_wait`) where the concrete type has -/// an inherent method with the same name but a different return type — -/// without UFCS, the inherent method wins and the macro fails type -/// checking. -macro_rules! provide_prod_trait { - ( - $trait:path, - fn $name:ident( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)? - ) => { - #[unsafe(no_mangle)] - pub extern "Rust" fn ${concat(__tt_static_, $name)}( - ptr: *const () - $(, $arg : $ty)* - ) $(-> $ret)? { - let tt: &ProdHandleConcrete = unsafe { &*(ptr as *const ProdHandleConcrete) }; - ::$name(tt $(, $arg)*) - } - }; -} - // ---- dispatched methods --------------------------------------------------- // // Keep this list in sync with the matching `tt_decl!` invocations in @@ -101,18 +79,6 @@ provide_prod!(fn send_compilation_event( )); provide_prod!(fn get_task_name(task: turbo_tasks::TaskId) -> ::std::string::String); -provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -provide_prod_trait!(turbo_tasks::TurboTasksCallApi, fn run_once_with_reason( - reason: turbo_tasks::util::StaticOrArc, - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); -provide_prod_trait!(turbo_tasks::TurboTasksApi, fn stop_and_wait() -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); - // TurboTasksApi provide_prod!(fn invalidate(task: turbo_tasks::TaskId)); provide_prod!(fn invalidate_with_reason( diff --git a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs index cd8e74bbf0b9..2ad6f564b887 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/eviction.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/eviction.rs @@ -81,36 +81,37 @@ async fn eviction_recompute() { let (tt, _persistence_dir) = create_tt("eviction_recompute"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); - - // Create state via operation (persistent task) - let state_op = create_state(1); - let state_vc = state_op.resolve().strongly_consistent().await?; - let state = state_op.read_strongly_consistent().await?; - - // Create compute task (persistent, depends on state) - let output = compute(state_vc); - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 1); - let initial_random = read.random; - - // Trigger snapshot + eviction - let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("snapshot had_data={had_data}, evicted: {counts:?}"); - assert!(had_data, "snapshot should have persisted data"); - - // Invalidate via state change — this requires restoring evicted tasks - state.set(2); - - // Read again — tasks must be restored from disk before re-executing - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 2); - assert_ne!(read.random, initial_random); - - anyhow::Ok(()) - }) - .await; + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + // Create state via operation (persistent task) + let state_op = create_state(1); + let state_vc = state_op.resolve().strongly_consistent().await?; + let state = state_op.read_strongly_consistent().await?; + + // Create compute task (persistent, depends on state) + let output = compute(state_vc); + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 1); + let initial_random = read.random; + + // Trigger snapshot + eviction + let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("snapshot had_data={had_data}, evicted: {counts:?}"); + assert!(had_data, "snapshot should have persisted data"); + + // Invalidate via state change — this requires restoring evicted tasks + state.set(2); + + // Read again — tasks must be restored from disk before re-executing + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 2); + assert_ne!(read.random, initial_random); + + anyhow::Ok(()) + }) + .await; tt.stop_and_wait().await; result.unwrap(); } @@ -123,51 +124,52 @@ async fn eviction_deep_chain() { let (tt, _persistence_dir) = create_tt("eviction_deep_chain"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); - - let state_op = create_state(10); - let state_vc = state_op.resolve().strongly_consistent().await?; - let state = state_op.read_strongly_consistent().await?; - - let output = deep_chain(state_vc); - let read = output.read_strongly_consistent().await?; - // (10+1)*3+10 = 43 - assert_eq!(read.value, 43); - let initial_random = read.random; - - // Snapshot + evict — expect multiple intermediate tasks evicted - let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("deep_chain: snapshot had_data={had_data}, evicted: {counts:?}"); - assert!(had_data, "snapshot should have persisted data"); - assert!( - counts.full + counts.data_and_meta + counts.data_only + counts.meta_only > 0, - "expected some tasks to be evicted" - ); - - // Change the deepest input — must propagate through all restored tasks - state.set(20); - - let read = output.read_strongly_consistent().await?; - // (20+1)*3+10 = 73 - assert_eq!(read.value, 73); - assert_ne!(read.random, initial_random); - let random_after_first = read.random; - - // Evict again and change again - let (had_data2, counts2) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("deep_chain (2nd): snapshot had_data={had_data2}, evicted: {counts2:?}"); - - state.set(0); - - let read = output.read_strongly_consistent().await?; - // (0+1)*3+10 = 13 - assert_eq!(read.value, 13); - assert_ne!(read.random, random_after_first); - - anyhow::Ok(()) - }) - .await; + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + let state_op = create_state(10); + let state_vc = state_op.resolve().strongly_consistent().await?; + let state = state_op.read_strongly_consistent().await?; + + let output = deep_chain(state_vc); + let read = output.read_strongly_consistent().await?; + // (10+1)*3+10 = 43 + assert_eq!(read.value, 43); + let initial_random = read.random; + + // Snapshot + evict — expect multiple intermediate tasks evicted + let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("deep_chain: snapshot had_data={had_data}, evicted: {counts:?}"); + assert!(had_data, "snapshot should have persisted data"); + assert!( + counts.full + counts.data_and_meta + counts.data_only + counts.meta_only > 0, + "expected some tasks to be evicted" + ); + + // Change the deepest input — must propagate through all restored tasks + state.set(20); + + let read = output.read_strongly_consistent().await?; + // (20+1)*3+10 = 73 + assert_eq!(read.value, 73); + assert_ne!(read.random, initial_random); + let random_after_first = read.random; + + // Evict again and change again + let (had_data2, counts2) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("deep_chain (2nd): snapshot had_data={had_data2}, evicted: {counts2:?}"); + + state.set(0); + + let read = output.read_strongly_consistent().await?; + // (0+1)*3+10 = 13 + assert_eq!(read.value, 13); + assert_ne!(read.random, random_after_first); + + anyhow::Ok(()) + }) + .await; tt.stop_and_wait().await; result.unwrap(); } @@ -180,49 +182,50 @@ async fn eviction_dependency_chain() { let (tt, _persistence_dir) = create_tt("eviction_dependency_chain"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); - - let state_op = create_state(10); - let state_vc = state_op.resolve().strongly_consistent().await?; - let state = state_op.read_strongly_consistent().await?; - - let output = compute_chain(state_vc); - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 20); // 10 * 2 - let initial_random = read.random; - - // Snapshot + evict - let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("snapshot had_data={had_data}, evicted: {counts:?}"); - assert!(had_data, "snapshot should have persisted data"); - assert!( - counts.full + counts.data_and_meta + counts.data_only + counts.meta_only > 0, - "expected some tasks to be evicted" - ); - - // Change the deepest input - state.set(5); - - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 10); // 5 * 2 - assert_ne!(read.random, initial_random); - let random_after_first = read.random; - - // Evict again - let (had_data2, counts2) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("snapshot (2nd) had_data={had_data2}, evicted: {counts2:?}"); - - // Change again - state.set(100); - - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 200); // 100 * 2 - assert_ne!(read.random, random_after_first); - - anyhow::Ok(()) - }) - .await; + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + let state_op = create_state(10); + let state_vc = state_op.resolve().strongly_consistent().await?; + let state = state_op.read_strongly_consistent().await?; + + let output = compute_chain(state_vc); + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 20); // 10 * 2 + let initial_random = read.random; + + // Snapshot + evict + let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("snapshot had_data={had_data}, evicted: {counts:?}"); + assert!(had_data, "snapshot should have persisted data"); + assert!( + counts.full + counts.data_and_meta + counts.data_only + counts.meta_only > 0, + "expected some tasks to be evicted" + ); + + // Change the deepest input + state.set(5); + + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 10); // 5 * 2 + assert_ne!(read.random, initial_random); + let random_after_first = read.random; + + // Evict again + let (had_data2, counts2) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("snapshot (2nd) had_data={had_data2}, evicted: {counts2:?}"); + + // Change again + state.set(100); + + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 200); // 100 * 2 + assert_ne!(read.random, random_after_first); + + anyhow::Ok(()) + }) + .await; tt.stop_and_wait().await; result.unwrap(); } @@ -358,44 +361,45 @@ async fn eviction_session_stateful_survives() { let (tt, _persistence_dir) = create_tt("eviction_session_stateful_survives"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); - - // read_session_counter internally creates+resolves create_session_counter(42). - // The transient run_once only reads read_session_counter, so - // create_session_counter has no transient dependents and is eligible for - // eviction consideration — but should be blocked by SessionStateful. - let reader = read_session_counter(42); - let read = reader.read_strongly_consistent().await?; - assert_eq!(read.value, 42); - - // Also build a normal (evictable) chain for comparison - let state_op = create_state(10); - let state_vc = state_op.resolve().strongly_consistent().await?; - let normal = deep_chain(state_vc); - let normal_read = normal.read_strongly_consistent().await?; - // (10+1)*3+10 = 43 - assert_eq!(normal_read.value, 43); - - // Snapshot + evict - let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("session_stateful: snapshot had_data={had_data}, evicted: {counts:?}"); - assert!(had_data, "snapshot should have persisted data"); - // The normal intermediate tasks (add_one, times_three, plus_ten) should be - // evicted. The session-stateful create_session_counter should NOT be fully - // evicted (its data is blocked by has_session_stateful_cells). - assert!( - counts.full + counts.data_and_meta + counts.data_only + counts.meta_only > 0, - "normal intermediate tasks should be evicted" - ); - - // After eviction, reading through the session-stateful chain should still work - let read2 = reader.read_strongly_consistent().await?; - assert_eq!(read2.value, 42); - - anyhow::Ok(()) - }) - .await; + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + // read_session_counter internally creates+resolves create_session_counter(42). + // The transient run_once only reads read_session_counter, so + // create_session_counter has no transient dependents and is eligible for + // eviction consideration — but should be blocked by SessionStateful. + let reader = read_session_counter(42); + let read = reader.read_strongly_consistent().await?; + assert_eq!(read.value, 42); + + // Also build a normal (evictable) chain for comparison + let state_op = create_state(10); + let state_vc = state_op.resolve().strongly_consistent().await?; + let normal = deep_chain(state_vc); + let normal_read = normal.read_strongly_consistent().await?; + // (10+1)*3+10 = 43 + assert_eq!(normal_read.value, 43); + + // Snapshot + evict + let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("session_stateful: snapshot had_data={had_data}, evicted: {counts:?}"); + assert!(had_data, "snapshot should have persisted data"); + // The normal intermediate tasks (add_one, times_three, plus_ten) should be + // evicted. The session-stateful create_session_counter should NOT be fully + // evicted (its data is blocked by has_session_stateful_cells). + assert!( + counts.full + counts.data_and_meta + counts.data_only + counts.meta_only > 0, + "normal intermediate tasks should be evicted" + ); + + // After eviction, reading through the session-stateful chain should still work + let read2 = reader.read_strongly_consistent().await?; + assert_eq!(read2.value, 42); + + anyhow::Ok(()) + }) + .await; tt.stop_and_wait().await; result.unwrap(); } @@ -411,50 +415,51 @@ async fn eviction_transient_reader_invalidated() { let (tt, _persistence_dir) = create_tt("eviction_transient_reader_invalidated"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); - - // Create persistent state + compute tasks - let state_op = create_state(50); - let state_vc = state_op.resolve().strongly_consistent().await?; - let state = state_op.read_strongly_consistent().await?; - - let output = compute(state_vc); - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 50); - let initial_random = read.random; - - // Snapshot + evict. The persistent `compute` task has a transient dependent - // (this run_once closure), so it may be blocked from full eviction. But we - // still exercise the evict path — some tasks (like create_state) may be - // data-only evicted. - let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("transient_reader: snapshot had_data={had_data}, evicted: {counts:?}"); - assert!(had_data, "snapshot should have persisted data"); - - // Mutate state — this invalidates the persistent task, which must propagate - // to the transient reader (this closure) even after eviction. - state.set(99); - - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 99); - assert_ne!( - read.random, initial_random, - "task should have been re-executed after invalidation" - ); - - // Second eviction cycle - let (_, counts2) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("transient_reader (2nd): evicted: {counts2:?}"); - - state.set(0); - - let read = output.read_strongly_consistent().await?; - assert_eq!(read.value, 0); - - anyhow::Ok(()) - }) - .await; + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + // Create persistent state + compute tasks + let state_op = create_state(50); + let state_vc = state_op.resolve().strongly_consistent().await?; + let state = state_op.read_strongly_consistent().await?; + + let output = compute(state_vc); + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 50); + let initial_random = read.random; + + // Snapshot + evict. The persistent `compute` task has a transient dependent + // (this run_once closure), so it may be blocked from full eviction. But we + // still exercise the evict path — some tasks (like create_state) may be + // data-only evicted. + let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("transient_reader: snapshot had_data={had_data}, evicted: {counts:?}"); + assert!(had_data, "snapshot should have persisted data"); + + // Mutate state — this invalidates the persistent task, which must propagate + // to the transient reader (this closure) even after eviction. + state.set(99); + + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 99); + assert_ne!( + read.random, initial_random, + "task should have been re-executed after invalidation" + ); + + // Second eviction cycle + let (_, counts2) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("transient_reader (2nd): evicted: {counts2:?}"); + + state.set(0); + + let read = output.read_strongly_consistent().await?; + assert_eq!(read.value, 0); + + anyhow::Ok(()) + }) + .await; tt.stop_and_wait().await; result.unwrap(); } @@ -525,61 +530,62 @@ async fn eviction_stress_concurrent() { } }); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); - - let state_op = create_state(1); - let state_vc = state_op.resolve().strongly_consistent().await?; - let state = state_op.read_strongly_consistent().await?; - - // fan_out creates width * 2 intermediate tasks per call - let width = 20u32; - let output = fan_out(state_vc, width); - - // Helper: compute the expected fan_out result for a given state value. - // fan_out sums (state + i) * (i + 2) for i in 0..width. - let expected_for = |state_val: u32| -> u32 { - (0..width) - .map(|i| state_val.wrapping_add(i).wrapping_mul(i.wrapping_add(2))) - .fold(0u32, |acc, x| acc.wrapping_add(x)) - }; - - // Initial read to populate all tasks in memory, then wait for the - // background eviction thread to snapshot + evict at least once so data - // is on disk and eligible for eviction on subsequent cycles. - let read = *output.read_strongly_consistent().await?; - assert_eq!(read, expected_for(1)); - // Give the background eviction thread time to run a snapshot+evict cycle. - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - - // Run invalidation cycles while the background eviction thread is active. - // The sleep between eviction cycles gives worker threads time to start - // restoring, then eviction runs and races with in-flight restores. - for i in 1u32..=50 { - state.set(i); - let read = tokio::time::timeout( - std::time::Duration::from_secs(5), - output.read_strongly_consistent(), - ) - .await - .unwrap_or_else(|_| { - panic!( - "cycle {i}: timed out waiting for read — likely a restore/eviction race \ - corrupted the task graph" + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + let state_op = create_state(1); + let state_vc = state_op.resolve().strongly_consistent().await?; + let state = state_op.read_strongly_consistent().await?; + + // fan_out creates width * 2 intermediate tasks per call + let width = 20u32; + let output = fan_out(state_vc, width); + + // Helper: compute the expected fan_out result for a given state value. + // fan_out sums (state + i) * (i + 2) for i in 0..width. + let expected_for = |state_val: u32| -> u32 { + (0..width) + .map(|i| state_val.wrapping_add(i).wrapping_mul(i.wrapping_add(2))) + .fold(0u32, |acc, x| acc.wrapping_add(x)) + }; + + // Initial read to populate all tasks in memory, then wait for the + // background eviction thread to snapshot + evict at least once so data + // is on disk and eligible for eviction on subsequent cycles. + let read = *output.read_strongly_consistent().await?; + assert_eq!(read, expected_for(1)); + // Give the background eviction thread time to run a snapshot+evict cycle. + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + // Run invalidation cycles while the background eviction thread is active. + // The sleep between eviction cycles gives worker threads time to start + // restoring, then eviction runs and races with in-flight restores. + for i in 1u32..=50 { + state.set(i); + let read = tokio::time::timeout( + std::time::Duration::from_secs(5), + output.read_strongly_consistent(), ) - })?; - let read = *read; - assert_eq!( - read, - expected_for(i), - "cycle {i}: expected {}, got {read}", - expected_for(i) - ); - } - - anyhow::Ok(()) - }) - .await; + .await + .unwrap_or_else(|_| { + panic!( + "cycle {i}: timed out waiting for read — likely a restore/eviction race \ + corrupted the task graph" + ) + })?; + let read = *read; + assert_eq!( + read, + expected_for(i), + "cycle {i}: expected {}, got {read}", + expected_for(i) + ); + } + + anyhow::Ok(()) + }) + .await; stop.store(true, Ordering::Relaxed); eviction_handle.await.unwrap(); @@ -667,60 +673,61 @@ async fn eviction_persistable_never_preserves_live_cell() { let (tt, _persistence_dir) = create_tt("eviction_persistable_never_preserves_live_cell"); let tt2 = tt.clone(); - let result = turbo_tasks::run_once(tt.clone().make_handle(), async move { - unmark_top_level_task_may_leak_eventually_consistent_state(); + let result = tt + .run_once(async move { + unmark_top_level_task_may_leak_eventually_consistent_state(); + + let state_op = create_state(0); + let state_vc = state_op.resolve().strongly_consistent().await?; + let state = state_op.read_strongly_consistent().await?; + + // First read goes through `read_session_alive_id` so the writer + // (`create_session_alive`) has a persistent parent and is eligible + // for the eviction sweep without being held alive by run_once. + let pre = read_session_alive_id(state_vc) + .read_strongly_consistent() + .await?; + assert!(pre.alive, "freshly constructed cell should be alive"); + let live_ptr = pre.ptr; + drop(pre); + + // Snapshot + evict. `create_session_alive`'s `cell_data` should retain + // the SessionAlive cell as residue (Evictability::Never), while + // clearing `data_restored` and persisted data flag bits. + let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); + println!("persistable_never: snapshot had_data={had_data}, evicted: {counts:?}"); + assert!(had_data, "snapshot should have persisted data"); + + // Invalidate the reader so the next read re-runs `read_session_alive_id`. + // That re-execution reads `create_session_alive`'s cell, which goes + // through `task(.., Data)` and triggers `restore_data_from` — the buggy + // path here runs `extend(incoming)` over `cell_data` and replaces the + // live Arc with a freshly decoded one whose `alive` is default. + state.set(1); + + let post = read_session_alive_id(state_vc) + .read_strongly_consistent() + .await?; + println!( + "post-restore: alive={}, ptr_match={}", + post.alive, + post.ptr == live_ptr + ); - let state_op = create_state(0); - let state_vc = state_op.resolve().strongly_consistent().await?; - let state = state_op.read_strongly_consistent().await?; + assert_eq!( + post.ptr, live_ptr, + "post-restore cell must still hold the live `alive` Arc; a different pointer \ + means restore_data_from overwrote the residue with a freshly decoded copy" + ); + assert!( + post.alive, + "post-restore cell must still report alive=true; alive=false means the live cell \ + value was replaced by a decoded copy with default fields" + ); - // First read goes through `read_session_alive_id` so the writer - // (`create_session_alive`) has a persistent parent and is eligible - // for the eviction sweep without being held alive by run_once. - let pre = read_session_alive_id(state_vc) - .read_strongly_consistent() - .await?; - assert!(pre.alive, "freshly constructed cell should be alive"); - let live_ptr = pre.ptr; - drop(pre); - - // Snapshot + evict. `create_session_alive`'s `cell_data` should retain - // the SessionAlive cell as residue (Evictability::Never), while - // clearing `data_restored` and persisted data flag bits. - let (had_data, counts) = tt2.backend().snapshot_and_evict_for_testing(&*tt2); - println!("persistable_never: snapshot had_data={had_data}, evicted: {counts:?}"); - assert!(had_data, "snapshot should have persisted data"); - - // Invalidate the reader so the next read re-runs `read_session_alive_id`. - // That re-execution reads `create_session_alive`'s cell, which goes - // through `task(.., Data)` and triggers `restore_data_from` — the buggy - // path here runs `extend(incoming)` over `cell_data` and replaces the - // live Arc with a freshly decoded one whose `alive` is default. - state.set(1); - - let post = read_session_alive_id(state_vc) - .read_strongly_consistent() - .await?; - println!( - "post-restore: alive={}, ptr_match={}", - post.alive, - post.ptr == live_ptr - ); - - assert_eq!( - post.ptr, live_ptr, - "post-restore cell must still hold the live `alive` Arc; a different pointer means \ - restore_data_from overwrote the residue with a freshly decoded copy" - ); - assert!( - post.alive, - "post-restore cell must still report alive=true; alive=false means the live cell \ - value was replaced by a decoded copy with default fields" - ); - - anyhow::Ok(()) - }) - .await; + anyhow::Ok(()) + }) + .await; tt.stop_and_wait().await; result.unwrap(); } diff --git a/turbopack/crates/turbo-tasks-backend/tests/scope_stress.rs b/turbopack/crates/turbo-tasks-backend/tests/scope_stress.rs index f44073088052..e5d94f8f0b58 100644 --- a/turbopack/crates/turbo-tasks-backend/tests/scope_stress.rs +++ b/turbopack/crates/turbo-tasks-backend/tests/scope_stress.rs @@ -2,7 +2,7 @@ #![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this use anyhow::Result; -use turbo_tasks::{Completion, TryJoinIterExt, Vc, run_once}; +use turbo_tasks::{Completion, TryJoinIterExt, Vc}; use turbo_tasks_testing::{Registration, register, run_with_tt}; static REGISTRATION: Registration = register!(); @@ -19,7 +19,7 @@ async fn rectangle_stress() -> Result<()> { .map(|(a, b)| { let tt = tt.clone(); async move { - run_once(tt, async move { + tt.run_once(async move { rectangle(a, b).strongly_consistent().await?; Ok(Vc::<()>::default()) }) diff --git a/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs b/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs index 409454eb03db..020cba8c29df 100644 --- a/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs +++ b/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs @@ -335,15 +335,16 @@ async fn ttl_invalidates_within_session() { let TestInstance { tt, .. } = REGISTRATION.create_turbo_tasks("ttl_invalidates_within_session", true); - let body = turbo_tasks::run_once(tt.clone(), { - let url = url.clone(); - async move { - let body = fetch_body(url).read_strongly_consistent().await?; - Ok((*body).clone()) - } - }) - .await - .unwrap(); + let body = tt + .run_once({ + let url = url.clone(); + async move { + let body = fetch_body(url).read_strongly_consistent().await?; + Ok((*body).clone()) + } + }) + .await + .unwrap(); assert_eq!(&*body, "v1"); // Change the server response @@ -360,15 +361,16 @@ async fn ttl_invalidates_within_session() { // The timer should have invalidated fetch_inner, so a new strongly consistent read // should re-fetch and return the updated body. - let body = turbo_tasks::run_once(tt.clone(), { - let url = url.clone(); - async move { - let body = fetch_body(url).read_strongly_consistent().await?; - Ok((*body).clone()) - } - }) - .await - .unwrap(); + let body = tt + .run_once({ + let url = url.clone(); + async move { + let body = fetch_body(url).read_strongly_consistent().await?; + Ok((*body).clone()) + } + }) + .await + .unwrap(); assert_eq!(&*body, "v2"); tt.stop_and_wait().await; @@ -397,15 +399,16 @@ async fn ttl_invalidates_on_session_restore() { // Session 1: fetch and cache let TestInstance { tt, .. } = REGISTRATION.create_turbo_tasks("ttl_invalidates_on_session_restore", true); - let body = turbo_tasks::run_once(tt.clone(), { - let url = url.clone(); - async move { - let body = fetch_body(url).read_strongly_consistent().await?; - Ok((*body).clone()) - } - }) - .await - .unwrap(); + let body = tt + .run_once({ + let url = url.clone(); + async move { + let body = fetch_body(url).read_strongly_consistent().await?; + Ok((*body).clone()) + } + }) + .await + .unwrap(); assert_eq!(&*body, "v1"); tt.stop_and_wait().await; @@ -429,7 +432,7 @@ async fn ttl_invalidates_on_session_restore() { // timer-triggered re-execution to settle. let TestInstance { tt, .. } = REGISTRATION.create_turbo_tasks("ttl_invalidates_on_session_restore", false); - turbo_tasks::run_once(tt.clone(), { + tt.run_once({ let url = url.clone(); async move { // First read returns the stale cached value, but triggers the timer @@ -443,15 +446,16 @@ async fn ttl_invalidates_on_session_restore() { // Wait for the timer to fire and re-execution to settle tokio::time::sleep(std::time::Duration::from_millis(500)).await; - let body = turbo_tasks::run_once(tt.clone(), { - let url = url.clone(); - async move { - let body = fetch_body(url).read_strongly_consistent().await?; - Ok((*body).clone()) - } - }) - .await - .unwrap(); + let body = tt + .run_once({ + let url = url.clone(); + async move { + let body = fetch_body(url).read_strongly_consistent().await?; + Ok((*body).clone()) + } + }) + .await + .unwrap(); assert_eq!(&*body, "v2"); tt.stop_and_wait().await; } @@ -487,15 +491,16 @@ async fn errors_retried_on_session_restore() { let TestInstance { tt, .. } = REGISTRATION.create_turbo_tasks("errors_retried_on_session_restore", true); - let is_err = turbo_tasks::run_once(tt.clone(), { - let url = url.clone(); - async move { - let is_err = *fetch_is_err(url).read_strongly_consistent().await?; - Ok(is_err) - } - }) - .await - .unwrap(); + let is_err = tt + .run_once({ + let url = url.clone(); + async move { + let is_err = *fetch_is_err(url).read_strongly_consistent().await?; + Ok(is_err) + } + }) + .await + .unwrap(); assert!(is_err, "first fetch should be an error"); tt.stop_and_wait().await; @@ -509,15 +514,16 @@ async fn errors_retried_on_session_restore() { let TestInstance { tt, .. } = REGISTRATION.create_turbo_tasks("errors_retried_on_session_restore", false); - let is_err = turbo_tasks::run_once(tt.clone(), { - let url = url.clone(); - async move { - let is_err = *fetch_is_err(url).read_strongly_consistent().await?; - Ok(is_err) - } - }) - .await - .unwrap(); + let is_err = tt + .run_once({ + let url = url.clone(); + async move { + let is_err = *fetch_is_err(url).read_strongly_consistent().await?; + Ok(is_err) + } + }) + .await + .unwrap(); assert!(!is_err, "second fetch should succeed after session restore"); tt.stop_and_wait().await; } diff --git a/turbopack/crates/turbo-tasks-testing/src/run.rs b/turbopack/crates/turbo-tasks-testing/src/run.rs index 06cbbb50b86f..184688ce4ece 100644 --- a/turbopack/crates/turbo-tasks-testing/src/run.rs +++ b/turbopack/crates/turbo-tasks-testing/src/run.rs @@ -1,24 +1,24 @@ use std::{env, fmt::Debug, future::Future, sync::Arc}; use anyhow::Result; -use turbo_tasks::{TurboTasks, TurboTasksHandle, trace::TraceRawVcs}; -use turbo_tasks_backend::{BackingStorage, TurboTasksBackend}; +use turbo_tasks::{TurboTasks, trace::TraceRawVcs}; +use turbo_tasks_backend::{TurboBackingStorage, TurboTasksBackend}; -/// A freshly created test instance: the `TurboTasks` handle (type-erased to -/// `TurboTasksHandle`) and a closure that, when called, takes a -/// snapshot and evicts all evictable tasks on that instance. -/// -/// The eviction closure captures the concrete backend type internally so -/// harness code holding an erased handle can still reach the -/// `snapshot_and_evict` API. +/// Concrete `TurboTasks` arc used by every test harness in the workspace. +/// All `test_config.trs` files in this workspace produce this exact type, +/// so the test surface can hold it directly instead of going through the +/// type-erased `TurboTasksHandle`. +pub type TestTurboTasks = Arc>>; + +/// A freshly created test instance: the concrete `TurboTasks` arc and a +/// closure that, when called, takes a snapshot and evicts all evictable +/// tasks on that instance. pub struct TestInstance { - pub tt: TurboTasksHandle, + pub tt: TestTurboTasks, pub snapshot_and_evict: Box, } -/// Type-erased factory returned by the `register!` macro. Stays non-generic so -/// call sites can write `static REGISTRATION: Registration = register!();` -/// without naming the backing storage type. +/// Factory returned by the `register!` macro. pub struct Registration { create_turbo_tasks: fn(&str, bool) -> TestInstance, } @@ -34,14 +34,10 @@ impl Registration { } } -/// Wrap a concrete `Arc>>` into a -/// [`TestInstance`]. Called from the `register!` macro — the `.trs` closure -/// returns a concrete backend-parameterized `TurboTasks`, and this function -/// erases the type while retaining eviction access via a capturing closure. -pub fn test_instance(tt: Arc>>) -> TestInstance -where - B: BackingStorage + 'static, -{ +/// Wrap a concrete `TestTurboTasks` into a [`TestInstance`]. Called from +/// the `register!` macro — the `.trs` closure returns the concrete arc, +/// and this function attaches an eviction closure for the test harness. +pub fn test_instance(tt: TestTurboTasks) -> TestInstance { let tt_for_evict = tt.clone(); let snapshot_and_evict = Box::new(move || { let _ = tt_for_evict @@ -49,7 +45,7 @@ where .snapshot_and_evict_for_testing(&*tt_for_evict); }); TestInstance { - tt: tt.make_handle(), + tt, snapshot_and_evict, } } @@ -77,7 +73,9 @@ where { let name = closure_to_name(&fut); let instance = registration.create_turbo_tasks(&name, true); - turbo_tasks::run_once(instance.tt, async move { Ok(fut.await) }) + instance + .tt + .run_once(async move { Ok(fut.await) }) .await .unwrap() } @@ -91,9 +89,7 @@ where { let name = closure_to_name(&fut); let instance = registration.create_turbo_tasks(&name, true); - turbo_tasks::run(instance.tt, async move { Ok(fut.await) }) - .await - .unwrap() + instance.tt.run(async move { Ok(fut.await) }).await.unwrap() } fn closure_to_name(value: &T) -> String { @@ -109,7 +105,11 @@ where F: Future> + Send + 'static, T: Debug + PartialEq + Eq + TraceRawVcs + Send + 'static, { - run_with_tt(registration, move |tt| turbo_tasks::run_once(tt, fut())).await + run_with_tt(registration, move |tt| { + let f = fut(); + async move { tt.run_once(f).await } + }) + .await } pub async fn run( @@ -120,12 +120,16 @@ where F: Future> + Send + 'static, T: Debug + PartialEq + Eq + TraceRawVcs + Send + 'static, { - run_with_tt(registration, move |tt| turbo_tasks::run(tt, fut())).await + run_with_tt(registration, move |tt| { + let f = fut(); + async move { Ok(tt.run(f).await?) } + }) + .await } pub async fn run_with_tt( registration: &Registration, - mut fut: impl FnMut(TurboTasksHandle) -> F + Send + 'static, + mut fut: impl FnMut(TestTurboTasks) -> F + Send + 'static, ) -> Result<()> where F: Future> + Send + 'static, diff --git a/turbopack/crates/turbo-tasks/src/handle.rs b/turbopack/crates/turbo-tasks/src/handle.rs index 72daff050ec5..ad0137e5fd2d 100644 --- a/turbopack/crates/turbo-tasks/src/handle.rs +++ b/turbopack/crates/turbo-tasks/src/handle.rs @@ -150,22 +150,6 @@ tt_decl!(fn send_compilation_event( tt_decl!(fn get_task_name(task: crate::TaskId) -> ::std::string::String); -tt_decl!(fn run( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - -tt_decl!(fn run_once( - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - -tt_decl!(fn run_once_with_reason( - reason: crate::util::StaticOrArc, - future: ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send + 'static>>, -) -> ::std::pin::Pin<::std::boxed::Box> + ::core::marker::Send>>); - -tt_decl!(fn stop_and_wait() - -> ::std::pin::Pin<::std::boxed::Box + ::core::marker::Send>>); - // `TurboTasksApi` methods (inherits TurboTasksCallApi above). tt_decl!(fn invalidate(task: crate::TaskId)); diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index 5872ec11f3c3..10c4fba0a551 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -101,9 +101,9 @@ pub use crate::{ CurrentCellRef, ReadCellTracking, ReadConsistency, ReadTracking, TaskPersistence, TaskPriority, TurboTasks, TurboTasksApi, TurboTasksCallApi, Unused, UpdateInfo, dynamic_call, emit, get_serialization_invalidator, mark_finished, mark_stateful, - mark_top_level_task, prevent_gc, run, run_once, run_once_with_reason, trait_call, - turbo_tasks, turbo_tasks_scope, turbo_tasks_weak, - unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks, + mark_top_level_task, prevent_gc, trait_call, turbo_tasks, turbo_tasks_scope, + turbo_tasks_weak, unmark_top_level_task_may_leak_eventually_consistent_state, + with_turbo_tasks, }, mapped_read_ref::MappedReadRef, output::OutputContent, diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 0be0085ea96d..b2418b3d9879 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -83,14 +83,6 @@ pub trait TurboTasksCallApi: Sync + Send { persistence: TaskPersistence, ) -> RawVc; - fn run( - &self, - future: Pin> + Send + 'static>>, - ) -> Pin> + Send>>; - fn run_once( - &self, - future: Pin> + Send + 'static>>, - ) -> Pin> + Send>>; fn run_once_with_reason( &self, reason: StaticOrArc, @@ -185,8 +177,6 @@ pub trait TurboTasksApi: TurboTasksCallApi + Sync + Send { fn task_statistics(&self) -> &TaskStatisticsApi; - fn stop_and_wait(&self) -> Pin + Send>>; - fn subscribe_to_compilation_events( &self, event_types: Option>, @@ -1339,24 +1329,6 @@ impl TurboTasksCallApi for TurboTasks { self.trait_call(trait_method, this, arg, persistence) } - #[track_caller] - fn run( - &self, - future: Pin> + Send + 'static>>, - ) -> Pin> + Send>> { - let this = self.pin(); - Box::pin(async move { this.run(future).await }) - } - - #[track_caller] - fn run_once( - &self, - future: Pin> + Send + 'static>>, - ) -> Pin> + Send>> { - let this = self.pin(); - Box::pin(async move { this.run_once(future).await }) - } - #[track_caller] fn run_once_with_reason( &self, @@ -1577,13 +1549,6 @@ impl TurboTasksApi for TurboTasks { self.backend.task_statistics() } - fn stop_and_wait(&self) -> Pin + Send + 'static>> { - let this = self.pin(); - Box::pin(async move { - this.stop_and_wait().await; - }) - } - fn subscribe_to_compilation_events( &self, event_types: Option>, @@ -1667,61 +1632,6 @@ pub(crate) fn debug_assert_not_in_top_level_task(operation: &str) { } } -pub async fn run( - tt: TurboTasksHandle, - future: impl Future> + Send + 'static, -) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - - tt.run(Box::pin(async move { - let result = future.await?; - tx.send(result) - .map_err(|_| anyhow!("unable to send result"))?; - Ok(()) - })) - .await?; - - Ok(rx.await?) -} - -pub async fn run_once( - tt: TurboTasksHandle, - future: impl Future> + Send + 'static, -) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - - tt.run_once(Box::pin(async move { - let result = future.await?; - tx.send(result) - .map_err(|_| anyhow!("unable to send result"))?; - Ok(()) - })) - .await?; - - Ok(rx.await?) -} - -pub async fn run_once_with_reason( - tt: TurboTasksHandle, - reason: impl InvalidationReason, - future: impl Future> + Send + 'static, -) -> Result { - let (tx, rx) = tokio::sync::oneshot::channel(); - - tt.run_once_with_reason( - (Arc::new(reason) as Arc).into(), - Box::pin(async move { - let result = future.await?; - tx.send(result) - .map_err(|_| anyhow!("unable to send result"))?; - Ok(()) - }), - ) - .await?; - - Ok(rx.await?) -} - /// Calls [`TurboTasks::dynamic_call`] for the current turbo tasks instance. pub fn dynamic_call( func: &'static NativeFunction,