Skip to content

Commit 080b12e

Browse files
authored
perf(pack): isolate multi-page endpoints (#3297)
* perf(pack): isolate multi-page endpoints * fix(pack): preserve multi-page endpoint semantics * fix(pack): scope dev stats to entrypoints * fix(pack): align node target watcher detection
1 parent 1165169 commit 080b12e

38 files changed

Lines changed: 1497 additions & 343 deletions

crates/pack-api/src/app.rs

Lines changed: 267 additions & 201 deletions
Large diffs are not rendered by default.

crates/pack-api/src/entrypoint.rs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
operation::EntrypointsOperation,
1515
project::ProjectContainer,
1616
utils::get_issues,
17-
webpack_stats::generate_webpack_stats,
17+
webpack_stats::{OutputAssetGroups, generate_webpack_stats},
1818
};
1919

2020
#[turbo_tasks::value(shared)]
@@ -59,15 +59,19 @@ pub async fn all_output_assets_operation(
5959
) -> Result<Vc<OutputAssets>> {
6060
let project = container.project();
6161

62-
let endpoint_assets = project
62+
let endpoint_asset_groups = project
6363
.get_all_endpoints()
6464
.await?
6565
.iter()
66-
.map(|endpoint| async move { endpoint.output().await?.output_assets.await })
66+
.map(|endpoint| async move { Ok(endpoint.output().await?.output_assets) })
6767
.try_join()
6868
.await?;
6969

70-
let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_assets
70+
let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_asset_groups
71+
.iter()
72+
.map(|assets| async move { assets.await })
73+
.try_join()
74+
.await?
7175
.iter()
7276
.flat_map(|assets| assets.iter().copied())
7377
.collect();
@@ -89,22 +93,56 @@ pub async fn all_output_assets_operation(
8993
let mut stats_outputs: Vec<ResolvedVc<Box<dyn OutputAsset>>> = Vec::new();
9094

9195
if !has_server {
92-
stats_outputs.push(make_stats_output(output_assets, dist_root).await?);
96+
stats_outputs.push(
97+
make_stats_output(
98+
output_assets,
99+
Vc::<OutputAssetGroups>::cell(endpoint_asset_groups),
100+
dist_root,
101+
)
102+
.await?,
103+
);
93104
} else {
94105
let server_dist_root_vc = container.project().server_dist_root();
95106
let server_dist_root_read = server_dist_root_vc.await?;
96107
let mut client: Vec<ResolvedVc<Box<dyn OutputAsset>>> = Vec::new();
97108
let mut server: Vec<ResolvedVc<Box<dyn OutputAsset>>> = Vec::new();
109+
let mut client_groups = Vec::with_capacity(endpoint_asset_groups.len());
110+
for assets in endpoint_asset_groups {
111+
let mut group = Vec::new();
112+
for asset in assets.await?.iter().copied() {
113+
if !asset.path().await?.is_inside_ref(&server_dist_root_read) {
114+
group.push(asset);
115+
}
116+
}
117+
if !group.is_empty() {
118+
client_groups.push(ResolvedVc::cell(group));
119+
}
120+
}
98121
for asset in output_assets.await?.iter().copied() {
99122
if asset.path().await?.is_inside_ref(&server_dist_root_read) {
100123
server.push(asset);
101124
} else {
102125
client.push(asset);
103126
}
104127
}
105-
stats_outputs.push(make_stats_output(Vc::cell(client), dist_root).await?);
128+
stats_outputs.push(
129+
make_stats_output(
130+
Vc::cell(client),
131+
Vc::<OutputAssetGroups>::cell(client_groups),
132+
dist_root,
133+
)
134+
.await?,
135+
);
106136
if !server.is_empty() {
107-
stats_outputs.push(make_stats_output(Vc::cell(server), server_dist_root_vc).await?);
137+
let server_assets = ResolvedVc::cell(server);
138+
stats_outputs.push(
139+
make_stats_output(
140+
*server_assets,
141+
Vc::<OutputAssetGroups>::cell(vec![server_assets]),
142+
server_dist_root_vc,
143+
)
144+
.await?,
145+
);
108146
}
109147
}
110148

@@ -113,9 +151,10 @@ pub async fn all_output_assets_operation(
113151

114152
async fn make_stats_output(
115153
assets: Vc<OutputAssets>,
154+
asset_groups: Vc<OutputAssetGroups>,
116155
dist_root: Vc<FileSystemPath>,
117156
) -> Result<ResolvedVc<Box<dyn OutputAsset>>> {
118-
let webpack_stats = generate_webpack_stats(assets, dist_root).await?;
157+
let webpack_stats = generate_webpack_stats(assets, asset_groups, dist_root).await?;
119158
let stats_json = serde_json::to_string_pretty(&*webpack_stats)?;
120159
let dist_root_owned = dist_root.owned().await?;
121160
let stats_output = VirtualOutputAsset::new(

crates/pack-api/src/project.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,12 +1489,7 @@ impl Project {
14891489
let app_project = self.app_project().to_resolved().await?.await?;
14901490
Ok(Entrypoints {
14911491
apps: match *app_project {
1492-
Some(app) => Some(
1493-
Endpoints(vec![ResolvedVc::upcast(
1494-
app.get_app_endpoint().to_resolved().await?,
1495-
)])
1496-
.resolved_cell(),
1497-
),
1492+
Some(app) => Some(app.get_app_endpoints().to_resolved().await?),
14981493
None => None,
14991494
},
15001495
libraries: match *library_project {

crates/pack-api/src/webpack_stats.rs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub struct AssetIntermediateInfo {
3131
pub dev_chunk_list: Option<RcStr>,
3232
}
3333

34+
#[turbo_tasks::value(transparent)]
35+
pub struct OutputAssetGroups(pub Vec<ResolvedVc<OutputAssets>>);
36+
3437
fn normalize_stats_path(path: RcStr) -> RcStr {
3538
path.strip_prefix("./").map(Into::into).unwrap_or(path)
3639
}
@@ -381,6 +384,7 @@ pub async fn get_asset_intermediate_info(
381384
#[turbo_tasks::function]
382385
pub async fn generate_webpack_stats(
383386
entry_assets: Vc<OutputAssets>,
387+
entry_asset_groups: Vc<OutputAssetGroups>,
384388
dist_root: Vc<FileSystemPath>,
385389
) -> Result<Vc<WebpackStats>> {
386390
let mut assets = vec![];
@@ -402,10 +406,13 @@ pub async fn generate_webpack_stats(
402406
})
403407
.try_join()
404408
.await?;
409+
let asset_info_by_asset: FxHashMap<_, _> = all_assets
410+
.iter()
411+
.copied()
412+
.zip(asset_results.iter())
413+
.collect();
405414

406-
let mut dev_chunk_lists: Vec<RcStr> = vec![];
407-
for info in asset_results {
408-
let info = info;
415+
for info in &asset_results {
409416
if seen_asset_paths.insert(info.asset.name.clone()) {
410417
assets.push(info.asset.clone());
411418
}
@@ -424,17 +431,39 @@ pub async fn generate_webpack_stats(
424431
modules.insert(module.id.clone(), module.clone());
425432
}
426433
}
427-
if let Some(dev_chunk_list) = &info.dev_chunk_list {
428-
dev_chunk_lists.push(dev_chunk_list.clone());
429-
}
430434
}
431435

432-
for dev_chunk_list in dev_chunk_lists {
433-
for entrypoint in entrypoints.values_mut() {
434-
entrypoint.chunks.push(dev_chunk_list.clone());
435-
entrypoint.assets.push(WebpackStatsEntrypointAssets {
436-
name: dev_chunk_list.clone(),
437-
});
436+
// Endpoint output groups preserve which evaluate entry owns each development chunk list.
437+
// Associating these lists after flattening all output assets made every entrypoint include
438+
// every other page's HMR bootstrap in multi-page builds.
439+
for group in entry_asset_groups.await?.iter().copied() {
440+
let group = group.await?;
441+
let group_entrypoints: FxIndexMap<_, _> = group
442+
.iter()
443+
.filter_map(|asset| asset_info_by_asset.get(asset))
444+
.flat_map(|info| info.entrypoints.iter())
445+
.map(|(name, _)| (name.clone(), ()))
446+
.collect();
447+
let group_chunk_lists: FxIndexMap<_, _> = group
448+
.iter()
449+
.filter_map(|asset| asset_info_by_asset.get(asset))
450+
.filter_map(|info| info.dev_chunk_list.as_ref())
451+
.map(|name| (name.clone(), ()))
452+
.collect();
453+
454+
for entrypoint_name in group_entrypoints.keys() {
455+
let Some(entrypoint) = entrypoints.get_mut(entrypoint_name) else {
456+
continue;
457+
};
458+
for dev_chunk_list in group_chunk_lists.keys() {
459+
if entrypoint.chunks.contains(dev_chunk_list) {
460+
continue;
461+
}
462+
entrypoint.chunks.push(dev_chunk_list.clone());
463+
entrypoint.assets.push(WebpackStatsEntrypointAssets {
464+
name: dev_chunk_list.clone(),
465+
});
466+
}
438467
}
439468
}
440469

crates/pack-cli/src/serve/source.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,17 @@ pub async fn create_web_entry_source(
2323
) -> Result<Vc<Box<dyn ContentSource>>> {
2424
let entries = match &*project.app_project().await? {
2525
Some(app_project) => {
26-
let app_endpoint = app_project.get_app_endpoint();
27-
28-
let asset_context = Vc::upcast(app_endpoint.app_module_context());
29-
30-
let runtime_entries = app_endpoint.app_runtime_entries();
31-
32-
let chunking_context = app_endpoint
26+
let asset_context = Vc::upcast(app_project.app_module_context());
27+
let runtime_entries = app_project.app_runtime_entries();
28+
let chunking_context = app_project
3329
.project()
3430
.client_chunking_context()
3531
.to_resolved()
3632
.await?;
3733

38-
app_endpoint
34+
app_project
35+
.resolved_entrypoints()
3936
.await?
40-
.entrypoints
4137
.iter()
4238
.map(async |app| {
4339
let module_graph = app

0 commit comments

Comments
 (0)