vercel/next.js · #98734
Turbopack: consume graph-derived chunk groups
crates/next-api/src/app.rs53 + / 34 −
@@ -1,4 +1,4 @@-use anyhow::{Context, Result};+use anyhow::{Context, Result, ensure}; use bincode::{Decode, Encode}; use next_core::{ app_structure::{@@ -1967,46 +1967,65 @@ impl AppEndpoint { // `per_page_module_graph`), so without this the layout segments would share // very little across pages. //- // It must not be created in production. Its `parent` is *this* endpoint's- // entry chunk group, and `parent` is part of a merged group's identity, so- // the group -- and hence the availability info it seeds into the layout- // segment chain below -- would differ for every endpoint. That makes each- // shared layout segment re-chunk once per descendant endpoint. In production- // a single whole-app module graph already shares these modules, so they are- // chunked as part of the entry instead.+ // It must not be created in production. The group is the merged child of+ // *this* endpoint's entry chunk group, and that parent is part of a merged+ // group's identity, so the group -- and hence the availability info it seeds+ // into the layout segment chain below -- would differ for every endpoint.+ // That makes each shared layout segment re-chunk once per descendant+ // endpoint. In production a single whole-app module graph already shares+ // these modules, so they are chunked as part of the entry instead. if *project.per_page_module_graph().await? { let chunk_group_info = module_graph.chunk_group_info(); let span = tracing::trace_span!("server utils"); async {- let parent_chunk_group = *chunk_group_info- .get_index_of(entry_chunk_group.key())- .await?;+ let has_server_utils = !client_references.server_utils.is_empty();++ // The server utilities of this entry are merged into a single group:+ // the one merged child of the entry chunk group carrying the server+ // utility merge tag.+ let derived_group = if !has_server_utils {+ None+ } else {+ let entry_index = *chunk_group_info+ .get_index_of(entry_chunk_group.key())+ .await?;+ let merged = chunk_group_info+ .get_shared_merged_chunk_group(+ entry_index,+ NEXT_SERVER_UTILITY_MERGE_TAG,+ )+ .await?;+ merged.as_ref().map(|group| group.chunk_group.clone())+ };++ ensure!(+ !has_server_utils || derived_group.is_some(),+ "could not find a graph-derived shared merged group for server \+ utilities"+ ); - let server_utils = client_references- .server_utils- .iter()- .map(async |m| Ok(ResolvedVc::upcast(m.await?.module)))- .try_join()- .await?;- let chunk_group = chunking_context- .chunk_group(- AssetIdent::from_path(- this.app_project.project().project_path().owned().await?,+ let mut combined_chunk_group = ChunkGroupResult::empty_resolved();+ if let Some(group) = derived_group {+ combined_chunk_group = chunking_context+ .chunk_group(+ AssetIdent::from_path(+ this.app_project+ .project()+ .project_path()+ .owned()+ .await?,+ )+ .with_modifier(rcstr!("server-utils"))+ .into_vc(),+ smallvec![group],+ module_graph,+ AvailabilityInfo::root(), )- .with_modifier(rcstr!("server-utils"))- .into_vc(),- smallvec![ChunkGroup::SharedMerged {- merge_tag: NEXT_SERVER_UTILITY_MERGE_TAG.clone(),- entries: server_utils,- parent: parent_chunk_group,- }],- module_graph,- AvailabilityInfo::root(),- )- .to_resolved()- .await?;+ .to_resolved()+ .await?;+ } - current_chunk_group = chunk_group;+ current_chunk_group = combined_chunk_group; anyhow::Ok(()) }crates/next-api/src/module_graph.rs2 + / 0 −
@@ -612,6 +612,7 @@ impl ClientReferencesGraph { // Client reference used by the template, not a server component client_references.push(ClientReference {+ parent_module: node, server_component: None, ty, });@@ -665,6 +666,7 @@ impl ClientReferencesGraph { }; client_references.push(ClientReference {+ parent_module: module, server_component: Some(sc), ty, });crates/next-core/src/next_app/app_client_references_chunks.rs304 + / 97 −
@@ -1,31 +1,170 @@-use anyhow::Result;-use smallvec::smallvec;+use anyhow::{Result, bail};+use smallvec::SmallVec; use tracing::Instrument;-use turbo_rcstr::rcstr;+use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{- FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToStringRef, Vc,+ FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString,+ ValueToStringRef, Vc, }; use turbopack_core::{ chunk::{ChunkGroupResult, ChunkingContext, availability_info::AvailabilityInfo}, module::Module, module_graph::{ ModuleGraph,- chunk_group_info::{ChunkGroup, ChunkGroupKey},+ chunk_group_info::{ChunkGroup, ChunkGroupInfo}, }, output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced}, }; use crate::{ next_client_reference::{- ClientReferenceType,- ecmascript_client_reference::ecmascript_client_reference_module::{- ecmascript_client_reference_merge_tag, ecmascript_client_reference_merge_tag_ssr,- },+ CLIENT_MERGE_TAG, ClientReference, ClientReferenceType, SSR_MERGE_TAG, visit_client_reference::ClientReferenceGraphResult, }, next_server_component::server_component_module::NextServerComponentModule, }; +fn is_isolated_merged_group_for_parent(+ chunk_group: &ChunkGroup,+ parent_indices: &FxIndexSet<u32>,+ merge_tag: &RcStr,+) -> bool {+ matches!(+ chunk_group,+ ChunkGroup::IsolatedMerged {+ parent,+ merge_tag: group_merge_tag,+ ..+ } if group_merge_tag == merge_tag && parent_indices.contains(&(*parent as u32))+ )+}++/// The chunk groups a client reference's merged group may hang off.+///+/// A client reference is reached from its source module, which can sit in several chunk groups+/// when the reference is shared between routes. When the reference belongs to a server component,+/// the relevant groups are the ones that contain that server component too.+///+/// An empty intersection is not an error: in a development module graph the source module and the+/// server component do not necessarily share a chunk group, and framework-level references have no+/// server component at all. Falling back to the source module's own groups is the conservative+/// choice -- the reference is still chunked, just not narrowed to the component's groups.+fn relevant_parent_indices(+ source_indices: FxIndexSet<u32>,+ server_component_indices: Option<&FxIndexSet<u32>>,+) -> FxIndexSet<u32> {+ let Some(server_component_indices) = server_component_indices else {+ return source_indices;+ };+ let intersection = source_indices+ .intersection(server_component_indices)+ .copied()+ .collect::<FxIndexSet<_>>();+ if intersection.is_empty() {+ source_indices+ } else {+ intersection+ }+}++/// A client reference, the module it is reached from, and the server component that owns it (if+/// any).+#[derive(Clone, Copy)]+struct ClientReferenceWithContext {+ /// The module that is chunked for this client reference.+ entry_module: ResolvedVc<Box<dyn Module>>,+ /// The module the client reference is reached from.+ source_module: ResolvedVc<Box<dyn Module>>,+ /// The server component this client reference belongs to, if it belongs to one. Framework+ /// references do not.+ server_component: Option<ResolvedVc<Box<dyn Module>>>,+}++/// Returns the graph-derived isolated merged groups (tagged `merge_tag`) that chunk `references`,+/// with their entries as derived by the module graph.+async fn derived_isolated_merged_groups(+ chunk_group_info: Vc<ChunkGroupInfo>,+ references: &[ClientReferenceWithContext],+ merge_tag: &RcStr,+) -> Result<SmallVec<[ChunkGroup; 1]>> {+ let mut groups = FxIndexMap::default();++ for &ClientReferenceWithContext {+ entry_module,+ source_module,+ server_component,+ } in references+ {+ let source_groups = chunk_group_info+ .get_chunk_groups_for_module(*source_module)+ .await?;+ let server_component_indices = if let Some(server_component) = server_component {+ Some(+ chunk_group_info+ .get_chunk_groups_for_module(*server_component)+ .await?+ .iter()+ .map(|group| group.index)+ .collect::<FxIndexSet<_>>(),+ )+ } else {+ None+ };+ let source_indices = source_groups.iter().map(|group| group.index).collect();+ let parent_indices =+ relevant_parent_indices(source_indices, server_component_indices.as_ref());++ let mut found = false;+ for group in chunk_group_info+ .get_chunk_groups_for_module(*entry_module)+ .await?+ .iter()+ {+ if is_isolated_merged_group_for_parent(&group.chunk_group, &parent_indices, merge_tag) {+ found = true;+ groups+ .entry(group.index)+ .or_insert_with(|| group.chunk_group.clone());+ }+ }+ if !found {+ bail!(+ "could not find a graph-derived isolated merged group for {}",+ entry_module.ident().to_string().await?,+ );+ }+ }++ Ok(groups.into_values().collect())+}++fn client_references_by_server_component(+ app_client_references: &ClientReferenceGraphResult,+) -> FxIndexMap<ResolvedVc<NextServerComponentModule>, Vec<ClientReference>> {+ let mut client_references_by_server_component: FxIndexMap<_, Vec<_>> = FxIndexMap::default();+ let mut framework_references = Vec::new();+ for &server_component in &app_client_references.server_component_entries {+ client_references_by_server_component+ .entry(server_component)+ .or_default();+ }+ for client_reference in &app_client_references.client_references {+ if let Some(server_component) = client_reference.server_component {+ client_references_by_server_component+ .entry(server_component)+ .or_default()+ .push(*client_reference);+ } else {+ framework_references.push(*client_reference);+ }+ }+ // Framework components need to go into first layout segment.+ if let Some((_, list)) = client_references_by_server_component.first_mut() {+ list.extend(framework_references);+ }+ client_references_by_server_component+}+ #[turbo_tasks::value] pub struct ClientReferencesChunks { #[bincode(with = "turbo_bincode::indexmap")]@@ -142,28 +281,8 @@ pub async fn get_app_client_references_chunks( // } // .cell()) } else {- let mut client_references_by_server_component: FxIndexMap<_, Vec<_>> =- FxIndexMap::default();- let mut framework_reference_types = Vec::new();- for &server_component in app_client_references.server_component_entries.iter() {- client_references_by_server_component- .entry(server_component)- .or_default();- }- for client_reference in app_client_references.client_references.iter() {- if let Some(server_component) = client_reference.server_component {- client_references_by_server_component- .entry(server_component)- .or_default()- .push(client_reference.ty);- } else {- framework_reference_types.push(client_reference.ty);- }- }- // Framework components need to go into first layout segment- if let Some((_, list)) = client_references_by_server_component.first_mut() {- list.extend(framework_reference_types);- }+ let client_references_by_server_component =+ client_references_by_server_component(&app_client_references); let chunk_group_info = module_graph.chunk_group_info(); @@ -181,32 +300,35 @@ pub async fn get_app_client_references_chunks( let mut client_component_ssr_chunks = FxIndexMap::default(); let mut client_component_client_chunks = FxIndexMap::default(); - for (server_component, client_reference_types) in+ for (server_component, client_references) in client_references_by_server_component.into_iter() {- let parent_chunk_group = *chunk_group_info- .get_index_of(ChunkGroupKey::Shared(ResolvedVc::upcast(server_component)))- .await?;- let base_ident = server_component.ident().owned().await?; let server_path = server_component.server_path().owned().await?; let is_layout = server_path.file_stem() == Some("layout"); let server_component_path = server_path.to_string_ref().await?; - let ssr_modules = client_reference_types+ let ssr_modules = client_references .iter()- .map(async |client_reference_ty| {- Ok(match client_reference_ty {+ .map(async |client_reference| {+ let parent_module = client_reference.parent_module;+ Ok(match client_reference.ty { ClientReferenceType::EcmascriptClientReference( ecmascript_client_reference, ) => { let ecmascript_client_reference_ref = ecmascript_client_reference.await?; - Some(ResolvedVc::upcast(- ecmascript_client_reference_ref.ssr_module,- ))+ Some(ClientReferenceWithContext {+ entry_module: ResolvedVc::upcast(+ ecmascript_client_reference_ref.ssr_module,+ ),+ source_module: parent_module,+ server_component: client_reference+ .server_component+ .map(ResolvedVc::upcast),+ }) } _ => None, })@@ -217,73 +339,105 @@ pub async fn get_app_client_references_chunks( let ssr_chunk_group = if !ssr_modules.is_empty() && let Some(ssr_chunking_context) = ssr_chunking_context {- let availability_info = current_ssr_chunk_group.await?.availability_info;- let _span = tracing::info_span!(+ async {+ let groups = derived_isolated_merged_groups(+ chunk_group_info,+ &ssr_modules,+ &SSR_MERGE_TAG,+ )+ .await?;+ // All groups of the segment are chunked together, by a single task.+ anyhow::Ok(if groups.is_empty() {+ None+ } else {+ Some(+ ssr_chunking_context+ .chunk_group(+ base_ident+ .clone()+ .with_modifier(rcstr!("ssr modules"))+ .into_vc(),+ groups,+ module_graph,+ current_ssr_chunk_group.await?.availability_info,+ )+ .to_resolved()+ .await?,+ )+ })+ }+ .instrument(tracing::info_span!( "server side rendering", layout_segment = display(&server_component_path),- )- .entered();-- Some(- ssr_chunking_context.chunk_group(- base_ident- .clone()- .with_modifier(rcstr!("ssr modules"))- .into_vc(),- smallvec![ChunkGroup::IsolatedMerged {- parent: parent_chunk_group,- merge_tag: ecmascript_client_reference_merge_tag_ssr(),- entries: ssr_modules,- }],- module_graph,- availability_info,- ),- )+ ))+ .await? } else { None }; - let client_modules = client_reference_types+ let client_modules = client_references .iter()- .map(async |client_reference_ty| {- Ok(match client_reference_ty {- ClientReferenceType::EcmascriptClientReference(- ecmascript_client_reference,- ) => {- ResolvedVc::upcast(ecmascript_client_reference.await?.client_module)- }- ClientReferenceType::CssClientReference(css_client_reference) => {- ResolvedVc::upcast(*css_client_reference)- }+ .map(async |client_reference| {+ let parent_module = client_reference.parent_module;+ Ok(ClientReferenceWithContext {+ entry_module: match client_reference.ty {+ ClientReferenceType::EcmascriptClientReference(+ ecmascript_client_reference,+ ) => ResolvedVc::upcast(+ ecmascript_client_reference.await?.client_module,+ ),+ ClientReferenceType::CssClientReference(css_client_reference) => {+ ResolvedVc::upcast(css_client_reference)+ }+ },+ source_module: parent_module,+ server_component: client_reference+ .server_component+ .map(ResolvedVc::upcast), }) }) .try_join() .await?; let client_chunk_group = if !client_modules.is_empty() {- let availability_info = current_client_chunk_group.await?.availability_info;- let _span = tracing::info_span!(+ async {+ let groups = derived_isolated_merged_groups(+ chunk_group_info,+ &client_modules,+ &CLIENT_MERGE_TAG,+ )+ .await?;+ // All groups of the segment are chunked together, by a single task.+ anyhow::Ok(if groups.is_empty() {+ None+ } else {+ Some(+ client_chunking_context+ .chunk_group(+ base_ident+ .clone()+ .with_modifier(rcstr!("client modules"))+ .into_vc(),+ groups,+ module_graph,+ current_client_chunk_group.await?.availability_info,+ )+ .to_resolved()+ .await?,+ )+ })+ }+ .instrument(tracing::info_span!( "client side rendering", layout_segment = display(&server_component_path),- )- .entered();-- Some(client_chunking_context.chunk_group(- base_ident.with_modifier(rcstr!("client modules")).into_vc(),- smallvec![ChunkGroup::IsolatedMerged {- parent: parent_chunk_group,- merge_tag: ecmascript_client_reference_merge_tag(),- entries: client_modules,- }],- module_graph,- availability_info, ))+ .await? } else { None }; if let Some(client_chunk_group) = client_chunk_group { let client_chunk_group = current_client_chunk_group- .concatenate(client_chunk_group)+ .concatenate(*client_chunk_group) .to_resolved() .await?; @@ -297,19 +451,19 @@ pub async fn get_app_client_references_chunks( .await?; layout_segment_client_chunks.insert(server_component, assets); - for &client_reference_ty in client_reference_types.iter() {+ for client_reference in &client_references { if let ClientReferenceType::EcmascriptClientReference(_) =- client_reference_ty+ client_reference.ty { client_component_client_chunks- .insert(client_reference_ty, client_chunk_group);+ .insert(client_reference.ty, client_chunk_group); } } } if let Some(ssr_chunk_group) = ssr_chunk_group { let ssr_chunk_group = current_ssr_chunk_group- .concatenate(ssr_chunk_group)+ .concatenate(*ssr_chunk_group) .to_resolved() .await?; @@ -321,11 +475,11 @@ pub async fn get_app_client_references_chunks( .output_assets_with_referenced() .to_resolved() .await?;- for &client_reference_ty in client_reference_types.iter() {+ for client_reference in &client_references { if let ClientReferenceType::EcmascriptClientReference(_) =- client_reference_ty+ client_reference.ty {- client_component_ssr_chunks.insert(client_reference_ty, assets);+ client_component_ssr_chunks.insert(client_reference.ty, assets); } } }@@ -345,8 +499,8 @@ pub async fn get_app_client_references_chunks( /// Flattens all client-side output assets from `client_references_chunks` so the /// page's HMR chunk list can subscribe to updates for chunks built outside the-/// entry's own module graph (each `chunk_group(IsolatedMerged)` call for a-/// client component group generates chunks separately).+/// entry's own module graph (each layout segment's isolated merged client+/// reference groups are chunked separately from the entry). #[turbo_tasks::function] pub async fn get_client_references_chunks_for_hmr( client_references_chunks: Vc<ClientReferencesChunks>,@@ -376,3 +530,56 @@ pub async fn get_client_references_chunks_for_hmr( // (Node.js/Edge), not in the browser, so they don't belong in the client HMR chunk list. Ok(Vc::cell(extras.into_iter().collect())) }++#[cfg(test)]+mod tests {+ use turbo_tasks::FxIndexSet;+ use turbopack_core::module_graph::chunk_group_info::ChunkGroup;++ use crate::{+ next_app::app_client_references_chunks::{+ is_isolated_merged_group_for_parent, relevant_parent_indices,+ },+ next_client_reference::{CLIENT_MERGE_TAG, SSR_MERGE_TAG},+ };++ #[test]+ fn parent_indices_prefer_a_non_empty_output_intersection() {+ let source = FxIndexSet::from_iter([1, 2]);++ assert_eq!(+ relevant_parent_indices(source.clone(), Some(&FxIndexSet::from_iter([2, 3]))),+ FxIndexSet::from_iter([2]),+ );+ assert_eq!(+ relevant_parent_indices(source.clone(), Some(&FxIndexSet::from_iter([3]))),+ source,+ );+ assert_eq!(relevant_parent_indices(source.clone(), None), source,);+ }++ #[test]+ fn isolated_merged_groups_must_match_parent_and_tag() {+ let group = ChunkGroup::IsolatedMerged {+ parent: 7,+ merge_tag: CLIENT_MERGE_TAG,+ entries: Vec::new(),+ };++ assert!(is_isolated_merged_group_for_parent(+ &group,+ &FxIndexSet::from_iter([7]),+ &CLIENT_MERGE_TAG,+ ));+ assert!(!is_isolated_merged_group_for_parent(+ &group,+ &FxIndexSet::from_iter([8]),+ &CLIENT_MERGE_TAG,+ ));+ assert!(!is_isolated_merged_group_for_parent(+ &group,+ &FxIndexSet::from_iter([7]),+ &SSR_MERGE_TAG,+ ));+ }+}crates/next-core/src/next_client_reference/css_client_reference/css_client_reference_module.rs3 + / 1 −
@@ -13,6 +13,8 @@ use turbopack_core::{ }; use turbopack_css::chunk::CssChunkPlaceable; +use crate::next_client_reference::CLIENT_MERGE_TAG;+ /// A [`CssClientReferenceModule`] is a marker module used to indicate which /// client reference should appear in the client reference manifest. #[turbo_tasks::value]@@ -106,7 +108,7 @@ impl ModuleReference for CssClientReference { fn chunking_type(&self) -> Option<ChunkingType> { Some(ChunkingType::Isolated { _ty: ChunkGroupType::Evaluated,- merge_tag: Some(rcstr!("client")),+ merge_tag: Some(CLIENT_MERGE_TAG), }) } }crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_module.rs4 + / 9 −
@@ -34,6 +34,8 @@ use turbopack_ecmascript::{ utils::StringifyJs, }; +use crate::next_client_reference::{CLIENT_MERGE_TAG, SSR_MERGE_TAG};+ /// A [`EcmascriptClientReferenceModule`] is used in RSC to represent /// a client or SSR asset. #[turbo_tasks::value]@@ -186,13 +188,6 @@ impl EcmascriptClientReferenceModule { } } -pub fn ecmascript_client_reference_merge_tag() -> RcStr {- rcstr!("client")-}-pub fn ecmascript_client_reference_merge_tag_ssr() -> RcStr {- rcstr!("ssr")-}- #[turbo_tasks::value_impl] impl Module for EcmascriptClientReferenceModule { #[turbo_tasks::function]@@ -229,7 +224,7 @@ impl Module for EcmascriptClientReferenceModule { EcmascriptClientReference::new( *ResolvedVc::upcast(*client_module), ChunkGroupType::Evaluated,- Some(ecmascript_client_reference_merge_tag()),+ Some(CLIENT_MERGE_TAG), rcstr!("ecmascript client reference to client"), ) .to_resolved()@@ -239,7 +234,7 @@ impl Module for EcmascriptClientReferenceModule { EcmascriptClientReference::new( *ResolvedVc::upcast(*ssr_module), ChunkGroupType::Entry,- Some(ecmascript_client_reference_merge_tag_ssr()),+ Some(SSR_MERGE_TAG), rcstr!("ecmascript client reference to ssr"), ) .to_resolved()crates/next-core/src/next_client_reference/mod.rs8 + / 0 −
@@ -1,7 +1,15 @@+use turbo_rcstr::{RcStr, rcstr};+ pub(crate) mod css_client_reference; pub(crate) mod ecmascript_client_reference; pub(crate) mod visit_client_reference; +/// Merge tag of the isolated chunk groups for the client side of client references (CSS and+/// ecmascript).+pub const CLIENT_MERGE_TAG: RcStr = rcstr!("client");+/// Merge tag of the isolated chunk groups for the SSR side of ecmascript client references.+pub const SSR_MERGE_TAG: RcStr = rcstr!("ssr");+ pub use css_client_reference::{ css_client_reference_module::CssClientReferenceModule, css_client_reference_transition::NextCssClientReferenceTransition,crates/next-core/src/next_client_reference/visit_client_reference.rs3 + / 0 −
@@ -27,6 +27,9 @@ use crate::{ Copy, Clone, Eq, PartialEq, Hash, Debug, ValueDebugFormat, NonLocalValue, Encode, Decode, )] pub struct ClientReference {+ /// The module whose reference creates the client/SSR chunk group.+ pub parent_module: ResolvedVc<Box<dyn Module>>,+ /// The server component used to order this reference in the output. pub server_component: Option<ResolvedVc<NextServerComponentModule>>, pub ty: ClientReferenceType, }crates/next-core/src/next_server_utility/server_utility_reference.rs2 + / 5 −
@@ -1,5 +1,3 @@-use std::sync::LazyLock;- use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, ValueToString, Vc}; use turbopack_core::{@@ -24,8 +22,7 @@ impl NextServerUtilityModuleReference { } } -pub static NEXT_SERVER_UTILITY_MERGE_TAG: LazyLock<RcStr> =- LazyLock::new(|| rcstr!("next-server-utility"));+pub const NEXT_SERVER_UTILITY_MERGE_TAG: RcStr = rcstr!("next-server-utility"); #[turbo_tasks::value_impl] impl ModuleReference for NextServerUtilityModuleReference {@@ -37,7 +34,7 @@ impl ModuleReference for NextServerUtilityModuleReference { fn chunking_type(&self) -> Option<ChunkingType> { Some(ChunkingType::Shared { inherit_async: true,- merge_tag: Some(NEXT_SERVER_UTILITY_MERGE_TAG.clone()),+ merge_tag: Some(NEXT_SERVER_UTILITY_MERGE_TAG), }) } test/e2e/app-dir/layout-segment-chunk-sharing/layout-segment-chunk-sharing.test.ts57 + / 2 −
@@ -141,6 +141,7 @@ describe('layout-segment-chunk-sharing', () => { ) mcpPort = await findPort()+ let traceServerOutput = '' traceServerProcess = spawn( 'node', [@@ -152,7 +153,37 @@ describe('layout-segment-chunk-sharing', () => { '--mcp-port', String(mcpPort), ],- { stdio: 'inherit' }+ { stdio: 'pipe' }+ )+ const recordTraceServerOutput = (chunk: Buffer) => {+ const text = chunk.toString()+ traceServerOutput += text+ process.stdout.write(text)+ }+ traceServerProcess.stdout?.on('data', recordTraceServerOutput)+ traceServerProcess.stderr?.on('data', recordTraceServerOutput)++ // The MCP server can answer while the trace is still being read, so+ // wait for the reader's end-of-file summary before querying spans.+ // Large traces report `Initial read completed`; smaller ones report+ // only their span count.+ await retry(+ async () => {+ const output = traceServerOutput.replace(+ // eslint-disable-next-line no-control-regex+ /\x1b\[[0-9;]*[A-Za-z]/g,+ ''+ )+ if (+ !/Initial read completed|(?:^|[\r\n])\d+ spans(?:[\r\n]|$)/.test(+ output+ )+ ) {+ throw new Error('Trace file is still being read')+ }+ },+ 120_000,+ 500 ) // Wait for the MCP HTTP server to be ready.@@ -177,7 +208,7 @@ describe('layout-segment-chunk-sharing', () => { 30_000, 500 )- }, 120_000)+ }, 180_000) afterAll(async () => { if (traceServerProcess?.pid) {@@ -231,6 +262,30 @@ describe('layout-segment-chunk-sharing', () => { 1 ) })++ // The root layout's client references (e.g. the framework's layout+ // router) belong to one isolated merged chunk group per page that renders+ // the layout. Those groups are chunked together, so the root layout's+ // client modules and its SSR modules are each chunked by a single task,+ // instead of once per group (`client modules 0`, `client modules 1`, ...).+ it('chunks all client reference groups of a segment in one task', async () => {+ const spans = await findSpans(+ 'app/layout.tsx [app-rsc] (ecmascript, Next.js Server Component, '+ )+ const chunkingOf = (context: string, modifier: string) =>+ spans.filter((name) =>+ new RegExp(+ `^turbopack_${context}::chunking_context chunking \\[project\\]/(?:.*/)?app/layout\\.tsx \\[app-rsc\\] \\(ecmascript, Next\\.js Server Component, ${modifier}(?: \\d+)?\\)$`+ ).test(name)+ )++ expect(chunkingOf('browser', 'client modules')).toEqual([+ expect.stringMatching(/, client modules\)$/),+ ])+ expect(chunkingOf('nodejs', 'ssr modules')).toEqual([+ expect.stringMatching(/, ssr modules\)$/),+ ])+ }) } ) })turbopack/crates/turbopack-core/src/chunk/chunk_group.rs11 + / 4 −
@@ -313,16 +313,23 @@ async fn chunk_group_content_operation( collecting_modules: FxIndexSet::default(), }; + let chunk_group_info = module_graph.chunk_group_info();+ let chunk_group_indices = chunk_groups+ .iter()+ .map(async |chunk_group| Ok(*chunk_group_info.get_index_of(chunk_group.key()).await?))+ .try_join()+ .await?;+ let chunk_group_info = chunk_group_info.await?; let available_modules = match availability_info.available_modules() { Some(available_modules) => Some(available_modules.snapshot().await?), None => None, }; - // The entries of all groups, in list order. An entry shared by several groups is traversed- // once.- let entries = chunk_groups+ // The ordered entries of all groups, in list order. An entry shared by several groups is+ // traversed once.+ let entries = chunk_group_indices .iter()- .flat_map(|chunk_group| chunk_group.entries())+ .flat_map(|&index| module_batches_graph.get_ordered_entries(&chunk_group_info, index)) .collect::<FxIndexSet<_>>() .into_iter() .map(|entry| module_batches_graph.get_entry_index(entry))turbopack/crates/turbopack-core/src/module_graph/chunk_group_info.rs59 + / 0 −
@@ -82,6 +82,18 @@ impl Hash for RoaringBitmapWrapper { #[turbo_tasks::value(transparent, cell = "keyed")] pub struct ModuleToChunkGroups(FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper>); +#[turbo_tasks::value]+pub struct ChunkGroupWithIndex {+ pub index: u32,+ pub chunk_group: ChunkGroup,+}++#[turbo_tasks::value(transparent)]+pub struct ChunkGroups(Vec<ChunkGroupWithIndex>);++#[turbo_tasks::value(transparent)]+pub struct OptionChunkGroupWithIndex(Option<ChunkGroupWithIndex>);+ #[turbo_tasks::value] pub struct ChunkGroupInfo { pub module_chunk_groups: ResolvedVc<ModuleToChunkGroups>,@@ -142,6 +154,53 @@ impl ChunkGroupInfo { } } }++ /// Returns the exact chunk groups that contain `module`, as derived while traversing the module+ /// graph.+ #[turbo_tasks::function]+ pub async fn get_chunk_groups_for_module(+ &self,+ module: ResolvedVc<Box<dyn Module>>,+ ) -> Result<Vc<ChunkGroups>> {+ let module_chunk_groups = self.module_chunk_groups.await?;+ let Some(groups) = module_chunk_groups.get(&module) else {+ return Ok(Vc::cell(Vec::new()));+ };+ Ok(Vc::cell(+ groups+ .iter()+ .map(|index| ChunkGroupWithIndex {+ index,+ chunk_group: self.chunk_groups[index as usize].clone(),+ })+ .collect(),+ ))+ }++ /// Returns the shared merged chunk group with `merge_tag` that `parent` is the parent of, if+ /// the graph produced one.+ ///+ /// A merge tag collects everything reachable through the tagged shared references of one+ /// parent group into a single group, so there is at most one such group per parent and tag.+ #[turbo_tasks::function]+ pub fn get_shared_merged_chunk_group(+ &self,+ parent: usize,+ merge_tag: RcStr,+ ) -> Vc<OptionChunkGroupWithIndex> {+ let key = ChunkGroupKey::SharedMerged {+ parent: ChunkGroupId::from(parent),+ merge_tag,+ };+ Vc::cell(+ self.chunk_group_keys+ .get_index_of(&key)+ .map(|index| ChunkGroupWithIndex {+ index: index as u32,+ chunk_group: self.chunk_groups[index].clone(),+ }),+ )+ } } /// Per-entry chunking heuristics.