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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/node_binding/napi-binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2293,6 +2293,7 @@ export interface RawExperiments {
css?: boolean
deferImport: boolean
sourceImport: boolean
fasterModuleConcatenation: boolean
pureFunctions: boolean
runtimeMode?: "webpack" | "rspack"
}
Expand Down
11 changes: 11 additions & 0 deletions crates/rspack/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3682,6 +3682,8 @@ pub struct ExperimentsBuilder {
defer_import: Option<bool>,
/// Whether to enable source import.
source_import: Option<bool>,
/// Whether to enable the faster module concatenation implementation.
faster_module_concatenation: Option<bool>,
// TODO: lazy compilation
pure_functions: Option<bool>,
runtime_mode: Option<RuntimeMode>,
Expand All @@ -3695,6 +3697,7 @@ impl From<Experiments> for ExperimentsBuilder {
async_web_assembly: None,
defer_import: Some(value.defer_import),
source_import: Some(value.source_import),
faster_module_concatenation: Some(value.faster_module_concatenation),
pure_functions: Some(value.pure_functions),
runtime_mode: Some(value.runtime_mode),
}
Expand All @@ -3709,6 +3712,7 @@ impl From<&mut ExperimentsBuilder> for ExperimentsBuilder {
async_web_assembly: value.async_web_assembly.take(),
defer_import: value.defer_import.take(),
source_import: value.source_import.take(),
faster_module_concatenation: value.faster_module_concatenation.take(),
pure_functions: value.pure_functions.take(),
runtime_mode: value.runtime_mode.take(),
}
Expand Down Expand Up @@ -3746,6 +3750,12 @@ impl ExperimentsBuilder {
self
}

/// Set whether to enable the faster module concatenation implementation.
pub fn faster_module_concatenation(&mut self, faster_module_concatenation: bool) -> &mut Self {
self.faster_module_concatenation = Some(faster_module_concatenation);
self
}

/// Build [`Experiments`] from options.
///
/// [`Experiments`]: rspack_core::options::Experiments
Expand All @@ -3764,6 +3774,7 @@ impl ExperimentsBuilder {
css: d!(self.css, false),
defer_import: d!(self.defer_import, false),
source_import: d!(self.source_import, false),
faster_module_concatenation: d!(self.faster_module_concatenation, false),
pure_functions: d!(self.pure_functions, _production),
runtime_mode: d!(self.runtime_mode, RuntimeMode::Webpack),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,7 @@ CompilerOptions {
css: false,
defer_import: false,
source_import: false,
faster_module_concatenation: false,
pure_functions: false,
runtime_mode: Webpack,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct RawExperiments {
pub css: Option<bool>,
pub defer_import: bool,
pub source_import: bool,
pub faster_module_concatenation: bool,
pub pure_functions: bool,
#[napi(ts_type = "\"webpack\" | \"rspack\"")]
pub runtime_mode: Option<String>,
Expand All @@ -32,6 +33,7 @@ impl From<RawExperiments> for Experiments {
css: value.css.unwrap_or(false),
defer_import: value.defer_import,
source_import: value.source_import,
faster_module_concatenation: value.faster_module_concatenation,
pure_functions: value.pure_functions,
runtime_mode,
}
Expand Down
210 changes: 210 additions & 0 deletions crates/rspack_core/src/artifacts/code_generation_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,62 @@ impl CodeGenerationDataTopLevelDeclarations {
}
}

#[derive(Clone, Debug, Default)]
pub struct CodeGenerationDataRenderedInitFragments {
start: String,
end: String,
}

impl CodeGenerationDataRenderedInitFragments {
pub fn new(start: String, end: String) -> Self {
Self { start, end }
}

pub fn start(&self) -> &str {
&self.start
}

pub fn end(&self) -> &str {
&self.end
}

pub fn is_empty(&self) -> bool {
self.start.is_empty() && self.end.is_empty()
}

pub(crate) fn hash_parts(start: &str, end: &str, state: &mut RspackHasher) {
state.write(b"CodeGenerationDataRenderedInitFragments");
state.write(&(start.len() as u64).to_be_bytes());
state.write(start.as_bytes());
state.write(&(end.len() as u64).to_be_bytes());
state.write(end.as_bytes());
}
}

impl RspackHash for CodeGenerationDataRenderedInitFragments {
fn hash(&self, state: &mut RspackHasher) {
Self::hash_parts(&self.start, &self.end, state);
}
}

#[derive(Clone, Debug)]
pub struct CodeGenerationDataRenderedInitFragmentsDigest {
inner: RspackHashDigest,
}

impl CodeGenerationDataRenderedInitFragmentsDigest {
pub fn new(inner: RspackHashDigest) -> Self {
Self { inner }
}
}

impl RspackHash for CodeGenerationDataRenderedInitFragmentsDigest {
fn hash(&self, state: &mut RspackHasher) {
state.write(b"CodeGenerationDataRenderedInitFragmentsDigest");
self.inner.hash(state);
}
}
Comment thread
LingyuCoder marked this conversation as resolved.

#[derive(Clone, Debug)]
pub struct CodeGenerationExportsFinalNames {
inner: HashMap<String, String>,
Expand Down Expand Up @@ -139,6 +195,23 @@ pub struct CodeGenerationResult {
}

impl CodeGenerationResult {
fn hash_rendered_init_fragments(&self, hasher: &mut RspackHasher) {
if let Some(fragments) = self.data.get::<CodeGenerationDataRenderedInitFragments>()
&& !fragments.is_empty()
{
fragments.hash(hasher);
}
}

fn hash_rendered_init_fragments_digest(&self, hasher: &mut RspackHasher) {
if let Some(digest) = self
.data
.get::<CodeGenerationDataRenderedInitFragmentsDigest>()
{
digest.hash(hasher);
}
}

pub fn with_javascript(mut self, generation_result: BoxSource) -> Self {
self.inner.insert(SourceType::JavaScript, generation_result);
self
Expand Down Expand Up @@ -173,6 +246,25 @@ impl CodeGenerationResult {
self.hash = Some(hasher.digest(hash_digest));
}

/// Hash a scoped normal-module result whose rendered init fragments are
/// carried as code generation data instead of being part of `inner`.
pub fn set_hash_with_rendered_init_fragments(
&mut self,
hash_function: &HashFunction,
hash_digest: &HashDigest,
hash_salt: &HashSalt,
) {
let mut hasher = RspackHasher::with_salt(hash_function, hash_salt);
for (source_type, source) in self.inner.as_ref() {
source_type.hash(&mut hasher);
std::hash::Hash::hash(source, &mut hasher);
}
self.hash_rendered_init_fragments(&mut hasher);
self.chunk_init_fragments.hash(&mut hasher);
self.runtime_requirements.hash(&mut hasher);
self.hash = Some(hasher.digest(hash_digest));
}

/// Concatenated modules already encode the generated module bodies into
/// `ConcatenatedModule::get_runtime_hash`, so we can reuse that digest here
/// and only mix in codegen-specific metadata instead of hashing the large
Expand All @@ -193,6 +285,24 @@ impl CodeGenerationResult {
self.runtime_requirements.hash(&mut hasher);
self.hash = Some(hasher.digest(hash_digest));
}

pub fn set_hash_for_faster_concatenated_module(
&mut self,
runtime_hash: &RspackHashDigest,
hash_function: &HashFunction,
hash_digest: &HashDigest,
hash_salt: &HashSalt,
) {
let mut hasher = RspackHasher::with_salt(hash_function, hash_salt);
runtime_hash.hash(&mut hasher);
for source_type in self.inner.as_ref().keys() {
source_type.hash(&mut hasher);
}
self.hash_rendered_init_fragments_digest(&mut hasher);
self.chunk_init_fragments.hash(&mut hasher);
self.runtime_requirements.hash(&mut hasher);
self.hash = Some(hasher.digest(hash_digest));
}
}

#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize)]
Expand Down Expand Up @@ -414,3 +524,103 @@ pub struct CodeGenerationJob {
pub runtimes: Vec<RuntimeSpec>,
pub scope: Option<ConcatenationScope>,
}

#[cfg(test)]
mod tests {
use super::*;

fn hash_with_fragments(start: &str, end: &str) -> RspackHashDigest {
let mut result = CodeGenerationResult::default();
if !start.is_empty() || !end.is_empty() {
result
.data
.insert(CodeGenerationDataRenderedInitFragments::new(
start.to_string(),
end.to_string(),
));
}
result.set_hash_with_rendered_init_fragments(
&HashFunction::Xxhash64,
&HashDigest::Hex,
&HashSalt::default(),
);
result.hash.expect("hash should be set")
}

fn concatenated_hash_with_fragments(start: &str, end: &str) -> RspackHashDigest {
let mut result = CodeGenerationResult::default();
if !start.is_empty() || !end.is_empty() {
let mut fragments_hasher = RspackHasher::new(&HashFunction::Xxhash64);
CodeGenerationDataRenderedInitFragments::hash_parts(start, end, &mut fragments_hasher);
result
.data
.insert(CodeGenerationDataRenderedInitFragmentsDigest::new(
fragments_hasher.digest(&HashDigest::Hex),
));
}
result.set_hash_for_faster_concatenated_module(
&RspackHashDigest::from("runtime"),
&HashFunction::Xxhash64,
&HashDigest::Hex,
&HashSalt::default(),
);
result.hash.expect("hash should be set")
}

#[test]
fn rendered_init_fragments_affect_codegen_hash() {
let without_fragments = hash_with_fragments("", "");
let with_start = hash_with_fragments("const value = 1;", "");
let with_end = hash_with_fragments("", "value();");

assert_ne!(without_fragments, with_start);
assert_ne!(without_fragments, with_end);
assert_ne!(with_start, with_end);
}

#[test]
fn rendered_init_fragment_boundaries_affect_codegen_hash() {
assert_ne!(
hash_with_fragments("ab", "c"),
hash_with_fragments("a", "bc")
);
}

#[test]
fn empty_rendered_init_fragments_preserve_codegen_hash() {
let without_fragments = hash_with_fragments("", "");
let mut result = CodeGenerationResult::default();
result
.data
.insert(CodeGenerationDataRenderedInitFragments::default());
result.set_hash_with_rendered_init_fragments(
&HashFunction::Xxhash64,
&HashDigest::Hex,
&HashSalt::default(),
);

assert_eq!(without_fragments, result.hash.expect("hash should be set"));
}

#[test]
fn rendered_init_fragments_affect_concatenated_codegen_hash() {
assert_ne!(
concatenated_hash_with_fragments("", ""),
concatenated_hash_with_fragments("const value = 1;", "")
);
}

#[test]
fn empty_rendered_init_fragments_preserve_concatenated_codegen_hash() {
let faster_hash = concatenated_hash_with_fragments("", "");
let mut legacy_result = CodeGenerationResult::default();
legacy_result.set_hash_for_concatenated_module(
&RspackHashDigest::from("runtime"),
&HashFunction::Xxhash64,
&HashDigest::Hex,
&HashSalt::default(),
);

assert_eq!(faster_hash, legacy_result.hash.expect("hash should be set"));
}
}
24 changes: 22 additions & 2 deletions crates/rspack_core/src/compilation/code_generation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,28 @@ pub(crate) async fn code_generation_modules(
// Concatenated modules are special here: `job.hash` already
// fingerprints the generated module bodies, so we only need
// to fold in the remaining codegen metadata.
codegen_res.set_hash_for_concatenated_module(
&job.hash,
if options.experiments.faster_module_concatenation {
codegen_res.set_hash_for_faster_concatenated_module(
&job.hash,
&options.output.hash_function,
&options.output.hash_digest,
&options.output.hash_salt,
);
} else {
codegen_res.set_hash_for_concatenated_module(
&job.hash,
&options.output.hash_function,
&options.output.hash_digest,
&options.output.hash_salt,
);
}
} else if options.experiments.faster_module_concatenation
&& job
.scope
.as_ref()
.is_some_and(ConcatenationScope::is_faster_module_concatenation)
{
codegen_res.set_hash_with_rendered_init_fragments(
&options.output.hash_function,
&options.output.hash_digest,
&options.output.hash_salt,
Expand Down
Loading
Loading