Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 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