use futures::Future;
use indexmap::IndexMap;
use rspack_collections::{IdentifierIndexMap, IdentifierMap};
use rspack_error::Result;
use rspack_util::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},
incremental::{IncrementalPasses, Mutation},
};
#[derive(Debug, Default)]
pub struct CodeSplittingCache {
chunk_by_ukey: ChunkByUkey,
chunk_graph: ChunkGraph,
chunk_group_by_ukey: ChunkGroupByUkey,
entrypoints: IndexMap<String, ChunkGroupUkey>,
async_entrypoints: Vec<ChunkGroupUkey>,
named_chunk_groups: HashMap<String, ChunkGroupUkey>,
named_chunks: HashMap<String, ChunkUkey>,
pub(crate) code_splitter: CodeSplitter,
pub(crate) module_idx: IdentifierMap<(u32, u32)>,
}
impl CodeSplittingCache {
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 {
let logger = this_compilation.get_logger("rspack.Compilation.codeSplittingCache");
if !this_compilation.entries.keys().eq(
this_compilation
.build_chunk_graph_artifact
.code_splitting_cache
.entrypoints
.keys(),
) {
logger.log("entrypoints change detected, rebuilding chunk graph");
return false;
}
let Some(mutations) = this_compilation
.incremental
.mutations_read(IncrementalPasses::MAKE)
else {
logger.log("incremental for make disabled, rebuilding chunk graph");
return false;
};
if mutations
.iter()
.any(|mutation| matches!(mutation, Mutation::ModuleRemove { .. }))
{
logger.log("module removal detected, rebuilding chunk graph");
return false;
}
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 = &self.code_splitter.block_modules_runtime_map;
if previous_modules_map.is_empty() {
logger.log("no cache detected, rebuilding chunk graph");
return false;
}
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 {
for conn in connections {
if conn
.active_state(module_graph, None, module_graph_cache)
.is_not_false()
{
res.push(m);
continue 'outer;
}
}
}
res
};
let mut previous_modules = IdentifierIndexMap::default();
let all_runtimes = previous_modules_map.values();
let mut miss_in_previous = true;
all_runtimes.for_each(|modules| {
let Some(outgoings) = modules.get(&DependenciesBlockIdentifier::Module(module)) else {
return;
};
miss_in_previous = false;
for (outgoing, state, _) in outgoings.iter() {
previous_modules
.entry(*outgoing)
.and_modify(|v| {
if state.is_not_false() {
*v = *state;
}
})
.or_insert(*state);
}
});
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()
{
logger.log(format!("module outgoings change detected: {module}"));
return false;
}
}
true
}
}
#[instrument(name = "Compilation:code_splitting",target=TRACING_BENCH_TARGET, skip_all)]
pub(crate) async fn use_code_splitting_cache<'a, T, F>(
compilation: &'a mut Compilation,
task: T,
) -> Result<()>
where
T: Fn(&'a mut Compilation) -> F,
F: Future<Output = Result<&'a mut Compilation>>,
{
if !compilation.incremental.enabled() {
task(compilation).await?;
return Ok(());
}
let incremental_code_splitting = compilation
.incremental
.passes_enabled(IncrementalPasses::BUILD_CHUNK_GRAPH);
let no_change = compilation
.build_chunk_graph_artifact
.code_splitting_cache
.can_skip_rebuilding(compilation);
if incremental_code_splitting || no_change {
let cache = &mut compilation.build_chunk_graph_artifact.code_splitting_cache;
rayon::scope(|s| {
s.spawn(|_| compilation.chunk_by_ukey = cache.chunk_by_ukey.clone());
s.spawn(|_| compilation.chunk_graph = cache.chunk_graph.clone());
s.spawn(|_| compilation.chunk_group_by_ukey = cache.chunk_group_by_ukey.clone());
s.spawn(|_| compilation.entrypoints = cache.entrypoints.clone());
s.spawn(|_| compilation.async_entrypoints = cache.async_entrypoints.clone());
s.spawn(|_| compilation.named_chunk_groups = cache.named_chunk_groups.clone());
s.spawn(|_| compilation.named_chunks = cache.named_chunks.clone());
});
if no_change {
let module_idx = cache.module_idx.clone();
let module_graph = compilation.get_module_graph_mut();
for (m, (pre, post)) in module_idx {
let mgm = module_graph.module_graph_module_by_identifier_mut(&m);
mgm.pre_order_index = Some(pre);
mgm.post_order_index = Some(post);
}
return Ok(());
}
}
let compilation = task(compilation).await?;
let cache = &mut compilation.build_chunk_graph_artifact.code_splitting_cache;
rayon::scope(|s| {
s.spawn(|_| cache.chunk_by_ukey = compilation.chunk_by_ukey.clone());
s.spawn(|_| cache.chunk_graph = compilation.chunk_graph.clone());
s.spawn(|_| cache.chunk_group_by_ukey = compilation.chunk_group_by_ukey.clone());
s.spawn(|_| cache.entrypoints = compilation.entrypoints.clone());
s.spawn(|_| cache.async_entrypoints = compilation.async_entrypoints.clone());
s.spawn(|_| cache.named_chunk_groups = compilation.named_chunk_groups.clone());
s.spawn(|_| cache.named_chunks = compilation.named_chunks.clone());
});
let mg = compilation.get_module_graph();
let mut map = IdentifierMap::default();
for (mid, mgm) in mg.module_graph_modules() {
let (Some(pre), Some(post)) = (mgm.pre_order_index, mgm.post_order_index) else {
continue;
};
map.insert(mid, (pre, post));
}
let cache = &mut compilation.build_chunk_graph_artifact.code_splitting_cache;
cache.module_idx = map;
Ok(())
}
#[derive(Debug, Default)]
pub struct BuildChunkGraphArtifact {
pub code_splitting_cache: CodeSplittingCache,
}
impl ArtifactExt for BuildChunkGraphArtifact {
const PASS: IncrementalPasses = IncrementalPasses::BUILD_CHUNK_GRAPH;
}