From 688d4d90d7686d9ccae4103778fc515910e3bd8a Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Wed, 8 Apr 2026 18:34:42 +0800 Subject: [PATCH] refactor(split-chunks): prune cache group matching --- .../src/raw_options/raw_split_chunks/mod.rs | 28 +-- .../src/split_chunks.rs | 24 ++- .../rspack_plugin_split_chunks/src/common.rs | 63 ++++++- .../src/module_group.rs | 1 + .../src/plugin/module_group.rs | 160 +++++++++++++++--- .../tests/module_layer_filter.rs | 24 +++ .../tests/module_type_filter.rs | 21 +++ .../min-chunks-short-circuit/index.js | 1 + .../min-chunks-short-circuit/rspack.config.js | 26 +++ .../min-chunks-short-circuit/test.config.js | 5 + .../type-layer-short-circuit/index.js | 1 + .../type-layer-short-circuit/rspack.config.js | 24 +++ .../type-layer-short-circuit/test.config.js | 5 + 13 files changed, 329 insertions(+), 54 deletions(-) create mode 100644 crates/rspack_plugin_split_chunks/tests/module_layer_filter.rs create mode 100644 crates/rspack_plugin_split_chunks/tests/module_type_filter.rs create mode 100644 tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/index.js create mode 100644 tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/rspack.config.js create mode 100644 tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/test.config.js create mode 100644 tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/index.js create mode 100644 tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/rspack.config.js create mode 100644 tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/test.config.js diff --git a/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs b/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs index c8d00fa156f5..499d981fde56 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs @@ -301,11 +301,8 @@ fn create_module_type_filter( raw: Either, ) -> rspack_plugin_split_chunks::ModuleTypeFilter { match raw { - Either::A(regex) => Arc::new(move |m| regex.test(m.module_type().as_str())), - Either::B(js_str) => { - let type_str = js_str.into_string(); - Arc::new(move |m| m.module_type().as_str() == type_str.as_str()) - } + Either::A(regex) => rspack_plugin_split_chunks::ModuleTypeFilter::Regex(regex), + Either::B(js_str) => rspack_plugin_split_chunks::ModuleTypeFilter::String(js_str.into_string()), } } @@ -313,26 +310,13 @@ fn create_module_layer_filter( raw: Either3, bool>>, ) -> rspack_plugin_split_chunks::ModuleLayerFilter { match raw { - Either3::A(regex) => Arc::new(move |layer| { - let regex = regex.clone(); - Box::pin(async move { Ok(layer.map(|layer| regex.test(&layer)).unwrap_or_default()) }) - }), + Either3::A(regex) => rspack_plugin_split_chunks::ModuleLayerFilter::Regex(regex), Either3::B(js_str) => { - let test = js_str.into_string(); - Arc::new(move |layer| { - let test = test.clone(); - Box::pin(async move { - Ok(if let Some(layer) = layer { - layer.starts_with(&test) - } else { - test.is_empty() - }) - }) - }) + rspack_plugin_split_chunks::ModuleLayerFilter::String(js_str.into_string()) } - Either3::C(f) => Arc::new(move |layer| { + Either3::C(f) => rspack_plugin_split_chunks::ModuleLayerFilter::Func(Arc::new(move |layer| { let f = f.clone(); Box::pin(async move { f.call_with_sync(layer).await }) - }), + })), } } diff --git a/crates/rspack_plugin_esm_library/src/split_chunks.rs b/crates/rspack_plugin_esm_library/src/split_chunks.rs index 9fbba07e2dbe..94ffaa82392c 100644 --- a/crates/rspack_plugin_esm_library/src/split_chunks.rs +++ b/crates/rspack_plugin_esm_library/src/split_chunks.rs @@ -174,16 +174,28 @@ async fn matches_module_to_cache_group( } // match r#type - if !(cache_group.r#type)(module) { + if !cache_group + .r#type + .test_internal(module.module_type().as_str()) + { return Ok(false); } // match layer - if !(cache_group.layer)(module.get_layer().map(ToString::to_string)) - .await - .to_rspack_result() - .unwrap_or(false) - { + let layer = module.get_layer(); + let satisfied_layer = if cache_group.layer.is_func() { + cache_group + .layer + .test_func(layer.cloned()) + .await + .to_rspack_result() + .unwrap_or(false) + } else { + cache_group + .layer + .test_internal(layer.map(|layer| layer.as_str())) + }; + if !satisfied_layer { return Ok(false); } diff --git a/crates/rspack_plugin_split_chunks/src/common.rs b/crates/rspack_plugin_split_chunks/src/common.rs index bb0669f7e01e..5686f27a33b4 100644 --- a/crates/rspack_plugin_split_chunks/src/common.rs +++ b/crates/rspack_plugin_split_chunks/src/common.rs @@ -7,7 +7,7 @@ use derive_more::Debug; use futures::future::BoxFuture; use rayon::prelude::*; use rspack_collections::IdentifierMap; -use rspack_core::{ChunkUkey, Compilation, Module, ModuleIdentifier, SourceType}; +use rspack_core::{ChunkUkey, Compilation, ModuleIdentifier, SourceType}; use rspack_error::Result; use rspack_regex::RspackRegex; use rustc_hash::{FxHashMap, FxHashSet}; @@ -66,16 +66,69 @@ impl ChunkFilter { } } -pub type ModuleTypeFilter = Arc bool + Send + Sync>; -pub type ModuleLayerFilter = +#[derive(Clone)] +pub enum ModuleTypeFilter { + All, + Regex(RspackRegex), + String(String), +} + +impl ModuleTypeFilter { + pub fn test_internal(&self, module_type: &str) -> bool { + match self { + Self::All => true, + Self::Regex(re) => re.test(module_type), + Self::String(expected) => expected == module_type, + } + } +} + +pub type ModuleLayerFilterFn = Arc) -> BoxFuture<'static, Result> + Send + Sync>; +#[derive(Clone)] +pub enum ModuleLayerFilter { + Func(ModuleLayerFilterFn), + All, + Regex(RspackRegex), + String(String), +} + +impl ModuleLayerFilter { + pub fn is_func(&self) -> bool { + matches!(self, Self::Func(_)) + } + + pub async fn test_func(&self, layer: Option) -> Result { + if let Self::Func(func) = self { + func(layer).await + } else { + panic!("ModuleLayerFilter is not a function"); + } + } + + pub fn test_internal(&self, layer: Option<&str>) -> bool { + match self { + Self::Func(_) => panic!("ModuleLayerFilter is a function"), + Self::All => true, + Self::Regex(re) => layer.is_some_and(|layer| re.test(layer)), + Self::String(test) => { + if let Some(layer) = layer { + layer.starts_with(test) + } else { + test.is_empty() + } + } + } + } +} + pub fn create_default_module_type_filter() -> ModuleTypeFilter { - Arc::new(|_| true) + ModuleTypeFilter::All } pub fn create_default_module_layer_filter() -> ModuleLayerFilter { - Arc::new(|_| Box::pin(async move { Ok(true) })) + ModuleLayerFilter::All } pub fn create_async_chunk_filter() -> ChunkFilter { diff --git a/crates/rspack_plugin_split_chunks/src/module_group.rs b/crates/rspack_plugin_split_chunks/src/module_group.rs index dec34023ce0f..3cc0d44959e1 100644 --- a/crates/rspack_plugin_split_chunks/src/module_group.rs +++ b/crates/rspack_plugin_split_chunks/src/module_group.rs @@ -11,6 +11,7 @@ use crate::{ common::{ModuleSizes, SplitChunkSizes}, }; +#[derive(Clone, Copy)] pub(crate) struct IndexedCacheGroup<'a> { pub cache_group_index: u32, pub cache_group: &'a CacheGroup, diff --git a/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs b/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs index 40323693d365..5274d7c1e046 100644 --- a/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs +++ b/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs @@ -23,7 +23,7 @@ use tracing::instrument; use super::ModuleGroupMap; use crate::{ SplitChunksPlugin, - common::{ChunkFilter, ModuleChunks, ModuleSizes}, + common::{ChunkFilter, ModuleChunks, ModuleSizes, ModuleTypeFilter}, min_size::remove_min_size_violating_modules, module_group::{IndexedCacheGroup, ModuleGroup, ModuleGroupKey, compare_entries}, options::{ @@ -34,6 +34,57 @@ use crate::{ }; type ChunksKey = u64; +type ChunkFilterCacheKey = (u32, ChunksKey); + +struct TypeFilteredCacheGroups<'a> { + all: Vec>, + regex: Vec>, + exact: FxHashMap>>, +} + +impl<'a> TypeFilteredCacheGroups<'a> { + fn new(cache_groups: &[IndexedCacheGroup<'a>]) -> Self { + let mut all = Vec::new(); + let mut regex = Vec::new(); + let mut exact = FxHashMap::default(); + + for cache_group in cache_groups.iter().copied() { + match &cache_group.cache_group.r#type { + ModuleTypeFilter::All => all.push(cache_group), + ModuleTypeFilter::Regex(_) => regex.push(cache_group), + ModuleTypeFilter::String(module_type) => exact + .entry(module_type.clone()) + .or_insert_with(Vec::new) + .push(cache_group), + } + } + + Self { all, regex, exact } + } + + fn candidates(&self, module_type: &str) -> Vec> { + let mut candidates = Vec::with_capacity( + self.all.len() + self.regex.len() + self.exact.get(module_type).map_or(0, Vec::len), + ); + + candidates.extend(self.all.iter().copied()); + + if let Some(exact) = self.exact.get(module_type) { + candidates.extend(exact.iter().copied()); + } + + candidates.extend( + self + .regex + .iter() + .copied() + .filter(|cache_group| cache_group.cache_group.r#type.test_internal(module_type)), + ); + + candidates.sort_unstable_by(|a, b| a.compare_by_index(b)); + candidates + } +} #[derive(Clone)] struct ChunkCombination { @@ -349,34 +400,85 @@ impl SplitChunksPlugin { ) -> Result { let module_graph = compilation.get_module_graph(); let module_group_map: FxDashMap = FxDashMap::default(); + let type_filtered_cache_groups = TypeFilteredCacheGroups::new(&cache_groups); + let sync_chunk_filter_result_cache: FxDashMap>> = + FxDashMap::default(); let module_group_results = rspack_parallel::scope::<_, Result<_>>(|token| { all_modules.iter().for_each(|mid| { - let s = unsafe { token.used((&cache_groups, mid, &module_graph, compilation, &module_group_map, &combinator, module_chunks, removed_module_chunks, chunk_index_map)) }; - s.spawn(|(cache_groups, mid, module_graph, compilation, module_group_map, combinator, module_chunks, removed_module_chunks, chunk_index_map)| async move { + let s = unsafe { token.used((&type_filtered_cache_groups, &sync_chunk_filter_result_cache, mid, &module_graph, compilation, &module_group_map, &combinator, module_chunks, removed_module_chunks, chunk_index_map)) }; + s.spawn(|(type_filtered_cache_groups, sync_chunk_filter_result_cache, mid, module_graph, compilation, module_group_map, combinator, module_chunks, removed_module_chunks, chunk_index_map)| async move { let belong_to_chunks = module_chunks.get(mid).expect("should have module chunks"); if belong_to_chunks.is_empty() { return Ok(()); } - if let Some(removed_chunks) = removed_module_chunks.get(mid) && belong_to_chunks.iter().all(|c| removed_chunks.contains(c)) { + let removed_chunks = removed_module_chunks.get(mid); + let available_chunks_upper_bound = removed_chunks.map_or(belong_to_chunks.len(), |removed| { + belong_to_chunks.len() - belong_to_chunks.intersection(removed).count() + }); + + if available_chunks_upper_bound == 0 { return Ok(()); } + let module = module_graph.module_by_identifier(mid).expect("should have module").as_ref(); + let module_type = module.module_type().as_str(); + let layer = module.get_layer().map(|layer| layer.as_str()); + let mut name_for_condition: Option>> = None; + let mut sync_chunk_filter_upper_bounds: FxHashMap = FxHashMap::default(); let mut filtered = vec![]; - for cache_group in cache_groups.iter() { - let mut is_match = true; - // Filter by `splitChunks.cacheGroups.{cacheGroup}.type` - is_match &= (cache_group.cache_group.r#type)(module); + for cache_group in type_filtered_cache_groups.candidates(module_type) { + if available_chunks_upper_bound < cache_group.cache_group.min_chunks as usize { + continue; + } + + if !cache_group.cache_group.chunk_filter.is_func() { + let selected_chunks_upper_bound = *sync_chunk_filter_upper_bounds + .entry(cache_group.cache_group_index) + .or_insert_with(|| match &cache_group.cache_group.chunk_filter { + ChunkFilter::All => available_chunks_upper_bound, + chunk_filter => belong_to_chunks + .iter() + .filter(|chunk| { + !removed_chunks.is_some_and(|chunks| chunks.contains(chunk)) + && chunk_filter.test_internal(chunk, compilation) + }) + .count(), + }); + + if selected_chunks_upper_bound < cache_group.cache_group.min_chunks as usize { + continue; + } + } + // Filter by `splitChunks.cacheGroups.{cacheGroup}.layer` - is_match &= (cache_group.cache_group.layer)(module.get_layer().map(ToString::to_string)).await?; + let is_match = if cache_group.cache_group.layer.is_func() { + cache_group + .cache_group + .layer + .test_func(layer.map(str::to_owned)) + .await? + } else { + cache_group + .cache_group + .layer + .test_internal(layer) + }; + if !is_match { + continue; + } // Filter by `splitChunks.cacheGroups.{cacheGroup}.test` - is_match &= match &cache_group.cache_group.test { - CacheGroupTest::String(str) => module - .name_for_condition().is_some_and(|name| name.starts_with(str)), - CacheGroupTest::RegExp(regexp) => module - .name_for_condition().is_some_and(|name| regexp.test(&name)), + let is_match = match &cache_group.cache_group.test { + CacheGroupTest::String(str) => name_for_condition + .get_or_insert_with(|| module.name_for_condition()) + .as_deref() + .is_some_and(|name| name.starts_with(str)), + CacheGroupTest::RegExp(regexp) => name_for_condition + .get_or_insert_with(|| module.name_for_condition()) + .as_deref() + .is_some_and(|name| regexp.test(name)), CacheGroupTest::Fn(f) => { let ctx = CacheGroupTestFnCtx { compilation, module }; f(ctx).await?.unwrap_or_default() @@ -384,10 +486,13 @@ impl SplitChunksPlugin { CacheGroupTest::Enabled => true, }; - if is_match { - filtered.push(cache_group); + if !is_match { + continue; } + + filtered.push(cache_group); } + let mut used_exports_combs = None; let mut non_used_exports_combs = None; @@ -455,9 +560,19 @@ impl SplitChunksPlugin { } ).copied().collect::>() } else { - chunk_combination.iter().filter(|c| { - cache_group.chunk_filter.test_internal(c, compilation) - }).copied().collect::>() + let cache_key = (cache_group_index, chunk_combination.key); + if let Some(selected_chunks) = sync_chunk_filter_result_cache.get(&cache_key) { + selected_chunks.value().as_ref().clone() + } else { + let selected_chunks = chunk_combination.iter().filter(|c| { + cache_group.chunk_filter.test_internal(c, compilation) + }).copied().collect::>(); + sync_chunk_filter_result_cache.insert( + cache_key, + Arc::new(selected_chunks.clone()), + ); + selected_chunks + } }; // Filter by `splitChunks.cacheGroups.{cacheGroup}.minChunks` @@ -472,7 +587,10 @@ impl SplitChunksPlugin { continue; } - if selected_chunks.iter().any(|c| removed_module_chunks.get(mid).is_some_and(|chunks| chunks.contains(c))) { + if selected_chunks + .iter() + .any(|c| removed_chunks.is_some_and(|chunks| chunks.contains(c))) + { continue; } let selected_chunks_key = matches!(&cache_group.chunk_filter, ChunkFilter::All) @@ -481,7 +599,7 @@ impl SplitChunksPlugin { MatchedItem { module, cache_group, - cache_group_index: *cache_group_index, + cache_group_index, selected_chunks, selected_chunks_key, }, diff --git a/crates/rspack_plugin_split_chunks/tests/module_layer_filter.rs b/crates/rspack_plugin_split_chunks/tests/module_layer_filter.rs new file mode 100644 index 000000000000..4b915fe4f2a6 --- /dev/null +++ b/crates/rspack_plugin_split_chunks/tests/module_layer_filter.rs @@ -0,0 +1,24 @@ +use rspack_plugin_split_chunks::{ModuleLayerFilter, create_default_module_layer_filter}; + +#[test] +fn default_layer_filter_is_sync_and_matches_everything() { + let filter = create_default_module_layer_filter(); + assert!(!filter.is_func()); + assert!(filter.test_internal(None)); + assert!(filter.test_internal(Some("any-layer"))); +} + +#[test] +fn string_and_regex_layer_filters_match_without_async() { + let string_filter = ModuleLayerFilter::String("app".to_string()); + assert!(!string_filter.is_func()); + assert!(string_filter.test_internal(Some("app/shared"))); + assert!(!string_filter.test_internal(Some("other/shared"))); + + let regex_filter = ModuleLayerFilter::Regex( + rspack_regex::RspackRegex::with_flags("^app", "").expect("regex should compile"), + ); + assert!(!regex_filter.is_func()); + assert!(regex_filter.test_internal(Some("app/shared"))); + assert!(!regex_filter.test_internal(Some("other/shared"))); +} diff --git a/crates/rspack_plugin_split_chunks/tests/module_type_filter.rs b/crates/rspack_plugin_split_chunks/tests/module_type_filter.rs new file mode 100644 index 000000000000..b6f6f44516ed --- /dev/null +++ b/crates/rspack_plugin_split_chunks/tests/module_type_filter.rs @@ -0,0 +1,21 @@ +use rspack_plugin_split_chunks::{ModuleTypeFilter, create_default_module_type_filter}; + +#[test] +fn default_module_type_filter_is_sync_and_matches_everything() { + let filter = create_default_module_type_filter(); + assert!(filter.test_internal("javascript/auto")); + assert!(filter.test_internal("css")); +} + +#[test] +fn string_and_regex_module_type_filters_match_synchronously() { + let string_filter = ModuleTypeFilter::String("css".to_string()); + assert!(string_filter.test_internal("css")); + assert!(!string_filter.test_internal("javascript/auto")); + + let regex_filter = ModuleTypeFilter::Regex( + rspack_regex::RspackRegex::with_flags("^javascript", "").expect("regex should compile"), + ); + assert!(regex_filter.test_internal("javascript/auto")); + assert!(!regex_filter.test_internal("css")); +} diff --git a/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/index.js b/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/index.js new file mode 100644 index 000000000000..efeee5db16c0 --- /dev/null +++ b/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/index.js @@ -0,0 +1 @@ +export const value = 1; diff --git a/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/rspack.config.js b/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/rspack.config.js new file mode 100644 index 000000000000..2ee1ae46e1d2 --- /dev/null +++ b/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/rspack.config.js @@ -0,0 +1,26 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + entry: { + main: './index', + }, + target: 'async-node', + output: { + filename: '[name].js', + }, + optimization: { + splitChunks: { + chunks: 'all', + minSize: 0, + cacheGroups: { + skipped: { + minChunks: 2, + test() { + throw new Error( + 'TEST_SHOULD_NOT_RUN_WHEN_MIN_CHUNKS_IS_IMPOSSIBLE', + ); + }, + }, + }, + }, + }, +}; diff --git a/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/test.config.js b/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/test.config.js new file mode 100644 index 000000000000..47f38baadd79 --- /dev/null +++ b/tests/rspack-test/configCases/split-chunks/min-chunks-short-circuit/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle() { + return ["main.js"]; + } +}; diff --git a/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/index.js b/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/index.js new file mode 100644 index 000000000000..efeee5db16c0 --- /dev/null +++ b/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/index.js @@ -0,0 +1 @@ +export const value = 1; diff --git a/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/rspack.config.js b/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/rspack.config.js new file mode 100644 index 000000000000..d41e78a90a62 --- /dev/null +++ b/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/rspack.config.js @@ -0,0 +1,24 @@ +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + entry: { + main: './index', + }, + target: 'async-node', + output: { + filename: '[name].js', + }, + optimization: { + splitChunks: { + chunks: 'all', + minSize: 0, + cacheGroups: { + skipped: { + type: 'css', + layer() { + throw new Error('LAYER_SHOULD_NOT_RUN_WHEN_TYPE_IS_FALSE'); + }, + }, + }, + }, + }, +}; diff --git a/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/test.config.js b/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/test.config.js new file mode 100644 index 000000000000..47f38baadd79 --- /dev/null +++ b/tests/rspack-test/configCases/split-chunks/type-layer-short-circuit/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle() { + return ["main.js"]; + } +};