Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 119 additions & 111 deletions crates/rspack_core/src/artifacts/build_chunk_graph_artifact.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
use std::mem;

use futures::Future;
use rspack_collections::{IdentifierIndexMap, IdentifierMap};
use rspack_collections::IdentifierMap;
use rspack_error::Result;
use rspack_util::{fx_hash::FxIndexMap, tracing_preset::TRACING_BENCH_TARGET};
use rustc_hash::FxHashMap as HashMap;
use tracing::instrument;

use crate::{
ArtifactExt, ChunkByUkey, ChunkGraph, ChunkGroupByUkey, ChunkGroupUkey, ChunkUkey, Compilation,
Logger, ModuleIdentifier,
build_chunk_graph::code_splitter::{CodeSplitter, DependenciesBlockIdentifier},
ArtifactExt, AsyncDependenciesBlockIdentifier, ChunkByUkey, ChunkGraph, ChunkGroupByUkey,
ChunkGroupUkey, ChunkUkey, Compilation, DependencyLocation, Logger, ModuleIdentifier,
build_chunk_graph::code_splitter::CodeSplitter,
fast_set,
incremental::{IncrementalPasses, Mutation},
};

struct ChunkGroupOriginUpdate {
block: AsyncDependenciesBlockIdentifier,
module: ModuleIdentifier,
loc: Option<DependencyLocation>,
request: Option<String>,
}

#[derive(Default)]
struct CodeSplittingReusePlan {
origin_updates: Vec<ChunkGroupOriginUpdate>,
}

#[derive(Debug, Default)]
pub struct BuildChunkGraphArtifact {
pub chunk_by_ukey: ChunkByUkey,
Expand All @@ -33,15 +45,10 @@ impl BuildChunkGraphArtifact {
fast_set(&mut self.code_splitter, code_splitter);
}

// we can skip rebuilding chunk graph if none of modules
// has changed its outgoings
// we don't need to check if module has changed its incomings
// if it changes, the incoming module changes its outgoings as well
fn can_skip_rebuilding(&self, this_compilation: &Compilation) -> bool {
self.can_skip_rebuilding_legacy(this_compilation)
}

fn can_skip_rebuilding_legacy(&self, this_compilation: &Compilation) -> bool {
fn plan_code_splitting_reuse(
&self,
this_compilation: &Compilation,
) -> Option<CodeSplittingReusePlan> {
let logger = this_compilation.get_logger("rspack.Compilation.codeSplittingCache");

if !this_compilation.entries.keys().eq(
Expand All @@ -51,7 +58,7 @@ impl BuildChunkGraphArtifact {
.keys(),
) {
logger.log("entrypoints change detected, rebuilding chunk graph");
return false;
return None;
}

let Some(mutations) = this_compilation
Expand All @@ -60,122 +67,116 @@ impl BuildChunkGraphArtifact {
else {
logger.log("incremental for build module graph disabled, rebuilding chunk graph");
// if disable incremental for build module graph phase, we can't skip rebuilding
return false;
return None;
};

// if we have module removal, we can't skip rebuilding
if mutations
.iter()
.any(|mutation| matches!(mutation, Mutation::ModuleRemove { .. }))
{
logger.log("module removal detected, rebuilding chunk graph");
return false;
return None;
}

let module_graph = this_compilation.get_module_graph();
let module_graph_cache = &this_compilation.module_graph_cache_artifact;
let affected_modules = mutations.get_affected_modules_with_module_graph(module_graph);
let previous_modules_map = &this_compilation
.build_chunk_graph_artifact
.code_splitter
.block_modules_runtime_map;
let affected_modules = mutations
.get_affected_modules_with_module_graph(module_graph)
.into_iter()
.collect::<Vec<_>>();
let previous_splitter = &self.code_splitter;

if previous_modules_map.is_empty() {
if previous_splitter.ordinal_by_module.is_empty() {
logger.log("no cache detected, rebuilding chunk graph");
return false;
return None;
}

for module in affected_modules {
let outgoings: Vec<ModuleIdentifier> = {
let mut res = vec![];
let mut active_modules = IdentifierIndexMap::<Vec<_>>::default();
let module = module_graph
.module_graph_module_by_identifier(&module)
.expect("should have module");
module
.all_dependencies()
.iter()
.filter(|dep_id| {
module_graph
.dependency_by_id(dep_id)
.as_module_dependency()
.is_none_or(|module_dep| !module_dep.weak())
})
.filter_map(|dep| module_graph.connection_by_dependency_id(dep))
.for_each(|conn| {
let m = *conn.module_identifier();
active_modules.entry(m).or_default().push(conn);
});

'outer: for (m, connections) in active_modules {
let side_effects_state_artifact = &this_compilation
.build_module_graph_artifact
.side_effects_state_artifact;
for conn in connections {
if conn
.active_state(
module_graph,
None,
module_graph_cache,
side_effects_state_artifact,
&this_compilation.exports_info_artifact,
)
.is_not_false()
{
res.push(m);
continue 'outer;
}
}
}
if affected_modules
.iter()
.any(|module| !previous_splitter.ordinal_by_module.contains_key(module))
{
logger.log("new module detected, rebuilding chunk graph");
return None;
}

res
};
let mut current_splitter = CodeSplitter::default();
if current_splitter
.prepare(&affected_modules, this_compilation)
.is_err()
{
logger.log("failed to prepare current block values, rebuilding chunk graph");
return None;
}

// get outgoings from all runtimes in the previous compilation
let mut previous_modules = IdentifierIndexMap::default();
let all_runtimes = previous_modules_map.values();
let mut miss_in_previous = true;
let mut plan = CodeSplittingReusePlan::default();
for module in affected_modules {
if !previous_splitter.module_code_splitting_value_equal(
&mut current_splitter,
module,
this_compilation,
) {
logger.log(format!(
"module code splitting value changed: {module}, rebuilding chunk graph"
));
return None;
}

all_runtimes.for_each(|modules| {
let Some(outgoings) = modules.get(&DependenciesBlockIdentifier::Module(module)) else {
return;
let module = module_graph
.module_by_identifier(&module)
.expect("affected module should exist");
for block_id in module.get_blocks() {
let Some(origin_index) = previous_splitter.block_origin_indices.get(block_id) else {
continue;
};
miss_in_previous = false;

for (outgoing, state, _) in outgoings.iter() {
// we must insert module even if state is false
// because we need to keep the import order
previous_modules
.entry(*outgoing)
.and_modify(|v| {
if state.is_not_false() {
*v = *state;
}
})
.or_insert(*state);
let Some(chunk_group_ukey) = self
.chunk_graph
.block_to_chunk_group_ukey
.get(block_id)
.copied()
else {
logger.log("cached block chunk group is missing, rebuilding chunk graph");
return None;
};
let chunk_group = self.chunk_group_by_ukey.expect_get(&chunk_group_ukey);
if chunk_group.origins().get(*origin_index).is_none() {
logger.log("cached block origin is missing, rebuilding chunk graph");
return None;
}
});

if miss_in_previous {
logger.log("new module detected, rebuilding chunk graph");
return false;
}

if previous_modules
.iter()
.filter(|(_, conn_state)| conn_state.is_not_false())
.map(|(m, _)| *m)
.collect::<Vec<_>>()
!= outgoings.clone()
{
// we find one module's outgoings has changed
// we cannot skip rebuilding
logger.log(format!("module outgoings change detected: {module}"));
return false;
let block = module_graph.block_by_id_expect(block_id);
plan.origin_updates.push(ChunkGroupOriginUpdate {
block: *block_id,
module: *block.parent(),
loc: block.loc(),
request: block.request().clone(),
});
}
}

true
Some(plan)
}

fn apply_code_splitting_reuse(&mut self, plan: CodeSplittingReusePlan) {
for update in plan.origin_updates {
let chunk_group_ukey = *self
.chunk_graph
.block_to_chunk_group_ukey
.get(&update.block)
.expect("validated block chunk group should exist");
let origin_index = *self
.code_splitter
.block_origin_indices
.get(&update.block)
.expect("validated block origin should exist");
self
.chunk_group_by_ukey
.expect_get_mut(&chunk_group_ukey)
.update_origin(
origin_index,
Some(update.module),
update.loc,
update.request,
);
}
}

/// Reset cached chunks back to the initial render state.
Expand Down Expand Up @@ -224,12 +225,19 @@ where
let incremental_code_splitting = compilation
.incremental
.passes_enabled(IncrementalPasses::BUILD_CHUNK_GRAPH);
let no_change = incremental_code_splitting
&& compilation
let reuse_plan = incremental_code_splitting
.then(|| {
compilation
.build_chunk_graph_artifact
.plan_code_splitting_reuse(compilation)
})
.flatten();

if let Some(reuse_plan) = reuse_plan {
compilation
.build_chunk_graph_artifact
.can_skip_rebuilding(compilation);
.apply_code_splitting_reuse(reuse_plan);

if no_change {
let module_idx = &compilation.build_chunk_graph_artifact.module_idx;
let module_graph = compilation
.build_module_graph_artifact
Expand Down
20 changes: 19 additions & 1 deletion crates/rspack_core/src/chunk_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,30 @@ impl ChunkGroup {
module_id: Option<ModuleIdentifier>,
loc: Option<DependencyLocation>,
request: Option<String>,
) {
) -> usize {
let index = self.origins.len();
self.origins.push(OriginRecord {
module: module_id,
loc,
request,
});
index
}

pub(crate) fn update_origin(
&mut self,
index: usize,
module: Option<ModuleIdentifier>,
loc: Option<DependencyLocation>,
request: Option<String>,
) {
let origin = self
.origins
.get_mut(index)
.expect("origin index should be valid");
origin.module = module;
origin.loc = loc;
origin.request = request;
}

pub fn origins(&self) -> &[OriginRecord] {
Expand Down
Loading
Loading