From 85113ac32cc62ee74d4abd3f33c0b814d8d296b1 Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Fri, 3 Apr 2026 19:03:54 +0800 Subject: [PATCH 1/9] perf(module-concat): add bailout witness model --- .../src/plugin/module_concatenation_plugin.rs | 236 ++++++++++++------ .../__snapshots__/stats.txt | 26 +- 2 files changed, 169 insertions(+), 93 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index 73ed474faba8..b206f947ee06 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -20,17 +20,74 @@ use rspack_core::{ }; use rspack_error::{Result, ToStringResultToRspackResultExt}; use rspack_hook::{plugin, plugin_hook}; -use rspack_util::itoa; +use rspack_util::{atom::Atom, itoa}; use rustc_hash::FxHashMap as HashMap; fn format_bailout_reason(msg: &str) -> String { format!("ModuleConcatenation bailout: {msg}") } +#[derive(Clone, Debug)] +enum BailoutWitness { + AsyncModule, + NotStrict, + NotInAnyChunk, + EntryPoint, + Deferred, + UnknownReexport { + export_name: Option, + used_info: String, + }, + DynamicExports { + export_name: Option, + provided_info: String, + used_info: String, + }, +} + +impl BailoutWitness { + fn format_export_name(export_name: Option<&Atom>) -> Cow<'_, str> { + export_name.map_or_else( + || Cow::Borrowed("other exports"), + |name| Cow::Owned(name.to_string()), + ) + } + + fn format_reason(&self) -> Cow<'static, str> { + match self { + Self::AsyncModule => Cow::Borrowed("Module is async"), + Self::NotStrict => Cow::Borrowed("Module is not in strict mode"), + Self::NotInAnyChunk => Cow::Borrowed("Module is not in any chunk"), + Self::EntryPoint => Cow::Borrowed("Module is an entry point"), + Self::Deferred => Cow::Borrowed("Module is deferred"), + Self::UnknownReexport { + export_name, + used_info, + } => Cow::Owned(format!( + "Reexports in this module do not have a static target (first hit: {} : {})", + Self::format_export_name(export_name.as_ref()), + used_info + )), + Self::DynamicExports { + export_name, + provided_info, + used_info, + } => Cow::Owned(format!( + "List of module exports is dynamic (first hit: {} : {} and {})", + Self::format_export_name(export_name.as_ref()), + provided_info, + used_info + )), + } + } +} + #[derive(Clone, Debug)] enum Warning { Id(ModuleIdentifier), Problem(String), + #[allow(dead_code)] + Witness(BailoutWitness), } #[derive(Debug, Clone)] @@ -140,7 +197,13 @@ impl RuntimeIdentifierCache { impl ModuleConcatenationPlugin { fn format_bailout_warning(&self, module: ModuleIdentifier, warning: &Warning) -> String { match warning { - Warning::Problem(id) => format_bailout_reason(&format!("Cannot concat with {module}: {id}")), + Warning::Problem(problem) => { + format_bailout_reason(&format!("Cannot concat with {module}: {problem}")) + } + Warning::Witness(witness) => format_bailout_reason(&format!( + "Cannot concat with {module}: {}", + witness.format_reason() + )), Warning::Id(id) => { let reason = self.get_inner_bailout_reason(id); let reason_with_prefix = match reason { @@ -909,7 +972,8 @@ impl ModuleConcatenationPlugin { .map(|module_id| { let mut can_be_root = true; let mut can_be_inner = true; - let mut bailout_reason = vec![]; + let mut string_bailout_reason = None; + let mut bailout_witnesses = vec![]; let number_of_module_chunks = compilation .build_chunk_graph_artifact .chunk_graph @@ -927,98 +991,88 @@ impl ModuleConcatenationPlugin { module_graph, &compilation.build_chunk_graph_artifact.chunk_graph, ) { - bailout_reason.push(reason); - return (false, false, module_id, bailout_reason); + string_bailout_reason = Some(reason); + return ( + false, + false, + module_id, + string_bailout_reason, + bailout_witnesses, + ); } if ModuleGraph::is_async(&compilation.async_modules_artifact, &module_id) { - bailout_reason.push("Module is async".into()); - return (false, false, module_id, bailout_reason); + bailout_witnesses.push(BailoutWitness::AsyncModule); + return ( + false, + false, + module_id, + string_bailout_reason, + bailout_witnesses, + ); } if !m.build_info().strict { - bailout_reason.push("Module is not in strict mode".into()); - return (false, false, module_id, bailout_reason); + bailout_witnesses.push(BailoutWitness::NotStrict); + return ( + false, + false, + module_id, + string_bailout_reason, + bailout_witnesses, + ); } if number_of_module_chunks == 0 { - bailout_reason.push("Module is not in any chunk".into()); - return (false, false, module_id, bailout_reason); + bailout_witnesses.push(BailoutWitness::NotInAnyChunk); + return ( + false, + false, + module_id, + string_bailout_reason, + bailout_witnesses, + ); } let exports_info = compilation .exports_info_artifact .get_prefetched_exports_info(&module_id, PrefetchExportsInfoMode::Default); let relevant_exports = exports_info.get_relevant_exports(None); - let unknown_exports = relevant_exports - .iter() - .filter(|export_info| { - export_info.is_reexport() - && !matches!( - get_target( - export_info, - module_graph, - &compilation.exports_info_artifact, - &|_| true, - &mut Default::default() - ), - Some(GetTargetResult::Target(_)) - ) - }) - .copied() - .collect::>(); - if !unknown_exports.is_empty() { - let cur_bailout_reason = unknown_exports - .into_iter() - .map(|export_info| { - let name = export_info - .name() - .map_or("other exports".to_string(), |name| name.to_string()); - format!("{} : {}", name, export_info.get_used_info()) - }) - .collect::>() - .join(", "); - // self.set_bailout_reason( - // &module_id, - // format!("Reexports in this module do not have a static target ({bailout_reason})"), - // &mut module_graph, - // ); - - bailout_reason.push( - format!("Reexports in this module do not have a static target ({cur_bailout_reason})") - .into(), + let unknown_export = relevant_exports.iter().find(|export_info| { + export_info.is_reexport() + && !matches!( + get_target( + export_info, + module_graph, + &compilation.exports_info_artifact, + &|_| true, + &mut Default::default() + ), + Some(GetTargetResult::Target(_)) + ) + }); + if let Some(export_info) = unknown_export { + bailout_witnesses.push(BailoutWitness::UnknownReexport { + export_name: export_info.name().cloned(), + used_info: export_info.get_used_info().to_string(), + }); + return ( + false, + false, + module_id, + string_bailout_reason, + bailout_witnesses, ); - - return (false, false, module_id, bailout_reason); } - let unknown_provided_exports = relevant_exports + let unknown_provided_export = relevant_exports .iter() - .filter(|export_info| !matches!(export_info.provided(), Some(ExportProvided::Provided))) - .copied() - .collect::>(); + .find(|export_info| !matches!(export_info.provided(), Some(ExportProvided::Provided))); - if !unknown_provided_exports.is_empty() { - let cur_bailout_reason = unknown_provided_exports - .into_iter() - .map(|export_info| { - let name = export_info - .name() - .map_or("other exports".to_string(), |name| name.to_string()); - format!( - "{} : {} and {}", - name, - export_info.get_provided_info(), - export_info.get_used_info(), - ) - }) - .collect::>() - .join(", "); - // self.set_bailout_reason( - // &module_id, - // format!("List of module exports is dynamic ({bailout_reason})"), - // &mut module_graph, - // ); - bailout_reason - .push(format!("List of module exports is dynamic ({cur_bailout_reason})").into()); + if let Some(export_info) = unknown_provided_export { + bailout_witnesses.push(BailoutWitness::DynamicExports { + export_name: export_info.name().cloned(), + provided_info: export_info.get_provided_info().to_string(), + used_info: export_info.get_used_info().to_string(), + }); can_be_root = false; } @@ -1029,15 +1083,21 @@ impl ModuleConcatenationPlugin { // &mut module_graph, // ); can_be_inner = false; - bailout_reason.push("Module is an entry point".into()); + bailout_witnesses.push(BailoutWitness::EntryPoint); } if module_graph.is_deferred(&compilation.imported_by_defer_modules_artifact, &module_id) { - bailout_reason.push("Module is deferred".into()); + bailout_witnesses.push(BailoutWitness::Deferred); can_be_inner = false; } - (can_be_root, can_be_inner, module_id, bailout_reason) + ( + can_be_root, + can_be_inner, + module_id, + string_bailout_reason, + bailout_witnesses, + ) // if can_be_root { // relevant_modules.push(module_id); // } @@ -1047,6 +1107,22 @@ impl ModuleConcatenationPlugin { }) .collect(); + let res: Vec<_> = res + .into_iter() + .map( + |(can_be_root, can_be_inner, module_id, string_bailout_reason, bailout_witnesses)| { + let mut bailout_reasons = Vec::with_capacity( + bailout_witnesses.len() + usize::from(string_bailout_reason.is_some()), + ); + if let Some(reason) = string_bailout_reason { + bailout_reasons.push(reason); + } + bailout_reasons.extend(bailout_witnesses.iter().map(BailoutWitness::format_reason)); + (can_be_root, can_be_inner, module_id, bailout_reasons) + }, + ) + .collect(); + let module_graph = compilation.get_module_graph_mut(); for (can_be_root, can_be_inner, module_id, bailout_reason) in res { diff --git a/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt b/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt index 57b5a7b059d1..266ab347f4a7 100644 --- a/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt +++ b/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt @@ -4,18 +4,18 @@ cacheable modules xx KiB modules by path ./node_modules/big-module/*.js xx bytes ./node_modules/big-module/index.js xx bytes [built] [code generated] [only some exports used: huh] - ModuleConcatenation bailout: Reexports in this module do not have a static target (huh : used in main) + ModuleConcatenation bailout: Reexports in this module do not have a static target (first hit: huh : used in main) ./node_modules/big-module/a.js xx bytes [built] [code generated] [only some exports used: a, huh] - ModuleConcatenation bailout: List of module exports is dynamic (huh : maybe provided (runtime-defined) and used in main) + ModuleConcatenation bailout: List of module exports is dynamic (first hit: huh : maybe provided (runtime-defined) and used in main) ./node_modules/big-module/log.js xx bytes [built] [code generated] [only some exports used: huh] Statement with side_effects in source code at ./node_modules/big-module/log.js - ModuleConcatenation bailout: List of module exports is dynamic (huh : maybe provided (runtime-defined) and used in main) + ModuleConcatenation bailout: List of module exports is dynamic (first hit: huh : maybe provided (runtime-defined) and used in main) modules by path ./node_modules/module-with-export/*.js xx KiB ./node_modules/module-with-export/index.js xx KiB [built] [code generated] [only some exports used: huh, smallVar] - ModuleConcatenation bailout: List of module exports is dynamic (huh : maybe provided (runtime-defined) and used in main) + ModuleConcatenation bailout: List of module exports is dynamic (first hit: huh : maybe provided (runtime-defined) and used in main) ./node_modules/module-with-export/emptyModule.js xx bytes [built] [code generated] [used exports unknown] ModuleConcatenation bailout: Module is not an ECMAScript module @@ -24,9 +24,9 @@ cacheable modules xx KiB [no exports used] Statement with side_effects in source code at ./index.js-31 ModuleConcatenation bailout: Module is an entry point - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/a.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (huh : used in main) - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (huh : used in main) - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/module-with-export/index.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (huh : used in main) + ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/a.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) + ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) + ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/module-with-export/index.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) Rspack x.x.x compiled successfully in X s asset main.no-side.js xx KiB [emitted] (name: main) @@ -35,17 +35,17 @@ cacheable modules xx KiB modules by path ./node_modules/big-module/*.js xx bytes ./node_modules/big-module/index.js xx bytes [built] [code generated] [only some exports used: a, huh] - ModuleConcatenation bailout: Reexports in this module do not have a static target (huh : used in main) + ModuleConcatenation bailout: Reexports in this module do not have a static target (first hit: huh : used in main) ./node_modules/big-module/a.js xx bytes [built] [code generated] [only some exports used: a, huh] - ModuleConcatenation bailout: List of module exports is dynamic (huh : maybe provided (runtime-defined) and used in main) + ModuleConcatenation bailout: List of module exports is dynamic (first hit: huh : maybe provided (runtime-defined) and used in main) ./node_modules/big-module/log.js xx bytes [built] [code generated] [only some exports used: huh] - ModuleConcatenation bailout: List of module exports is dynamic (huh : maybe provided (runtime-defined) and used in main) + ModuleConcatenation bailout: List of module exports is dynamic (first hit: huh : maybe provided (runtime-defined) and used in main) modules by path ./node_modules/module-with-export/*.js xx KiB ./node_modules/module-with-export/index.js xx KiB [built] [code generated] [only some exports used: huh, smallVar] - ModuleConcatenation bailout: List of module exports is dynamic (huh : maybe provided (runtime-defined) and used in main) + ModuleConcatenation bailout: List of module exports is dynamic (first hit: huh : maybe provided (runtime-defined) and used in main) ./node_modules/module-with-export/emptyModule.js xx bytes [built] [code generated] [used exports unknown] ModuleConcatenation bailout: Module is not an ECMAScript module @@ -53,6 +53,6 @@ cacheable modules xx KiB [no exports] [no exports used] ModuleConcatenation bailout: Module is an entry point - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (huh : used in main) - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/module-with-export/index.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (huh : used in main) + ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) + ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/module-with-export/index.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) Rspack x.x.x compiled successfully in X s \ No newline at end of file From 1f26f8ea2f350683004150d0af57507a6cc6cef6 Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Fri, 3 Apr 2026 19:23:31 +0800 Subject: [PATCH 2/9] perf(module-concat): short-circuit bailout witnesses --- .../src/plugin/module_concatenation_plugin.rs | 316 ++++++++++-------- .../__snapshots__/stats.txt | 2 +- 2 files changed, 173 insertions(+), 145 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index b206f947ee06..903353f2d8bd 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -34,6 +34,29 @@ enum BailoutWitness { NotInAnyChunk, EntryPoint, Deferred, + MissingChunk { + module: String, + expected_chunk: String, + actual_chunk: String, + }, + NonModuleReference { + module: String, + }, + CrossChunkImporter { + module: String, + importer: String, + }, + UnsupportedSyntaxImporter { + module: String, + importer: String, + syntax: String, + }, + RuntimeDependentImporter { + module: String, + importer: String, + expected_runtime: String, + referenced_runtime: String, + }, UnknownReexport { export_name: Option, used_info: String, @@ -60,6 +83,32 @@ impl BailoutWitness { Self::NotInAnyChunk => Cow::Borrowed("Module is not in any chunk"), Self::EntryPoint => Cow::Borrowed("Module is an entry point"), Self::Deferred => Cow::Borrowed("Module is deferred"), + Self::MissingChunk { + module, + expected_chunk, + actual_chunk, + } => Cow::Owned(format!( + "Module {module} is not in the same chunk(s) (expected in chunk(s) {expected_chunk}, module is in chunk(s) {actual_chunk})" + )), + Self::NonModuleReference { module } => Cow::Owned(format!("Module {module} is referenced")), + Self::CrossChunkImporter { module, importer } => Cow::Owned(format!( + "Module {module} is referenced from a different chunk by {importer}" + )), + Self::UnsupportedSyntaxImporter { + module, + importer, + syntax, + } => Cow::Owned(format!( + "Module {module} is referenced from {importer} with unsupported syntax ({syntax})" + )), + Self::RuntimeDependentImporter { + module, + importer, + expected_runtime, + referenced_runtime, + } => Cow::Owned(format!( + "Module {module} is runtime-dependent referenced by {importer} (expected runtime {expected_runtime}, module is only referenced in {referenced_runtime})" + )), Self::UnknownReexport { export_name, used_info, @@ -85,8 +134,6 @@ impl BailoutWitness { #[derive(Clone, Debug)] enum Warning { Id(ModuleIdentifier), - Problem(String), - #[allow(dead_code)] Witness(BailoutWitness), } @@ -197,9 +244,6 @@ impl RuntimeIdentifierCache { impl ModuleConcatenationPlugin { fn format_bailout_warning(&self, module: ModuleIdentifier, warning: &Warning) -> String { match warning { - Warning::Problem(problem) => { - format_bailout_reason(&format!("Cannot concat with {module}: {problem}")) - } Warning::Witness(witness) => format_bailout_reason(&format!( "Cannot concat with {module}: {}", witness.format_reason() @@ -384,36 +428,39 @@ impl ModuleConcatenationPlugin { .collect(); if !missing_chunks.is_empty() { - let problem_string = { - let mut missing_chunks_list = missing_chunks - .iter() - .map(|&chunk| { - let chunk = chunk_by_ukey.expect_get(chunk); - chunk.name().unwrap_or("unnamed chunk(s)") - }) - .collect::>(); - missing_chunks_list.sort_unstable(); - - let mut chunks = chunk_graph - .get_module_chunks(*module_id) - .iter() - .map(|&chunk| { - let chunk = chunk_by_ukey.expect_get(&chunk); - chunk.name().unwrap_or("unnamed chunk(s)") - }) - .collect::>(); - chunks.sort_unstable(); - - format!( - "Module {} is not in the same chunk(s) (expected in chunk(s) {}, module is in chunk(s) {})", - module_readable_identifier, - missing_chunks_list.join(", "), - chunks.join(", ") - ) - }; - statistics.incorrect_chunks += 1; - let problem = Warning::Problem(problem_string); + let mut missing_chunks_list = missing_chunks + .iter() + .map(|&chunk| { + let chunk = chunk_by_ukey.expect_get(chunk); + chunk.name().unwrap_or("unnamed chunk(s)") + }) + .collect::>(); + missing_chunks_list.sort_unstable(); + + let mut chunks = chunk_graph + .get_module_chunks(*module_id) + .iter() + .map(|&chunk| { + let chunk = chunk_by_ukey.expect_get(&chunk); + chunk.name().unwrap_or("unnamed chunk(s)") + }) + .collect::>(); + chunks.sort_unstable(); + + let problem = Warning::Witness(BailoutWitness::MissingChunk { + module: module_readable_identifier, + expected_chunk: missing_chunks_list + .first() + .copied() + .unwrap_or("unnamed chunk(s)") + .to_string(), + actual_chunk: chunks + .first() + .copied() + .unwrap_or("unnamed chunk(s)") + .to_string(), + }); failure_cache.insert(*module_id, problem.clone()); return Some(problem); } @@ -442,23 +489,9 @@ impl ModuleConcatenationPlugin { // TODO: ADD module connection explanations if has_active_non_modules_connections { - let problem = { - // let importing_explanations = active_non_modules_connections - // .iter() - // .flat_map(|&c| c.explanation()) - // .collect::>(); - // let mut explanations: Vec<_> = importing_explanations.into_iter().collect(); - // explanations.sort(); - format!( - "Module {module_readable_identifier} is referenced", - // if !explanations.is_empty() { - // format!("by: {}", explanations.join(", ")) - // } else { - // "in an unsupported way".to_string() - // } - ) - }; - let problem = Warning::Problem(problem); + let problem = Warning::Witness(BailoutWitness::NonModuleReference { + module: module_readable_identifier, + }); statistics.incorrect_dependency += 1; failure_cache.insert(*module_id, problem.clone()); return Some(problem); @@ -518,7 +551,7 @@ impl ModuleConcatenationPlugin { .keys() .copied() .collect::>(); - let other_chunk_modules = incoming_modules + let other_chunk_witness = incoming_modules .iter() .filter(|&origin_module| { chunk_graph @@ -526,84 +559,73 @@ impl ModuleConcatenationPlugin { .iter() .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey)) }) - .collect::>(); - - if !other_chunk_modules.is_empty() { - let problem = { - let mut names: Vec<_> = other_chunk_modules - .into_iter() - .map(|mid| { - get_cached_readable_identifier( - mid, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ) - }) - .collect(); - names.sort(); - format!( - "Module {} is referenced from different chunks by these modules: {}", - module_readable_identifier, - names.join(", ") + .map(|origin_module| { + ( + get_cached_readable_identifier( + origin_module, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), + *origin_module, ) - }; + }) + .min_by(|(left_name, left_id), (right_name, right_id)| { + left_name + .cmp(right_name) + .then_with(|| left_id.cmp(right_id)) + }); + if let Some((importer, _)) = other_chunk_witness { statistics.incorrect_chunks_of_importer += 1; - let problem = Warning::Problem(problem); + let problem = Warning::Witness(BailoutWitness::CrossChunkImporter { + module: module_readable_identifier, + importer, + }); failure_cache.insert(*module_id, problem.clone()); return Some(problem); } - let mut non_esm_connections = IdentifierMap::with_capacity_and_hasher( - incoming_connections_from_modules.len(), - Default::default(), - ); - for (origin_module, connections) in incoming_connections_from_modules.iter() { - let has_non_esm_connections = connections.iter().any(|connection| { - let dep = module_graph.dependency_by_id(&connection.dependency_id); - !is_esm_dep_like(dep) - }); - - if has_non_esm_connections { - non_esm_connections.insert(*origin_module, connections); - } - } - - if !non_esm_connections.is_empty() { - let problem = { - let names: Vec<_> = non_esm_connections + let non_esm_witness = incoming_connections_from_modules + .iter() + .filter_map(|(origin_module, connections)| { + let mut syntaxes = connections .iter() - .map(|(origin_module, connections)| { - let readable_identifier = get_cached_readable_identifier( + .filter_map(|connection| { + let dep = module_graph.dependency_by_id(&connection.dependency_id); + (!is_esm_dep_like(dep)).then(|| dep.dependency_type().to_string()) + }) + .collect::>(); + syntaxes.sort(); + syntaxes.dedup(); + syntaxes.first().cloned().map(|syntax| { + ( + get_cached_readable_identifier( origin_module, module_graph, &compilation.module_static_cache, &compilation.options.context, - ); - let mut names = connections - .iter() - .map(|item| { - let dep = module_graph.dependency_by_id(&item.dependency_id); - dep.dependency_type().to_string() - }) - .collect::>(); - names.sort(); - format!( - "{} (referenced with {})", - readable_identifier, - names.join(",") - ) - }) - .collect(); + ), + *origin_module, + syntax, + ) + }) + }) + .min_by( + |(left_name, left_id, left_syntax), (right_name, right_id, right_syntax)| { + left_name + .cmp(right_name) + .then_with(|| left_syntax.cmp(right_syntax)) + .then_with(|| left_id.cmp(right_id)) + }, + ); - format!( - "Module {} is referenced from these modules with unsupported syntax: {}", - module_readable_identifier, - names.join(", ") - ) - }; - let problem = Warning::Problem(problem); + if let Some((importer, _, syntax)) = non_esm_witness { + let problem = Warning::Witness(BailoutWitness::UnsupportedSyntaxImporter { + module: module_readable_identifier, + importer, + syntax, + }); statistics.incorrect_module_dependency += 1; failure_cache.insert(*module_id, problem.clone()); return Some(problem); @@ -613,7 +635,7 @@ impl ModuleConcatenationPlugin { && runtime.len() > 1 { let mut other_runtime_connections = Vec::new(); - 'outer: for (origin_module, connections) in incoming_connections_from_modules { + 'outer: for (origin_module, connections) in incoming_connections_from_modules.iter() { let mut current_runtime_condition = RuntimeCondition::Boolean(false); for connection in connections { let runtime_condition = filter_runtime(Some(runtime), |runtime| { @@ -648,37 +670,43 @@ impl ModuleConcatenationPlugin { } if current_runtime_condition != RuntimeCondition::Boolean(false) { - other_runtime_connections.push((origin_module, current_runtime_condition)); + other_runtime_connections.push((*origin_module, current_runtime_condition)); } } - if !other_runtime_connections.is_empty() { - let problem = { - format!( - "Module {} is runtime-dependent referenced by these modules: {}", - module_readable_identifier, - other_runtime_connections - .iter() - .map(|(origin_module, runtime_condition)| { - let readable_identifier = get_cached_readable_identifier( - origin_module, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ); - format!( - "{} (expected runtime {}, module is only referenced in {})", - readable_identifier, - runtime, - runtime_condition.as_spec().expect("should be spec") - ) - }) - .collect::>() - .join(", ") + let runtime_witness = other_runtime_connections + .into_iter() + .map(|(origin_module, runtime_condition)| { + ( + get_cached_readable_identifier( + &origin_module, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), + origin_module, + runtime_condition + .as_spec() + .expect("should be spec") + .to_string(), ) - }; + }) + .min_by( + |(left_name, left_id, left_runtime), (right_name, right_id, right_runtime)| { + left_name + .cmp(right_name) + .then_with(|| left_runtime.cmp(right_runtime)) + .then_with(|| left_id.cmp(right_id)) + }, + ); - let problem = Warning::Problem(problem); + if let Some((importer, _, referenced_runtime)) = runtime_witness { + let problem = Warning::Witness(BailoutWitness::RuntimeDependentImporter { + module: module_readable_identifier, + importer, + expected_runtime: runtime.to_string(), + referenced_runtime, + }); statistics.incorrect_runtime_condition += 1; failure_cache.insert(*module_id, problem.clone()); return Some(problem); diff --git a/tests/rspack-test/statsOutputCases/scope-hoisting-multi/__snapshots__/stats.txt b/tests/rspack-test/statsOutputCases/scope-hoisting-multi/__snapshots__/stats.txt index cbf2c25b610c..12a423c8fae0 100644 --- a/tests/rspack-test/statsOutputCases/scope-hoisting-multi/__snapshots__/stats.txt +++ b/tests/rspack-test/statsOutputCases/scope-hoisting-multi/__snapshots__/stats.txt @@ -27,7 +27,7 @@ cacheable modules xx KiB Statement with side_effects in source code at ./second.js-63 ModuleConcatenation bailout: Module is an entry point ./lazy_shared.js xx bytes [built] [code generated] - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/scope-hoisting-multi/common_lazy_shared.js: Module ./common_lazy_shared.js is referenced from different chunks by these modules: ./lazy_first.js, ./lazy_second.js + ModuleConcatenation bailout: Cannot concat with /statsOutputCases/scope-hoisting-multi/common_lazy_shared.js: Module ./common_lazy_shared.js is referenced from a different chunk by ./lazy_first.js ./common_lazy_shared.js xx bytes [built] [code generated] ./lazy_first.js + 1 modules xx bytes [code generated] ./lazy_second.js + 1 modules xx bytes [code generated] From b0acb7a35085aca3616814468451a09add6446a5 Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Fri, 3 Apr 2026 20:56:17 +0800 Subject: [PATCH 3/9] perf(module-concat): cheapen bailout witness selection --- .../src/plugin/module_concatenation_plugin.rs | 207 +++++++----------- 1 file changed, 85 insertions(+), 122 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index 903353f2d8bd..710b2294fbab 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -407,12 +407,15 @@ impl ModuleConcatenationPlugin { statistics.cache_hit += 1; incomings.clone() } else { - let module_readable_identifier = get_cached_readable_identifier( - module_id, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ); + let get_module_readable_identifier = || { + get_cached_readable_identifier( + module_id, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ) + }; + let root_chunks = chunk_graph.get_module_chunks(config.root_module); if !possible_modules.contains(module_id) { statistics.invalid_module += 1; @@ -421,43 +424,30 @@ impl ModuleConcatenationPlugin { return Some(problem); } - let missing_chunks: Vec<_> = chunk_graph - .get_module_chunks(config.root_module) + let expected_chunk = root_chunks .iter() - .filter(|chunk| !chunk_graph.is_module_in_chunk(module_id, **chunk)) - .collect(); + .copied() + .filter(|chunk| !chunk_graph.is_module_in_chunk(module_id, *chunk)) + .min(); - if !missing_chunks.is_empty() { + if let Some(expected_chunk) = expected_chunk { statistics.incorrect_chunks += 1; - let mut missing_chunks_list = missing_chunks - .iter() - .map(|&chunk| { - let chunk = chunk_by_ukey.expect_get(chunk); - chunk.name().unwrap_or("unnamed chunk(s)") - }) - .collect::>(); - missing_chunks_list.sort_unstable(); - - let mut chunks = chunk_graph + let actual_chunk = chunk_graph .get_module_chunks(*module_id) .iter() - .map(|&chunk| { - let chunk = chunk_by_ukey.expect_get(&chunk); - chunk.name().unwrap_or("unnamed chunk(s)") - }) - .collect::>(); - chunks.sort_unstable(); + .copied() + .min(); let problem = Warning::Witness(BailoutWitness::MissingChunk { - module: module_readable_identifier, - expected_chunk: missing_chunks_list - .first() - .copied() + module: get_module_readable_identifier(), + expected_chunk: chunk_by_ukey + .expect_get(&expected_chunk) + .name() .unwrap_or("unnamed chunk(s)") .to_string(), - actual_chunk: chunks - .first() - .copied() + actual_chunk: actual_chunk + .and_then(|chunk| chunk_by_ukey.get(&chunk)) + .and_then(|chunk| chunk.name()) .unwrap_or("unnamed chunk(s)") .to_string(), }); @@ -490,7 +480,7 @@ impl ModuleConcatenationPlugin { // TODO: ADD module connection explanations if has_active_non_modules_connections { let problem = Warning::Witness(BailoutWitness::NonModuleReference { - module: module_readable_identifier, + module: get_module_readable_identifier(), }); statistics.incorrect_dependency += 1; failure_cache.insert(*module_id, problem.clone()); @@ -553,78 +543,64 @@ impl ModuleConcatenationPlugin { .collect::>(); let other_chunk_witness = incoming_modules .iter() - .filter(|&origin_module| { - chunk_graph - .get_module_chunks(config.root_module) + .copied() + .filter(|origin_module| { + root_chunks .iter() .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey)) }) - .map(|origin_module| { - ( - get_cached_readable_identifier( - origin_module, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ), - *origin_module, - ) - }) - .min_by(|(left_name, left_id), (right_name, right_id)| { - left_name - .cmp(right_name) - .then_with(|| left_id.cmp(right_id)) - }); + .min(); - if let Some((importer, _)) = other_chunk_witness { + if let Some(origin_module) = other_chunk_witness { statistics.incorrect_chunks_of_importer += 1; let problem = Warning::Witness(BailoutWitness::CrossChunkImporter { - module: module_readable_identifier, - importer, + module: get_module_readable_identifier(), + importer: get_cached_readable_identifier( + &origin_module, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), }); failure_cache.insert(*module_id, problem.clone()); return Some(problem); } - let non_esm_witness = incoming_connections_from_modules - .iter() - .filter_map(|(origin_module, connections)| { - let mut syntaxes = connections + let non_esm_witness = incoming_connections_from_modules.iter().fold( + None, + |best: Option<(ModuleIdentifier, &'static str)>, (origin_module, connections)| { + let best_syntax = connections .iter() .filter_map(|connection| { let dep = module_graph.dependency_by_id(&connection.dependency_id); - (!is_esm_dep_like(dep)).then(|| dep.dependency_type().to_string()) + (!is_esm_dep_like(dep)).then(|| dep.dependency_type().as_str()) }) - .collect::>(); - syntaxes.sort(); - syntaxes.dedup(); - syntaxes.first().cloned().map(|syntax| { - ( - get_cached_readable_identifier( - origin_module, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ), - *origin_module, - syntax, - ) - }) - }) - .min_by( - |(left_name, left_id, left_syntax), (right_name, right_id, right_syntax)| { - left_name - .cmp(right_name) - .then_with(|| left_syntax.cmp(right_syntax)) - .then_with(|| left_id.cmp(right_id)) - }, - ); + .min(); + + match (best, best_syntax) { + (current, None) => current, + (None, Some(syntax)) => Some((*origin_module, syntax)), + (Some((best_module, best_syntax)), Some(syntax)) => { + Some(if (*origin_module, syntax) < (best_module, best_syntax) { + (*origin_module, syntax) + } else { + (best_module, best_syntax) + }) + } + } + }, + ); - if let Some((importer, _, syntax)) = non_esm_witness { + if let Some((origin_module, syntax)) = non_esm_witness { let problem = Warning::Witness(BailoutWitness::UnsupportedSyntaxImporter { - module: module_readable_identifier, - importer, - syntax, + module: get_module_readable_identifier(), + importer: get_cached_readable_identifier( + &origin_module, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), + syntax: syntax.to_string(), }); statistics.incorrect_module_dependency += 1; failure_cache.insert(*module_id, problem.clone()); @@ -634,7 +610,7 @@ impl ModuleConcatenationPlugin { if let Some(runtime) = runtime && runtime.len() > 1 { - let mut other_runtime_connections = Vec::new(); + let mut runtime_witness = None; 'outer: for (origin_module, connections) in incoming_connections_from_modules.iter() { let mut current_runtime_condition = RuntimeCondition::Boolean(false); for connection in connections { @@ -670,42 +646,29 @@ impl ModuleConcatenationPlugin { } if current_runtime_condition != RuntimeCondition::Boolean(false) { - other_runtime_connections.push((*origin_module, current_runtime_condition)); + let should_replace = runtime_witness + .as_ref() + .is_none_or(|(best_origin_module, _)| origin_module < best_origin_module); + if should_replace { + runtime_witness = Some((*origin_module, current_runtime_condition)); + } } } - let runtime_witness = other_runtime_connections - .into_iter() - .map(|(origin_module, runtime_condition)| { - ( - get_cached_readable_identifier( - &origin_module, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ), - origin_module, - runtime_condition - .as_spec() - .expect("should be spec") - .to_string(), - ) - }) - .min_by( - |(left_name, left_id, left_runtime), (right_name, right_id, right_runtime)| { - left_name - .cmp(right_name) - .then_with(|| left_runtime.cmp(right_runtime)) - .then_with(|| left_id.cmp(right_id)) - }, - ); - - if let Some((importer, _, referenced_runtime)) = runtime_witness { + if let Some((origin_module, runtime_condition)) = runtime_witness { let problem = Warning::Witness(BailoutWitness::RuntimeDependentImporter { - module: module_readable_identifier, - importer, + module: get_module_readable_identifier(), + importer: get_cached_readable_identifier( + &origin_module, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), expected_runtime: runtime.to_string(), - referenced_runtime, + referenced_runtime: runtime_condition + .as_spec() + .expect("should be spec") + .to_string(), }); statistics.incorrect_runtime_condition += 1; failure_cache.insert(*module_id, problem.clone()); From 62e850bf0132169d131e0679dffb6a3e6bb1fefe Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Sat, 4 Apr 2026 11:59:20 +0800 Subject: [PATCH 4/9] perf(module-concat): defer bailout witness formatting --- .../src/plugin/module_concatenation_plugin.rs | 378 +++++++++--------- 1 file changed, 198 insertions(+), 180 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index 710b2294fbab..8820dfe5e660 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -6,7 +6,7 @@ use rspack_collections::{ Identifiable, IdentifierDashMap, IdentifierIndexSet, IdentifierMap, IdentifierSet, }; use rspack_core::{ - BoxDependency, BoxModule, Compilation, CompilationOptimizeChunkModules, DependencyId, + BoxDependency, BoxModule, ChunkUkey, Compilation, CompilationOptimizeChunkModules, DependencyId, DependencyType, ExportProvided, ExportsInfoArtifact, ExtendedReferencedExport, GetTargetResult, ImportedByDeferModulesArtifact, LibIdentOptions, Logger, ModuleGraph, ModuleGraphCacheArtifact, ModuleGraphConnection, ModuleGraphModule, ModuleIdentifier, OptimizationBailoutItem, Plugin, @@ -35,27 +35,21 @@ enum BailoutWitness { EntryPoint, Deferred, MissingChunk { - module: String, - expected_chunk: String, - actual_chunk: String, - }, - NonModuleReference { - module: String, + expected_chunk: ChunkUkey, + actual_chunk: Option, }, + NonModuleReference, CrossChunkImporter { - module: String, - importer: String, + importer: ModuleIdentifier, }, UnsupportedSyntaxImporter { - module: String, - importer: String, - syntax: String, + importer: ModuleIdentifier, + dependency_type: DependencyType, }, RuntimeDependentImporter { - module: String, - importer: String, - expected_runtime: String, - referenced_runtime: String, + importer: ModuleIdentifier, + expected_runtime: RuntimeSpec, + referenced_runtime: RuntimeSpec, }, UnknownReexport { export_name: Option, @@ -72,11 +66,25 @@ impl BailoutWitness { fn format_export_name(export_name: Option<&Atom>) -> Cow<'_, str> { export_name.map_or_else( || Cow::Borrowed("other exports"), - |name| Cow::Owned(name.to_string()), + |name| Cow::Borrowed(name.as_str()), ) } - fn format_reason(&self) -> Cow<'static, str> { + fn format_reason( + &self, + compilation: &Compilation, + module_id: &ModuleIdentifier, + ) -> Cow<'static, str> { + let module_graph = compilation.get_module_graph(); + let module_readable_identifier = || { + get_cached_readable_identifier( + module_id, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ) + }; + match self { Self::AsyncModule => Cow::Borrowed("Module is async"), Self::NotStrict => Cow::Borrowed("Module is not in strict mode"), @@ -84,30 +92,69 @@ impl BailoutWitness { Self::EntryPoint => Cow::Borrowed("Module is an entry point"), Self::Deferred => Cow::Borrowed("Module is deferred"), Self::MissingChunk { - module, expected_chunk, actual_chunk, } => Cow::Owned(format!( - "Module {module} is not in the same chunk(s) (expected in chunk(s) {expected_chunk}, module is in chunk(s) {actual_chunk})" + "Module {} is not in the same chunk(s) (expected in chunk(s) {}, module is in chunk(s) {})", + module_readable_identifier(), + compilation + .build_chunk_graph_artifact + .chunk_by_ukey + .expect_get(expected_chunk) + .name() + .unwrap_or("unnamed chunk(s)"), + actual_chunk.map_or("unnamed chunk(s)", |chunk| { + compilation + .build_chunk_graph_artifact + .chunk_by_ukey + .expect_get(&chunk) + .name() + .unwrap_or("unnamed chunk(s)") + }) )), - Self::NonModuleReference { module } => Cow::Owned(format!("Module {module} is referenced")), - Self::CrossChunkImporter { module, importer } => Cow::Owned(format!( - "Module {module} is referenced from a different chunk by {importer}" + Self::NonModuleReference => Cow::Owned(format!( + "Module {} is referenced", + module_readable_identifier() + )), + Self::CrossChunkImporter { importer } => Cow::Owned(format!( + "Module {} is referenced from a different chunk by {}", + module_readable_identifier(), + get_cached_readable_identifier( + importer, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ) )), Self::UnsupportedSyntaxImporter { - module, importer, - syntax, + dependency_type, } => Cow::Owned(format!( - "Module {module} is referenced from {importer} with unsupported syntax ({syntax})" + "Module {} is referenced from {} with unsupported syntax ({})", + module_readable_identifier(), + get_cached_readable_identifier( + importer, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), + dependency_type.as_str() )), Self::RuntimeDependentImporter { - module, importer, expected_runtime, referenced_runtime, } => Cow::Owned(format!( - "Module {module} is runtime-dependent referenced by {importer} (expected runtime {expected_runtime}, module is only referenced in {referenced_runtime})" + "Module {} is runtime-dependent referenced by {} (expected runtime {}, module is only referenced in {})", + module_readable_identifier(), + get_cached_readable_identifier( + importer, + module_graph, + &compilation.module_static_cache, + &compilation.options.context, + ), + expected_runtime, + referenced_runtime )), Self::UnknownReexport { export_name, @@ -242,11 +289,16 @@ impl RuntimeIdentifierCache { } impl ModuleConcatenationPlugin { - fn format_bailout_warning(&self, module: ModuleIdentifier, warning: &Warning) -> String { + fn format_bailout_warning( + &self, + compilation: &Compilation, + module: ModuleIdentifier, + warning: &Warning, + ) -> String { match warning { Warning::Witness(witness) => format_bailout_reason(&format!( "Cannot concat with {module}: {}", - witness.format_reason() + witness.format_reason(compilation, &module) )), Warning::Id(id) => { let reason = self.get_inner_bailout_reason(id); @@ -407,14 +459,6 @@ impl ModuleConcatenationPlugin { statistics.cache_hit += 1; incomings.clone() } else { - let get_module_readable_identifier = || { - get_cached_readable_identifier( - module_id, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ) - }; let root_chunks = chunk_graph.get_module_chunks(config.root_module); if !possible_modules.contains(module_id) { @@ -439,17 +483,8 @@ impl ModuleConcatenationPlugin { .min(); let problem = Warning::Witness(BailoutWitness::MissingChunk { - module: get_module_readable_identifier(), - expected_chunk: chunk_by_ukey - .expect_get(&expected_chunk) - .name() - .unwrap_or("unnamed chunk(s)") - .to_string(), - actual_chunk: actual_chunk - .and_then(|chunk| chunk_by_ukey.get(&chunk)) - .and_then(|chunk| chunk.name()) - .unwrap_or("unnamed chunk(s)") - .to_string(), + expected_chunk, + actual_chunk, }); failure_cache.insert(*module_id, problem.clone()); return Some(problem); @@ -479,17 +514,19 @@ impl ModuleConcatenationPlugin { // TODO: ADD module connection explanations if has_active_non_modules_connections { - let problem = Warning::Witness(BailoutWitness::NonModuleReference { - module: get_module_readable_identifier(), - }); + let problem = Warning::Witness(BailoutWitness::NonModuleReference); statistics.incorrect_dependency += 1; failure_cache.insert(*module_id, problem.clone()); return Some(problem); } } - let mut incoming_connections_from_modules = - IdentifierMap::with_capacity_and_hasher(incomings.from_modules.len(), Default::default()); + let mut incoming_modules = Vec::with_capacity(incomings.from_modules.len()); + let mut cross_chunk_witness = None; + let mut non_esm_witness = None; + let track_runtime_witness = runtime.is_some_and(|runtime| runtime.len() > 1); + let mut runtime_witness = None; + for (origin_module, connections) in incomings.from_modules.iter() { let number_of_chunks = module_cache.get(origin_module).map_or_else( || chunk_graph.get_number_of_module_chunks(*origin_module), @@ -518,103 +555,38 @@ impl ModuleConcatenationPlugin { continue; } - let active_connections: Vec<_> = connections - .iter() - .filter(|&connection| { - is_connection_active_in_runtime( - connection, - runtime, - active_incomings, - cached_module_runtime, - module_graph, - &module_graph_artifacts, - ) - }) - .collect(); - - if !active_connections.is_empty() { - incoming_connections_from_modules.insert(*origin_module, active_connections); - } - } - - let mut incoming_modules = incoming_connections_from_modules - .keys() - .copied() - .collect::>(); - let other_chunk_witness = incoming_modules - .iter() - .copied() - .filter(|origin_module| { - root_chunks - .iter() - .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey)) - }) - .min(); + let mut has_active_connection = false; + let mut importer_non_esm = None; + let mut current_runtime_condition = RuntimeCondition::Boolean(false); + let mut has_full_runtime_coverage = false; - if let Some(origin_module) = other_chunk_witness { - statistics.incorrect_chunks_of_importer += 1; - let problem = Warning::Witness(BailoutWitness::CrossChunkImporter { - module: get_module_readable_identifier(), - importer: get_cached_readable_identifier( - &origin_module, + for connection in connections { + if !is_connection_active_in_runtime( + connection, + runtime, + active_incomings, + cached_module_runtime, module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ), - }); - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } + &module_graph_artifacts, + ) { + continue; + } - let non_esm_witness = incoming_connections_from_modules.iter().fold( - None, - |best: Option<(ModuleIdentifier, &'static str)>, (origin_module, connections)| { - let best_syntax = connections - .iter() - .filter_map(|connection| { - let dep = module_graph.dependency_by_id(&connection.dependency_id); - (!is_esm_dep_like(dep)).then(|| dep.dependency_type().as_str()) - }) - .min(); - - match (best, best_syntax) { - (current, None) => current, - (None, Some(syntax)) => Some((*origin_module, syntax)), - (Some((best_module, best_syntax)), Some(syntax)) => { - Some(if (*origin_module, syntax) < (best_module, best_syntax) { - (*origin_module, syntax) - } else { - (best_module, best_syntax) - }) + has_active_connection = true; + + let dep = module_graph.dependency_by_id(&connection.dependency_id); + if !is_esm_dep_like(dep) { + let dependency_type = *dep.dependency_type(); + if importer_non_esm + .as_ref() + .is_none_or(|best: &DependencyType| dependency_type.as_str() < best.as_str()) + { + importer_non_esm = Some(dependency_type); } } - }, - ); - if let Some((origin_module, syntax)) = non_esm_witness { - let problem = Warning::Witness(BailoutWitness::UnsupportedSyntaxImporter { - module: get_module_readable_identifier(), - importer: get_cached_readable_identifier( - &origin_module, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ), - syntax: syntax.to_string(), - }); - statistics.incorrect_module_dependency += 1; - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } - - if let Some(runtime) = runtime - && runtime.len() > 1 - { - let mut runtime_witness = None; - 'outer: for (origin_module, connections) in incoming_connections_from_modules.iter() { - let mut current_runtime_condition = RuntimeCondition::Boolean(false); - for connection in connections { - let runtime_condition = filter_runtime(Some(runtime), |runtime| { + if track_runtime_witness { + let runtime_condition = filter_runtime(runtime, |runtime| { connection.is_target_active( module_graph, runtime, @@ -626,15 +598,15 @@ impl ModuleConcatenationPlugin { ) }); - if runtime_condition == RuntimeCondition::Boolean(false) { + if runtime_condition == RuntimeCondition::Boolean(true) { + has_full_runtime_coverage = true; continue; } - if runtime_condition == RuntimeCondition::Boolean(true) { - continue 'outer; + if runtime_condition == RuntimeCondition::Boolean(false) { + continue; } - // here two runtime_condition must be `RuntimeCondition::Spec` if current_runtime_condition != RuntimeCondition::Boolean(false) { current_runtime_condition .as_spec_mut() @@ -644,38 +616,77 @@ impl ModuleConcatenationPlugin { current_runtime_condition = runtime_condition; } } + } - if current_runtime_condition != RuntimeCondition::Boolean(false) { - let should_replace = runtime_witness - .as_ref() - .is_none_or(|(best_origin_module, _)| origin_module < best_origin_module); - if should_replace { - runtime_witness = Some((*origin_module, current_runtime_condition)); - } - } + if !has_active_connection { + continue; } - if let Some((origin_module, runtime_condition)) = runtime_witness { - let problem = Warning::Witness(BailoutWitness::RuntimeDependentImporter { - module: get_module_readable_identifier(), - importer: get_cached_readable_identifier( - &origin_module, - module_graph, - &compilation.module_static_cache, - &compilation.options.context, - ), - expected_runtime: runtime.to_string(), - referenced_runtime: runtime_condition - .as_spec() - .expect("should be spec") - .to_string(), - }); - statistics.incorrect_runtime_condition += 1; - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); + incoming_modules.push(*origin_module); + + if root_chunks + .iter() + .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey)) + && cross_chunk_witness + .as_ref() + .is_none_or(|best: &ModuleIdentifier| origin_module < best) + { + cross_chunk_witness = Some(*origin_module); + } + + if let Some(dependency_type) = importer_non_esm + && non_esm_witness.as_ref().is_none_or( + |(best_module, best_type): &(ModuleIdentifier, DependencyType)| { + (*origin_module, dependency_type.as_str()) < (*best_module, best_type.as_str()) + }, + ) + { + non_esm_witness = Some((*origin_module, dependency_type)); + } + + if track_runtime_witness + && !has_full_runtime_coverage + && let RuntimeCondition::Spec(spec) = current_runtime_condition + && runtime_witness.as_ref().is_none_or( + |(best_module, _): &(ModuleIdentifier, RuntimeSpec)| origin_module < best_module, + ) + { + runtime_witness = Some((*origin_module, spec)); } } + if let Some(origin_module) = cross_chunk_witness { + statistics.incorrect_chunks_of_importer += 1; + let problem = Warning::Witness(BailoutWitness::CrossChunkImporter { + importer: origin_module, + }); + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } + + if let Some((origin_module, dependency_type)) = non_esm_witness { + let problem = Warning::Witness(BailoutWitness::UnsupportedSyntaxImporter { + importer: origin_module, + dependency_type, + }); + statistics.incorrect_module_dependency += 1; + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } + + if let Some(runtime) = runtime + && let Some((origin_module, referenced_runtime)) = runtime_witness + { + let problem = Warning::Witness(BailoutWitness::RuntimeDependentImporter { + importer: origin_module, + expected_runtime: runtime.clone(), + referenced_runtime, + }); + statistics.incorrect_runtime_condition += 1; + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } + incoming_modules.sort(); success_cache.insert(*module_id, runtime, incoming_modules.clone()); incoming_modules @@ -1108,7 +1119,11 @@ impl ModuleConcatenationPlugin { if let Some(reason) = string_bailout_reason { bailout_reasons.push(reason); } - bailout_reasons.extend(bailout_witnesses.iter().map(BailoutWitness::format_reason)); + bailout_reasons.extend( + bailout_witnesses + .iter() + .map(|witness| witness.format_reason(compilation, &module_id)), + ); (can_be_root, can_be_inner, module_id, bailout_reasons) }, ) @@ -1396,12 +1411,15 @@ impl ModuleConcatenationPlugin { concat_configurations.push(current_configuration); } else { stats_empty_configurations += 1; + let formatted_warnings = current_configuration + .get_warnings_sorted() + .into_iter() + .map(|warning| self.format_bailout_warning(compilation, warning.0, &warning.1)) + .collect::>(); let module_graph = compilation.get_module_graph_mut(); let optimization_bailouts = module_graph.get_optimization_bailout_mut(current_root); - for warning in current_configuration.get_warnings_sorted() { - optimization_bailouts.push(OptimizationBailoutItem::Message( - self.format_bailout_warning(warning.0, &warning.1), - )); + for warning in formatted_warnings { + optimization_bailouts.push(OptimizationBailoutItem::Message(warning)); } } } From c7022bdc5fc32dad012b40ee5be1ec8a1348a315 Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Tue, 7 Apr 2026 12:49:43 +0800 Subject: [PATCH 5/9] perf(module-concat): reduce warning and cache churn --- .../src/plugin/module_concatenation_plugin.rs | 471 +++++++++--------- 1 file changed, 235 insertions(+), 236 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index 8820dfe5e660..cfedc13dda96 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -189,7 +189,7 @@ pub struct ConcatConfiguration { pub root_module: ModuleIdentifier, runtime: Option, modules: IdentifierIndexSet, - warnings: IdentifierMap, + warning: Option<(ModuleIdentifier, Warning)>, } impl ConcatConfiguration { @@ -201,7 +201,7 @@ impl ConcatConfiguration { root_module, runtime, modules, - warnings: IdentifierMap::default(), + warning: None, } } @@ -218,13 +218,17 @@ impl ConcatConfiguration { } fn add_warning(&mut self, module: ModuleIdentifier, problem: Warning) { - self.warnings.insert(module, problem); + if self + .warning + .as_ref() + .is_none_or(|(existing_module, _)| module < *existing_module) + { + self.warning = Some((module, problem)); + } } - fn get_warnings_sorted(&self) -> Vec<(ModuleIdentifier, Warning)> { - let mut sorted_warnings: Vec<_> = self.warnings.clone().into_iter().collect(); - sorted_warnings.sort_by_key(|(id, _)| *id); - sorted_warnings + fn get_warning(&self) -> Option<(ModuleIdentifier, Warning)> { + self.warning.clone() } fn get_modules(&self) -> &IdentifierIndexSet { @@ -349,9 +353,9 @@ impl ModuleConcatenationPlugin { artifacts: &ModuleGraphArtifacts, mi: ModuleIdentifier, runtime: Option<&RuntimeSpec>, - imports_cache: &mut RuntimeIdentifierCache, + imports_cache: &mut RuntimeIdentifierCache>, module_cache: &IdentifierMap, - ) -> IdentifierIndexSet { + ) -> Arc { if let Some(set) = imports_cache.get(&mi, runtime) { return set.clone(); } @@ -404,6 +408,7 @@ impl ModuleConcatenationPlugin { } } + let set = Arc::new(set); imports_cache.insert(mi, runtime, set.clone()); set } @@ -418,10 +423,10 @@ impl ModuleConcatenationPlugin { possible_modules: &IdentifierSet, candidates: &mut IdentifierSet, failure_cache: &mut IdentifierMap, - success_cache: &mut RuntimeIdentifierCache>, + success_cache: &mut RuntimeIdentifierCache>, avoid_mutate_on_failure: bool, statistics: &mut Statistics, - imports_cache: &mut RuntimeIdentifierCache, + imports_cache: &mut RuntimeIdentifierCache>, module_cache: &IdentifierMap, ) -> Option { statistics @@ -455,242 +460,242 @@ impl ModuleConcatenationPlugin { exports_info_artifact: &compilation.exports_info_artifact, }; - let incoming_modules = if let Some(incomings) = success_cache.get(module_id, runtime) { - statistics.cache_hit += 1; - incomings.clone() - } else { - let root_chunks = chunk_graph.get_module_chunks(config.root_module); - - if !possible_modules.contains(module_id) { - statistics.invalid_module += 1; - let problem = Warning::Id(*module_id); - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } + let incoming_modules: Arc<[ModuleIdentifier]> = + if let Some(incomings) = success_cache.get(module_id, runtime) { + statistics.cache_hit += 1; + incomings.clone() + } else { + let root_chunks = chunk_graph.get_module_chunks(config.root_module); - let expected_chunk = root_chunks - .iter() - .copied() - .filter(|chunk| !chunk_graph.is_module_in_chunk(module_id, *chunk)) - .min(); + if !possible_modules.contains(module_id) { + statistics.invalid_module += 1; + let problem = Warning::Id(*module_id); + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } - if let Some(expected_chunk) = expected_chunk { - statistics.incorrect_chunks += 1; - let actual_chunk = chunk_graph - .get_module_chunks(*module_id) + let expected_chunk = root_chunks .iter() .copied() + .filter(|chunk| !chunk_graph.is_module_in_chunk(module_id, *chunk)) .min(); - let problem = Warning::Witness(BailoutWitness::MissingChunk { - expected_chunk, - actual_chunk, - }); - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } - - let NoRuntimeModuleCache { - incomings, - active_incomings, - runtime: cached_module_runtime, - .. - } = module_cache - .get(module_id) - .expect("should have module cache"); - - if !incomings.from_non_modules.is_empty() { - let has_active_non_modules_connections = - incomings.from_non_modules.iter().any(|connection| { - is_connection_active_in_runtime( - connection, - runtime, - active_incomings, - cached_module_runtime, - module_graph, - &module_graph_artifacts, - ) + if let Some(expected_chunk) = expected_chunk { + statistics.incorrect_chunks += 1; + let actual_chunk = chunk_graph + .get_module_chunks(*module_id) + .iter() + .copied() + .min(); + + let problem = Warning::Witness(BailoutWitness::MissingChunk { + expected_chunk, + actual_chunk, }); - - // TODO: ADD module connection explanations - if has_active_non_modules_connections { - let problem = Warning::Witness(BailoutWitness::NonModuleReference); - statistics.incorrect_dependency += 1; failure_cache.insert(*module_id, problem.clone()); return Some(problem); } - } - let mut incoming_modules = Vec::with_capacity(incomings.from_modules.len()); - let mut cross_chunk_witness = None; - let mut non_esm_witness = None; - let track_runtime_witness = runtime.is_some_and(|runtime| runtime.len() > 1); - let mut runtime_witness = None; - - for (origin_module, connections) in incomings.from_modules.iter() { - let number_of_chunks = module_cache.get(origin_module).map_or_else( - || chunk_graph.get_number_of_module_chunks(*origin_module), - |m| m.number_of_chunks, - ); - - if number_of_chunks == 0 { - // Ignore connection from orphan modules - continue; - } + let NoRuntimeModuleCache { + incomings, + active_incomings, + runtime: cached_module_runtime, + .. + } = module_cache + .get(module_id) + .expect("should have module cache"); + + if !incomings.from_non_modules.is_empty() { + let has_active_non_modules_connections = + incomings.from_non_modules.iter().any(|connection| { + is_connection_active_in_runtime( + connection, + runtime, + active_incomings, + cached_module_runtime, + module_graph, + module_graph_cache, + &compilation.exports_info_artifact, + ) + }); - let is_intersect = if let Some(runtime) = runtime { - if let Some(origin_runtime) = module_cache.get(origin_module).map(|m| &m.runtime) { - !runtime.is_disjoint(origin_runtime) - } else { - let origin_runtime = RuntimeSpec::from_runtimes( - chunk_graph.get_module_runtimes_iter(*origin_module, chunk_by_ukey), - ); - !runtime.is_disjoint(&origin_runtime) + // TODO: ADD module connection explanations + if has_active_non_modules_connections { + let problem = Warning::Witness(BailoutWitness::NonModuleReference); + statistics.incorrect_dependency += 1; + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); } - } else { - false - }; - - if !is_intersect { - continue; } - let mut has_active_connection = false; - let mut importer_non_esm = None; - let mut current_runtime_condition = RuntimeCondition::Boolean(false); - let mut has_full_runtime_coverage = false; + let mut incoming_modules = Vec::with_capacity(incomings.from_modules.len()); + let mut cross_chunk_witness = None; + let mut non_esm_witness = None; + let track_runtime_witness = runtime.is_some_and(|runtime| runtime.len() > 1); + let mut runtime_witness = None; - for connection in connections { - if !is_connection_active_in_runtime( - connection, - runtime, - active_incomings, - cached_module_runtime, - module_graph, - &module_graph_artifacts, - ) { + for (origin_module, connections) in incomings.from_modules.iter() { + let number_of_chunks = module_cache.get(origin_module).map_or_else( + || chunk_graph.get_number_of_module_chunks(*origin_module), + |m| m.number_of_chunks, + ); + + if number_of_chunks == 0 { + // Ignore connection from orphan modules continue; } - has_active_connection = true; - - let dep = module_graph.dependency_by_id(&connection.dependency_id); - if !is_esm_dep_like(dep) { - let dependency_type = *dep.dependency_type(); - if importer_non_esm - .as_ref() - .is_none_or(|best: &DependencyType| dependency_type.as_str() < best.as_str()) - { - importer_non_esm = Some(dependency_type); + let is_intersect = if let Some(runtime) = runtime { + if let Some(origin_runtime) = module_cache.get(origin_module).map(|m| &m.runtime) { + !runtime.is_disjoint(origin_runtime) + } else { + let origin_runtime = RuntimeSpec::from_runtimes( + chunk_graph.get_module_runtimes_iter(*origin_module, chunk_by_ukey), + ); + !runtime.is_disjoint(&origin_runtime) } + } else { + false + }; + + if !is_intersect { + continue; } - if track_runtime_witness { - let runtime_condition = filter_runtime(runtime, |runtime| { - connection.is_target_active( - module_graph, - runtime, - module_graph_cache, - &compilation - .build_module_graph_artifact - .side_effects_state_artifact, - &compilation.exports_info_artifact, - ) - }); + let mut has_active_connection = false; + let mut importer_non_esm = None; + let mut current_runtime_condition = RuntimeCondition::Boolean(false); + let mut has_full_runtime_coverage = false; - if runtime_condition == RuntimeCondition::Boolean(true) { - has_full_runtime_coverage = true; + for connection in connections { + if !is_connection_active_in_runtime( + connection, + runtime, + active_incomings, + cached_module_runtime, + module_graph, + &module_graph_artifacts, + ) { continue; } - if runtime_condition == RuntimeCondition::Boolean(false) { - continue; + has_active_connection = true; + + let dep = module_graph.dependency_by_id(&connection.dependency_id); + if !is_esm_dep_like(dep) { + let dependency_type = *dep.dependency_type(); + if importer_non_esm + .as_ref() + .is_none_or(|best: &DependencyType| dependency_type.as_str() < best.as_str()) + { + importer_non_esm = Some(dependency_type); + } } - if current_runtime_condition != RuntimeCondition::Boolean(false) { - current_runtime_condition - .as_spec_mut() - .expect("should be spec") - .extend(runtime_condition.as_spec().expect("should be spec")); - } else { - current_runtime_condition = runtime_condition; + if track_runtime_witness { + let runtime_condition = filter_runtime(runtime, |runtime| { + connection.is_target_active( + module_graph, + runtime, + module_graph_cache, + &compilation.exports_info_artifact, + ) + }); + + if runtime_condition == RuntimeCondition::Boolean(true) { + has_full_runtime_coverage = true; + continue; + } + + if runtime_condition == RuntimeCondition::Boolean(false) { + continue; + } + + if current_runtime_condition != RuntimeCondition::Boolean(false) { + current_runtime_condition + .as_spec_mut() + .expect("should be spec") + .extend(runtime_condition.as_spec().expect("should be spec")); + } else { + current_runtime_condition = runtime_condition; + } } } - } - if !has_active_connection { - continue; - } + if !has_active_connection { + continue; + } - incoming_modules.push(*origin_module); + incoming_modules.push(*origin_module); - if root_chunks - .iter() - .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey)) - && cross_chunk_witness - .as_ref() - .is_none_or(|best: &ModuleIdentifier| origin_module < best) - { - cross_chunk_witness = Some(*origin_module); - } + if root_chunks + .iter() + .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey)) + && cross_chunk_witness + .as_ref() + .is_none_or(|best: &ModuleIdentifier| origin_module < best) + { + cross_chunk_witness = Some(*origin_module); + } - if let Some(dependency_type) = importer_non_esm - && non_esm_witness.as_ref().is_none_or( - |(best_module, best_type): &(ModuleIdentifier, DependencyType)| { - (*origin_module, dependency_type.as_str()) < (*best_module, best_type.as_str()) - }, - ) - { - non_esm_witness = Some((*origin_module, dependency_type)); - } + if let Some(dependency_type) = importer_non_esm + && non_esm_witness.as_ref().is_none_or( + |(best_module, best_type): &(ModuleIdentifier, DependencyType)| { + (*origin_module, dependency_type.as_str()) < (*best_module, best_type.as_str()) + }, + ) + { + non_esm_witness = Some((*origin_module, dependency_type)); + } - if track_runtime_witness - && !has_full_runtime_coverage - && let RuntimeCondition::Spec(spec) = current_runtime_condition - && runtime_witness.as_ref().is_none_or( - |(best_module, _): &(ModuleIdentifier, RuntimeSpec)| origin_module < best_module, - ) - { - runtime_witness = Some((*origin_module, spec)); + if track_runtime_witness + && !has_full_runtime_coverage + && let RuntimeCondition::Spec(spec) = current_runtime_condition + && runtime_witness.as_ref().is_none_or( + |(best_module, _): &(ModuleIdentifier, RuntimeSpec)| origin_module < best_module, + ) + { + runtime_witness = Some((*origin_module, spec)); + } } - } - if let Some(origin_module) = cross_chunk_witness { - statistics.incorrect_chunks_of_importer += 1; - let problem = Warning::Witness(BailoutWitness::CrossChunkImporter { - importer: origin_module, - }); - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } + if let Some(origin_module) = cross_chunk_witness { + statistics.incorrect_chunks_of_importer += 1; + let problem = Warning::Witness(BailoutWitness::CrossChunkImporter { + importer: origin_module, + }); + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } - if let Some((origin_module, dependency_type)) = non_esm_witness { - let problem = Warning::Witness(BailoutWitness::UnsupportedSyntaxImporter { - importer: origin_module, - dependency_type, - }); - statistics.incorrect_module_dependency += 1; - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } + if let Some((origin_module, dependency_type)) = non_esm_witness { + let problem = Warning::Witness(BailoutWitness::UnsupportedSyntaxImporter { + importer: origin_module, + dependency_type, + }); + statistics.incorrect_module_dependency += 1; + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } - if let Some(runtime) = runtime - && let Some((origin_module, referenced_runtime)) = runtime_witness - { - let problem = Warning::Witness(BailoutWitness::RuntimeDependentImporter { - importer: origin_module, - expected_runtime: runtime.clone(), - referenced_runtime, - }); - statistics.incorrect_runtime_condition += 1; - failure_cache.insert(*module_id, problem.clone()); - return Some(problem); - } + if let Some(runtime) = runtime + && let Some((origin_module, referenced_runtime)) = runtime_witness + { + let problem = Warning::Witness(BailoutWitness::RuntimeDependentImporter { + importer: origin_module, + expected_runtime: runtime.clone(), + referenced_runtime, + }); + statistics.incorrect_runtime_condition += 1; + failure_cache.insert(*module_id, problem.clone()); + return Some(problem); + } - incoming_modules.sort(); - success_cache.insert(*module_id, runtime, incoming_modules.clone()); - incoming_modules - }; + incoming_modules.sort(); + let incoming_modules: Arc<[ModuleIdentifier]> = incoming_modules.into(); + success_cache.insert(*module_id, runtime, incoming_modules.clone()); + incoming_modules + }; let backup = if avoid_mutate_on_failure { Some(config.snapshot()) @@ -700,7 +705,7 @@ impl ModuleConcatenationPlugin { config.add(*module_id); - for origin_module in &incoming_modules { + for origin_module in incoming_modules.iter() { if let Some(problem) = Self::try_to_add( compilation, config, @@ -732,7 +737,10 @@ impl ModuleConcatenationPlugin { runtime, imports_cache, module_cache, - ) { + ) + .iter() + .copied() + { candidates.insert(imp); } statistics.added += 1; @@ -1169,12 +1177,10 @@ impl ModuleConcatenationPlugin { let start = logger.time("find modules to concatenate"); let mut concat_configurations: Vec = Vec::new(); let mut used_as_inner: IdentifierSet = IdentifierSet::default(); - let mut imports_cache = RuntimeIdentifierCache::::default(); + let mut imports_cache = RuntimeIdentifierCache::>::default(); let module_graph = compilation.get_module_graph(); let module_graph_cache = &compilation.module_graph_cache_artifact; - let module_static_cache = &compilation.module_static_cache; - let compilation_context = &compilation.options.context; let cache_modules = relevant_modules .iter() .chain(possible_inners.iter()) @@ -1203,13 +1209,6 @@ impl ModuleConcatenationPlugin { ), ); - let _ = get_cached_readable_identifier( - &module_id, - module_graph, - module_static_cache, - compilation_context, - ); - let connections = module .get_dependencies() .iter() @@ -1337,7 +1336,7 @@ impl ModuleConcatenationPlugin { ConcatConfiguration::new(*current_root, active_runtime.clone()); let mut failure_cache = IdentifierMap::default(); - let mut success_cache = RuntimeIdentifierCache::default(); + let mut success_cache = RuntimeIdentifierCache::>::default(); let mut candidates_visited = IdentifierSet::default(); let mut candidates = VecDeque::new(); let imports = { @@ -1351,16 +1350,15 @@ impl ModuleConcatenationPlugin { exports_info_artifact: &compilation.exports_info_artifact, }; - Self::get_imports( - module_graph, - &module_graph_artifacts, - *current_root, - active_runtime.as_ref(), - &mut imports_cache, - &modules_without_runtime_cache, - ) - }; - for import in imports { + let imports = Self::get_imports( + module_graph, + &module_graph_artifacts, + *current_root, + active_runtime.as_ref(), + &mut imports_cache, + &modules_without_runtime_cache, + ); + for import in imports.iter().copied() { candidates.push_back(import); } @@ -1411,14 +1409,15 @@ impl ModuleConcatenationPlugin { concat_configurations.push(current_configuration); } else { stats_empty_configurations += 1; - let formatted_warnings = current_configuration - .get_warnings_sorted() - .into_iter() - .map(|warning| self.format_bailout_warning(compilation, warning.0, &warning.1)) - .collect::>(); + let formatted_warning = + current_configuration + .get_warning() + .map(|(warning_module, warning)| { + self.format_bailout_warning(compilation, warning_module, &warning) + }); let module_graph = compilation.get_module_graph_mut(); let optimization_bailouts = module_graph.get_optimization_bailout_mut(current_root); - for warning in formatted_warnings { + if let Some(warning) = formatted_warning { optimization_bailouts.push(OptimizationBailoutItem::Message(warning)); } } From 6dc07368768b98fe971af4ca023b478f4d51d909 Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Tue, 7 Apr 2026 13:18:04 +0800 Subject: [PATCH 6/9] test(stats-output): align side-effects snapshot --- .../side-effects-optimization/__snapshots__/stats.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt b/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt index 266ab347f4a7..25207d6acac2 100644 --- a/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt +++ b/tests/rspack-test/statsOutputCases/side-effects-optimization/__snapshots__/stats.txt @@ -25,8 +25,6 @@ cacheable modules xx KiB Statement with side_effects in source code at ./index.js-31 ModuleConcatenation bailout: Module is an entry point ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/a.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/module-with-export/index.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) Rspack x.x.x compiled successfully in X s asset main.no-side.js xx KiB [emitted] (name: main) @@ -54,5 +52,4 @@ cacheable modules xx KiB [no exports used] ModuleConcatenation bailout: Module is an entry point ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/side-effects-optimization/node_modules/module-with-export/index.js because of /statsOutputCases/side-effects-optimization/node_modules/big-module/index.js: Reexports in this module do not have a static target (first hit: huh : used in main) Rspack x.x.x compiled successfully in X s \ No newline at end of file From 0be7b28e1904318519b9b0a821bef2681cfbb5fe Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Tue, 7 Apr 2026 13:43:11 +0800 Subject: [PATCH 7/9] test(stats-output): align scope-hoisting bailouts snapshot --- .../scope-hoisting-bailouts/__snapshots__/stats.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/rspack-test/statsOutputCases/scope-hoisting-bailouts/__snapshots__/stats.txt b/tests/rspack-test/statsOutputCases/scope-hoisting-bailouts/__snapshots__/stats.txt index 74c657d248b7..f47f4e423bb8 100644 --- a/tests/rspack-test/statsOutputCases/scope-hoisting-bailouts/__snapshots__/stats.txt +++ b/tests/rspack-test/statsOutputCases/scope-hoisting-bailouts/__snapshots__/stats.txt @@ -5,9 +5,6 @@ cacheable modules xx bytes Statement with side_effects in source code at ./index.js-26 ModuleConcatenation bailout: Module is an entry point ModuleConcatenation bailout: Cannot concat with /statsOutputCases/scope-hoisting-bailouts/cjs.js: Module is not an ECMAScript module - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/scope-hoisting-bailouts/eval.js: Module uses eval() - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/scope-hoisting-bailouts/module-id.js: Module uses module.id - ModuleConcatenation bailout: Cannot concat with /statsOutputCases/scope-hoisting-bailouts/module-loaded.js: Module uses module.loaded ./entry.js xx bytes [built] [code generated] ModuleConcatenation bailout: Module is an entry point ./cjs.js xx bytes [built] [code generated] From 929e856579f685a210a5dcc595ae28cd83668775 Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Tue, 7 Apr 2026 14:59:37 +0800 Subject: [PATCH 8/9] fix(module-concat): resolve rebase drift --- .../src/plugin/module_concatenation_plugin.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index cfedc13dda96..8834823b0e26 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -514,8 +514,7 @@ impl ModuleConcatenationPlugin { active_incomings, cached_module_runtime, module_graph, - module_graph_cache, - &compilation.exports_info_artifact, + &module_graph_artifacts, ) }); @@ -597,8 +596,9 @@ impl ModuleConcatenationPlugin { connection.is_target_active( module_graph, runtime, - module_graph_cache, - &compilation.exports_info_artifact, + module_graph_artifacts.mg_cache, + module_graph_artifacts.side_effects_state_artifact, + module_graph_artifacts.exports_info_artifact, ) }); @@ -1349,15 +1349,15 @@ impl ModuleConcatenationPlugin { side_effects_state_artifact: &side_effects_state_artifact, exports_info_artifact: &compilation.exports_info_artifact, }; - - let imports = Self::get_imports( - module_graph, - &module_graph_artifacts, - *current_root, - active_runtime.as_ref(), - &mut imports_cache, - &modules_without_runtime_cache, - ); + Self::get_imports( + module_graph, + &module_graph_artifacts, + *current_root, + active_runtime.as_ref(), + &mut imports_cache, + &modules_without_runtime_cache, + ) + }; for import in imports.iter().copied() { candidates.push_back(import); } From 25d0e18a772da280f309efc53549cdbc5660279c Mon Sep 17 00:00:00 2001 From: LingyuCoder Date: Tue, 7 Apr 2026 15:58:13 +0800 Subject: [PATCH 9/9] perf(module-concat): reuse per-root state --- .../src/plugin/module_concatenation_plugin.rs | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs index 8834823b0e26..84e1cd4fb325 100644 --- a/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/module_concatenation_plugin.rs @@ -290,6 +290,11 @@ impl RuntimeIdentifierCache { self.no_runtime_map.get(module) } } + + fn clear(&mut self) { + self.no_runtime_map.clear(); + self.runtime_map.clear(); + } } impl ModuleConcatenationPlugin { @@ -691,7 +696,6 @@ impl ModuleConcatenationPlugin { return Some(problem); } - incoming_modules.sort(); let incoming_modules: Arc<[ModuleIdentifier]> = incoming_modules.into(); success_cache.insert(*module_id, runtime, incoming_modules.clone()); incoming_modules @@ -1178,6 +1182,11 @@ impl ModuleConcatenationPlugin { let mut concat_configurations: Vec = Vec::new(); let mut used_as_inner: IdentifierSet = IdentifierSet::default(); let mut imports_cache = RuntimeIdentifierCache::>::default(); + let mut failure_cache = IdentifierMap::default(); + let mut success_cache = RuntimeIdentifierCache::>::default(); + let mut candidates_visited = IdentifierSet::default(); + let mut candidates = VecDeque::new(); + let mut import_candidates = IdentifierSet::default(); let module_graph = compilation.get_module_graph(); let module_graph_cache = &compilation.module_graph_cache_artifact; @@ -1251,31 +1260,34 @@ impl ModuleConcatenationPlugin { module_graph.get_incoming_connections_by_origin_module(&module_id); let (incoming_connections_from_non_modules, incoming_connections_from_modules) = incoming_connections.into_parts(); + let mut incoming_connections_from_modules = incoming_connections_from_modules + .into_iter() + .map(|(origin_module, connections)| { + (origin_module, connections.into_iter().cloned().collect()) + }) + .collect::>(); + incoming_connections_from_modules.sort_by_key(|(origin_module, _)| *origin_module); let incomings = IncomingConnections { from_non_modules: incoming_connections_from_non_modules .into_iter() .cloned() .collect(), - from_modules: incoming_connections_from_modules - .into_iter() - .map(|(origin_module, connections)| { - (origin_module, connections.into_iter().cloned().collect()) - }) - .collect(), + from_modules: incoming_connections_from_modules, }; let incoming_connections_len = incomings.from_non_modules.len() + incomings .from_modules - .values() - .map(std::vec::Vec::len) + .iter() + .map(|(_, connections)| connections.len()) .sum::(); let mut active_incomings = HashMap::with_capacity_and_hasher(incoming_connections_len, Default::default()); - for connection in incomings - .from_non_modules - .iter() - .chain(incomings.from_modules.values().flatten()) - { + for connection in incomings.from_non_modules.iter().chain( + incomings + .from_modules + .iter() + .flat_map(|(_, connections)| connections.iter()), + ) { active_incomings.insert( connection.dependency_id, connection.is_active( @@ -1335,18 +1347,16 @@ impl ModuleConcatenationPlugin { let mut current_configuration = ConcatConfiguration::new(*current_root, active_runtime.clone()); - let mut failure_cache = IdentifierMap::default(); - let mut success_cache = RuntimeIdentifierCache::>::default(); - let mut candidates_visited = IdentifierSet::default(); - let mut candidates = VecDeque::new(); + failure_cache.clear(); + success_cache.clear(); + candidates_visited.clear(); let imports = { - let side_effects_state_artifact = compilation + let side_effects_state_artifact = &compilation .build_module_graph_artifact - .side_effects_state_artifact - .clone(); + .side_effects_state_artifact; let module_graph_artifacts = ModuleGraphArtifacts { mg_cache: module_graph_cache, - side_effects_state_artifact: &side_effects_state_artifact, + side_effects_state_artifact, exports_info_artifact: &compilation.exports_info_artifact, }; Self::get_imports( @@ -1358,11 +1368,12 @@ impl ModuleConcatenationPlugin { &modules_without_runtime_cache, ) }; + candidates.clear(); for import in imports.iter().copied() { candidates.push_back(import); } - let mut import_candidates = IdentifierSet::default(); + import_candidates.clear(); while let Some(imp) = candidates.pop_front() { if candidates_visited.contains(&imp) { continue; @@ -1620,7 +1631,7 @@ struct Statistics { #[derive(Debug, Default)] struct IncomingConnections { from_non_modules: Vec, - from_modules: IdentifierMap>, + from_modules: Vec<(ModuleIdentifier, Vec)>, } #[derive(Debug)]