vercel/next.js · #100871

turbopack: drop ASTs of tree-shaken modules

lukesandberg · merged Oct 30, 202615 files · 240 + / 7 −
crates/next-api/src/app.rs3 + / 0 −
@@ -1012,9 +1012,12 @@ impl AppProject {                 let (full, binding_usage_info) = if remove_unused_imports {                     let full_with_unused_references =                         ModuleGraph::from_graphs(graphs.clone(), None);+                    // This is a per-route graph: a module that is unused here might still be code+                    // generated via another graph, so dropping its data would only cause re-parses.                     let binding_usage_info = compute_binding_usage_info(                         full_with_unused_references,                         should_read_binding_usage,+                        /* drop_unused_module_data */ false,                     );                     (                         ModuleGraph::from_graphs(graphs, Some(binding_usage_info)),
crates/next-api/src/pages.rs6 + / 1 −
@@ -919,7 +919,12 @@ impl PageEndpoint {              let graph = if remove_unused_imports {                 let graph = ModuleGraph::from_graphs(graphs.clone(), None);-                let binding_usage_info = compute_binding_usage_info(graph, true);+                // This is a per-route graph: a module that is unused here might still be code+                // generated via another graph, so dropping its data would only cause re-parses.+                let binding_usage_info = compute_binding_usage_info(+                    graph, /* remove_unused_imports */ true,+                    /* drop_unused_module_data */ false,+                );                 ModuleGraph::from_graphs(graphs, Some(binding_usage_info))             } else {                 ModuleGraph::from_graphs(graphs, None)
crates/next-api/src/project.rs9 + / 2 −
@@ -2942,7 +2942,10 @@ async fn whole_app_module_graph_operation(         let base = if turbopack_remove_unused_imports {             // TODO suboptimal that we do compute_binding_usage_info twice (once for the base             // graph and later for the full graph)-            let binding_usage_info = compute_binding_usage_info(base, true);+            let binding_usage_info = compute_binding_usage_info(+                base, /* remove_unused_imports */ true,+                /* drop_unused_module_data */ false,+            );             ModuleGraph::from_graphs(vec![base_single_module_graph], Some(binding_usage_info))         } else {             base@@ -2989,7 +2992,11 @@ async fn whole_app_module_graph_operation(          let (full, binding_usage_info) = if turbopack_remove_unused_imports {             let full_with_unused_references = ModuleGraph::from_graphs(graphs.clone(), None);-            let binding_usage_info = compute_binding_usage_info(full_with_unused_references, true);+            let binding_usage_info = compute_binding_usage_info(+                full_with_unused_references,+                /* remove_unused_imports */ true,+                /* drop_unused_module_data */ true,+            );             (                 ModuleGraph::from_graphs(graphs, Some(binding_usage_info)),                 Some(binding_usage_info),
turbopack/crates/turbo-tasks-backend/src/backend/mod.rs22 + / 0 −
@@ -1189,6 +1189,19 @@ impl TurboTasksBackend {         Ok(ReadOutcome::Scheduled(listener))     } +    fn drop_task_cell_content_hint(+        &self,+        task_id: TaskId,+        cell: CellId,+        turbo_tasks: &TurboTasks<TurboTasksBackend>,+    ) {+        let mut ctx = self.execute_context(turbo_tasks);+        let mut task = ctx.task(task_id, TaskDataCategory::Data);+        // Only drop the content, if it's there. A later read recomputes the task, exactly like+        // after a `final_read_hint` read.+        let _ = task.remove_cell_data(&cell, &get_value_type(cell.type_id()).persistence);+    }+     fn listen_to_cell(         &self,         task: &mut TaskGuard<'_>,@@ -3957,6 +3970,15 @@ impl Backend for TurboTasksBackend {         self.try_read_own_task_cell(task_id, cell, turbo_tasks)     } +    fn drop_task_cell_content_hint(+        &self,+        task_id: TaskId,+        cell: CellId,+        turbo_tasks: &TurboTasks<Self>,+    ) {+        self.drop_task_cell_content_hint(task_id, cell, turbo_tasks)+    }+     fn read_task_collectibles(         &self,         task_id: TaskId,
turbopack/crates/turbo-tasks-backend/tests/drop_content_hint.rsadded92 + / 0 −
@@ -0,0 +1,92 @@+#![feature(arbitrary_self_types)]+#![feature(arbitrary_self_types_pointers)]+#![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this++use std::{+    sync::atomic::{AtomicU32, Ordering},+    time::Duration,+};++use anyhow::Result;+use turbo_tasks::Vc;+use turbo_tasks_testing::{Registration, register, run_once_without_cache_check};++static REGISTRATION: Registration = register!();++static COMPUTE_COUNT: AtomicU32 = AtomicU32::new(0);++#[tokio::test(flavor = "multi_thread", worker_threads = 2)]+async fn drop_content_hint() {+    // The test counts executions, so it must run only once, on a fresh instance.+    run_once_without_cache_check(&REGISTRATION, async {+        // Initial read computes the value.+        assert_eq!(*read_operation(1).read_strongly_consistent().await?, 42);+        assert_eq!(COMPUTE_COUNT.load(Ordering::SeqCst), 1);++        // Dropping present content doesn't execute the task.+        drop_operation(2).read_strongly_consistent().await?;+        // Dropping content that is not present anymore doesn't recompute it either.+        drop_operation(3).read_strongly_consistent().await?;+        // Give a (wrongly) scheduled recomputation a chance to run.+        tokio::time::sleep(Duration::from_millis(100)).await;+        assert_eq!(COMPUTE_COUNT.load(Ordering::SeqCst), 1);++        // A later read recomputes the dropped content.+        assert_eq!(*read_operation(4).read_strongly_consistent().await?, 42);+        assert_eq!(COMPUTE_COUNT.load(Ordering::SeqCst), 2);++        // And the content is kept again after that read.+        assert_eq!(*read_operation(5).read_strongly_consistent().await?, 42);+        assert_eq!(COMPUTE_COUNT.load(Ordering::SeqCst), 2);++        // Dropping through a chain of task outputs drops the cell the chain ends in.+        drop_through_output_operation(6)+            .read_strongly_consistent()+            .await?;+        assert_eq!(*read_operation(7).read_strongly_consistent().await?, 42);+        assert_eq!(COMPUTE_COUNT.load(Ordering::SeqCst), 3);++        anyhow::Ok(())+    })+    .await+    .unwrap()+}++/// Not serializable, so the content can't be restored from the persistent cache and must be+/// recomputed after it has been dropped.+#[turbo_tasks::value(serialization = "skip")]+struct Content(u32);++#[turbo_tasks::function]+fn compute() -> Vc<Content> {+    COMPUTE_COUNT.fetch_add(1, Ordering::SeqCst);+    Content(42).cell()+}++/// Returns the output of another task, so its own output points to a task output.+#[turbo_tasks::function]+fn forward() -> Vc<Content> {+    compute()+}++/// `nonce` makes every call a new task, so that it actually reads the cell again. (It has to be+/// used, as unused arguments are not part of the task key.)+#[turbo_tasks::function(operation, root)]+async fn read_operation(nonce: u32) -> Result<Vc<u32>> {+    let _ = nonce;+    Ok(Vc::cell(compute().await?.0))+}++#[turbo_tasks::function(operation, root)]+async fn drop_operation(nonce: u32) -> Result<Vc<()>> {+    let _ = nonce;+    compute().drop_content_hint().await?;+    Ok(Vc::cell(()))+}++#[turbo_tasks::function(operation, root)]+async fn drop_through_output_operation(nonce: u32) -> Result<Vc<()>> {+    let _ = nonce;+    forward().drop_content_hint().await?;+    Ok(Vc::cell(()))+}
turbopack/crates/turbo-tasks-macros/src/value_impl_macro.rs7 + / 1 −
@@ -2,7 +2,7 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, TokenStream as TokenStream2}; use quote::{ToTokens, quote}; use syn::{-    Error, Expr, ExprLit, Generics, ImplItem, ImplItemFn, ItemImpl, Lit, LitStr, Meta,+    Attribute, Error, Expr, ExprLit, Generics, ImplItem, ImplItemFn, ItemImpl, Lit, LitStr, Meta,     MetaNameValue, Path, Token, Type,     parse::{Parse, ParseStream},     parse_macro_input,@@ -167,6 +167,7 @@ pub fn value_impl(args: TokenStream, input: TokenStream) -> TokenStream {     }      fn trait_value_impl(+        attrs: &[Attribute],         ty: &Type,         generics: &Generics,         ty_ident: &Ident,@@ -315,6 +316,10 @@ pub fn value_impl(args: TokenStream, input: TokenStream) -> TokenStream {             #[automatically_derived]             unsafe impl #impl_generics turbo_tasks::UpcastStrict<::std::boxed::Box<dyn #trait_path>> for #ty #where_clause {} +            // Forward the impl's attributes, so that e.g. `#[async_trait]` placed below+            // `#[turbo_tasks::value_impl]` applies to the plain (non-turbo-tasks) methods. The+            // turbo-tasks functions are already lowered to sync signatures at this point.+            #(#attrs)*             impl #impl_generics #trait_path for #ty #where_clause {                 #(#trait_items)*                 #(#trait_functions)*@@ -340,6 +345,7 @@ pub fn value_impl(args: TokenStream, input: TokenStream) -> TokenStream {     match &item.trait_ {         None => inherent_value_impl(&item.self_ty, &ty_ident, &item.items).into(),         Some((_, trait_path, _)) => trait_value_impl(+            &item.attrs,             &item.self_ty,             &item.generics,             &ty_ident,
turbopack/crates/turbo-tasks/src/backend.rs16 + / 0 −
@@ -676,6 +676,22 @@ pub trait Backend: Sized + Sync + Send {         turbo_tasks: &TurboTasks<Self>,     ) -> Result<TypedCellContent>; +    /// Hint that the content of the cell `cell` of task `task_id` is no longer needed, so the+    /// backend may drop it to free memory. A later read of a dropped cell recomputes the owning+    /// task, like after a `final_read_hint` read.+    ///+    /// The task must exist. The cell content doesn't need to be present (e.g. it was already+    /// dropped); this never reads or recomputes it.+    ///+    /// This doesn't track any dependency. The default implementation does nothing.+    fn drop_task_cell_content_hint(+        &self,+        _task_id: TaskId,+        _cell: CellId,+        _turbo_tasks: &TurboTasks<Self>,+    ) {+    }+     /// INVALIDATION: Be careful with this, when reader is None, it will not track dependencies, so     /// using it could break cache invalidation.     fn read_task_collectibles(
turbopack/crates/turbo-tasks/src/manager.rs7 + / 0 −
@@ -197,6 +197,9 @@ pub trait TurboTasksApi: TurboTasksCallApi + Sync + Send {         index: CellId,     ) -> Result<TypedCellContent>; +    /// See [`Backend::drop_task_cell_content_hint`].+    fn drop_task_cell_content_hint(&self, task: TaskId, index: CellId);+     fn read_own_task_cell(&self, task: TaskId, index: CellId) -> Result<TypedCellContent>;     fn update_own_task_cell(         &self,@@ -1951,6 +1954,10 @@ impl<B: Backend + 'static> TurboTasksApi for TurboTasks<B> {             .try_read_own_task_cell(current_task, index, self)     } +    fn drop_task_cell_content_hint(&self, task: TaskId, index: CellId) {+        self.backend.drop_task_cell_content_hint(task, index, self)+    }+     #[track_caller]     fn try_read_local_output(         &self,
turbopack/crates/turbo-tasks/src/vc/mod.rs18 + / 0 −
@@ -439,6 +439,24 @@ where         }     } +    /// Hint that the cell content this `Vc` points to is no longer needed, so it may be dropped to+    /// free memory. Future reads might need to recompute the value.+    ///+    /// This resolves `self` like [`Vc::to_resolved`] does (tracking the dependencies on task+    /// outputs, and waiting for them if needed), but unlike [`Vc::final_read_hint`] it never reads+    /// the cell content. If the content isn't present (e.g. it was already dropped, or it was never+    /// computed because its dependents were restored from a persistent cache), this does nothing,+    /// and in particular doesn't recompute it.+    ///+    /// This does not track any dependency on the cell.+    pub async fn drop_content_hint(self) -> Result<()> {+        let RawVcUnpacked::TaskCell(task, index) = self.node.resolve().await?.unpack() else {+            unreachable!("resolve always returns a TaskCell");+        };+        crate::turbo_tasks().drop_task_cell_content_hint(task, index);+        Ok(())+    }+     /// Returns `true` if the reference is resolved, meaning the underlying [`RawVc`] uses the     /// [`RawVcUnpacked::TaskCell`] representation.     ///
turbopack/crates/turbopack-cli/src/build/mod.rs5 + / 1 −
@@ -320,7 +320,11 @@ async fn build_internal(         true,     );     let mut module_graph = ModuleGraph::from_graphs(vec![single_graph], None);-    let binding_usage = compute_binding_usage_info(module_graph, true);+    let binding_usage = compute_binding_usage_info(+        module_graph,+        /* remove_unused_imports */ true,+        /* drop_unused_module_data */ true,+    );     let unused_references = binding_usage         .connect()         .unused_references()
turbopack/crates/turbopack-core/src/module.rs14 + / 0 −
@@ -1,3 +1,5 @@+use anyhow::Result;+use async_trait::async_trait; use turbo_rcstr::RcStr; use turbo_tasks::{ResolvedVc, ValueToString, Vc}; @@ -37,6 +39,7 @@ pub enum ModuleSideEffects { /// /// For documentation about where this is used and how it fits into the rest of Turbopack, see /// [`crate::_layers`].+#[async_trait] #[turbo_tasks::value_trait] pub trait Module {     /// The identifier of the [`Module`]. It's expected to be unique and capture all properties of@@ -77,6 +80,17 @@ pub trait Module {     /// [packagejson]: https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free     #[turbo_tasks::function]     fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects>;++    /// Hints that the module won't be code generated (e.g. because tree shaking removed it), so+    /// data that it only keeps in memory for code generation (e.g. its parsed AST) can be dropped+    /// to free memory.+    ///+    /// Implementations must not compute that data if it isn't in memory, and must stay correct if+    /// the module is code generated nevertheless (e.g. by recomputing the data). This is not a+    /// turbo-tasks function, so calling it doesn't create a task. The default does nothing.+    async fn drop_code_gen_data_hint(&self) -> Result<()> {+        Ok(())+    } }  #[turbo_tasks::value_trait]
turbopack/crates/turbopack-core/src/module_graph/binding_usage_info.rs30 + / 2 −
@@ -5,7 +5,7 @@ use auto_hash_map::AutoSet; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::Instrument; use turbo_rcstr::RcStr;-use turbo_tasks::{OperationVc, ResolvedVc, Vc};+use turbo_tasks::{OperationVc, ResolvedVc, TryJoinIterExt, Vc};  use crate::{     chunk::chunking_context::UnusedReferences,@@ -113,10 +113,17 @@ impl BindingUsageInfo {     } } +/// Computes which exports and references of the modules in `graph` are used.+///+/// With `drop_unused_module_data`, modules that are not reachable via used references (i.e. that+/// remove_unused_imports removes) are hinted to drop data they only retain for code generation, see+/// [`Module::drop_code_gen_data_hint`]. Only enable this when this graph decides which modules+/// are code generated, otherwise the data might be needed again and has to be recomputed. #[turbo_tasks::function(operation)] pub async fn compute_binding_usage_info(     graph: OperationVc<ModuleGraph>,     remove_unused_imports: bool,+    drop_unused_module_data: bool, ) -> Result<Vc<BindingUsageInfo>> {     let span_outer = tracing::info_span!(         "compute binding usage info",@@ -374,6 +381,23 @@ pub async fn compute_binding_usage_info(         span.record("visit_count", visit_count);         span.record("unused_reference_count", unused_references.len()); +        if drop_unused_module_data {+            // The traversal visits every module that is reachable via used references (and sets+            // its usage), so the remaining ones are unused.+            graph_ref+                .iter_nodes()+                .filter(|module| !used_exports.contains_key(module))+                .map(async |module| {+                    module+                        .into_trait_ref()+                        .await?+                        .drop_code_gen_data_hint()+                        .await+                })+                .try_join()+                .await?;+        }+         #[cfg(debug_assertions)]         {             use std::sync::LazyLock;@@ -713,7 +737,11 @@ mod graph_tests {             true,         );         let graph = ModuleGraph::from_graphs(vec![single_graph], None);-        let binding_usage = compute_binding_usage_info(graph, false).connect().await?;+        let binding_usage = compute_binding_usage_info(+            graph, /* remove_unused_imports */ false, /* drop_unused_module_data */ false,+        )+        .connect()+        .await?;          let escapes = names             .iter()
turbopack/crates/turbopack-ecmascript/src/lib.rs9 + / 0 −
@@ -885,7 +885,16 @@ impl EcmascriptModuleAsset { }  #[turbo_tasks::value_impl]+#[async_trait::async_trait] impl Module for EcmascriptModuleAsset {+    /// Drops the parsed AST. Analysis keeps it in memory for code generation, which drops it with+    /// `final_read_hint` after its final read.+    async fn drop_code_gen_data_hint(&self) -> Result<()> {+        // Analysis and code generation read the AST through `failsafe_parse`, which returns this+        // cell, unless parsing failed and the last successful parse is kept.+        self.parse().await?.drop_content_hint().await+    }+     #[turbo_tasks::function]     async fn ident(&self) -> Result<Vc<AssetIdent>> {         let mut ident = self.source.ident().owned().await?;
turbopack/crates/turbopack-tests/tests/execution.rs1 + / 0 −
@@ -598,6 +598,7 @@ async fn run_test_operation(prepared_test: ResolvedVc<PreparedTest>) -> Result<V         Some(compute_binding_usage_info(             module_graph,             options.remove_unused_imports,+            /* drop_unused_module_data */ false,         ))     } else {         None
turbopack/crates/turbopack-tests/tests/snapshot.rs1 + / 0 −
@@ -509,6 +509,7 @@ async fn run_test_operation(resource: RcStr) -> Result<Vc<FileSystemPath>> {         Some(compute_binding_usage_info(             module_graph,             options.remove_unused_imports,+            /* drop_unused_module_data */ false,         ))     } else {         None