Skip to content

Commit 91ca4ea

Browse files
Boshenclaudeautofix-ci[bot]
authored
feat(rust): make bundler generic over FileSystem for in-memory benchmarks (#8652)
## Summary - Propagate `Fs: FileSystem` type parameter through the bundler pipeline (`TaskContext` → `ModuleLoader` → `ScanStage` → `Bundle`) so benchmarks can inject a `MemoryFileSystem`, eliminating disk I/O noise from measurements - Add `BundleFactory::create_bundle_with_fs()` for injecting custom FS/resolver pairs - Update benchmarks to preload source files into `MemoryFileSystem` outside the timed loop The public API (`Bundler`, `BundlerBuilder`) remains unchanged — all generic structs use `= OsFileSystem` defaults. Closes #8642 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 9710def commit 91ca4ea

File tree

22 files changed

+223
-85
lines changed

22 files changed

+223
-85
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bench/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ test = false
1616

1717
[dependencies]
1818
rolldown = { workspace = true, features = ["experimental"] }
19+
rolldown_fs = { workspace = true, features = ["memory"] }
20+
rolldown_resolver = { workspace = true }
1921
rolldown_workspace = { workspace = true }
2022

2123
[[bench]]

crates/bench/benches/bundle.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bench::{DeriveOptions, derive_benchmark_items};
1+
use bench::{DeriveOptions, create_bench_context, derive_benchmark_items};
22
use criterion::{Criterion, criterion_group, criterion_main};
33

44
use rolldown_common::BundlerOptions;
@@ -23,6 +23,8 @@ fn criterion_benchmark(c: &mut Criterion) {
2323
.into_iter()
2424
.flat_map(|(name, options)| derive_benchmark_items(&derive_options, name, options))
2525
.for_each(|item| {
26+
let mut ctx = create_bench_context(&item.options);
27+
2628
group.bench_function(format!("bundle@{}", item.name), move |b| {
2729
b.to_async(
2830
tokio::runtime::Builder::new_multi_thread()
@@ -32,12 +34,15 @@ fn criterion_benchmark(c: &mut Criterion) {
3234
.build()
3335
.unwrap(),
3436
)
35-
.iter(|| async {
36-
let mut bundler =
37-
rolldown::Bundler::new(item.options.clone()).expect("Failed to create bundler");
38-
let result = bundler.generate().await;
39-
if let Err(e) = result {
40-
panic!("Failed to bundle: {e}");
37+
.iter(|| {
38+
let mem_fs = ctx.mem_fs.clone();
39+
let resolver = ctx.create_resolver();
40+
let bundle = ctx.factory.create_bundle_with_fs(mem_fs, resolver);
41+
async move {
42+
let result = bundle.generate().await;
43+
if let Err(e) = result {
44+
panic!("Failed to bundle: {e}");
45+
}
4146
}
4247
});
4348
});

crates/bench/benches/scan.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bench::{DeriveOptions, derive_benchmark_items};
1+
use bench::{DeriveOptions, create_bench_context, derive_benchmark_items};
22
use criterion::{Criterion, criterion_group, criterion_main};
33

44
use rolldown_common::BundlerOptions;
@@ -20,6 +20,8 @@ fn criterion_benchmark(c: &mut Criterion) {
2020
.into_iter()
2121
.flat_map(|(name, options)| derive_benchmark_items(&derive_options, name, options))
2222
.for_each(|item| {
23+
let mut ctx = create_bench_context(&item.options);
24+
2325
group.bench_function(format!("scan@{}", item.name), move |b| {
2426
b.to_async(
2527
tokio::runtime::Builder::new_multi_thread()
@@ -29,10 +31,13 @@ fn criterion_benchmark(c: &mut Criterion) {
2931
.build()
3032
.unwrap(),
3133
)
32-
.iter(|| async {
33-
let mut rolldown_bundler =
34-
rolldown::Bundler::new(item.options.clone()).expect("Failed to create bundler");
35-
rolldown_bundler.scan().await.expect("should not failed in scan");
34+
.iter(|| {
35+
let mem_fs = ctx.mem_fs.clone();
36+
let resolver = ctx.create_resolver();
37+
let bundle = ctx.factory.create_bundle_with_fs(mem_fs, resolver);
38+
async move {
39+
bundle.scan().await.expect("should not fail in scan");
40+
}
3641
});
3742
});
3843
});

crates/bench/src/lib.rs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
use std::path::PathBuf;
1+
use std::path::{Path, PathBuf};
2+
use std::sync::Arc;
23

3-
use rolldown::BundlerOptions;
4+
use rolldown::{
5+
BundleFactory, BundleFactoryOptions, BundlerOptions, Platform, ResolveOptions, TsConfig,
6+
};
7+
use rolldown_fs::MemoryFileSystem;
8+
use rolldown_resolver::Resolver;
49
use rolldown_workspace::root_dir;
510

611
pub fn join_by_workspace_root(path: &str) -> PathBuf {
@@ -60,3 +65,86 @@ pub fn derive_benchmark_items(
6065

6166
ret
6267
}
68+
69+
/// Walk a directory recursively and load all files into a `MemoryFileSystem`.
70+
/// This is used in benchmarks to eliminate disk I/O from the timed section.
71+
pub fn preload_into_memory_fs(dir: &Path) -> MemoryFileSystem {
72+
let mut fs = MemoryFileSystem::default();
73+
walk_and_load(dir, &mut fs);
74+
fs
75+
}
76+
77+
fn walk_and_load(dir: &Path, fs: &mut MemoryFileSystem) {
78+
let entries = match std::fs::read_dir(dir) {
79+
Ok(entries) => entries,
80+
Err(_) => return,
81+
};
82+
for entry in entries.flatten() {
83+
let path = entry.path();
84+
if path.is_dir() {
85+
walk_and_load(&path, fs);
86+
} else if path.is_file()
87+
&& let Ok(content) = std::fs::read(&path)
88+
{
89+
fs.add_file_bytes(&path, &content);
90+
}
91+
}
92+
}
93+
94+
/// Precomputed benchmark context: factory, MemoryFileSystem, and resolver config.
95+
/// Created once per benchmark item (outside the timed loop).
96+
pub struct BenchContext {
97+
pub factory: BundleFactory,
98+
pub mem_fs: MemoryFileSystem,
99+
pub cwd: PathBuf,
100+
pub platform: Platform,
101+
pub tsconfig: TsConfig,
102+
pub raw_resolve: ResolveOptions,
103+
}
104+
105+
impl BenchContext {
106+
/// Create a fresh resolver for each benchmark iteration to avoid cache warming bias.
107+
pub fn create_resolver(&self) -> Arc<Resolver<MemoryFileSystem>> {
108+
Arc::new(Resolver::new(
109+
self.mem_fs.clone(),
110+
self.cwd.clone(),
111+
self.platform,
112+
&self.tsconfig,
113+
self.raw_resolve.clone(),
114+
))
115+
}
116+
}
117+
118+
/// Create a `BenchContext` for a given set of bundler options.
119+
/// This performs all one-time setup (option normalization, FS preloading, resolver creation)
120+
/// so the timed loop only measures bundling work.
121+
pub fn create_bench_context(options: &BundlerOptions) -> BenchContext {
122+
let cwd = options
123+
.cwd
124+
.clone()
125+
.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current dir"));
126+
let mem_fs = preload_into_memory_fs(&cwd);
127+
// Mirror the normalization in prepare_build_context: derive platform from format,
128+
// and add default condition_names for Browser/Node.
129+
let format = options.format.unwrap_or(rolldown::OutputFormat::Esm);
130+
let platform = options.platform.unwrap_or(match format {
131+
rolldown::OutputFormat::Cjs => Platform::Node,
132+
rolldown::OutputFormat::Esm | rolldown::OutputFormat::Iife | rolldown::OutputFormat::Umd => {
133+
Platform::Browser
134+
}
135+
});
136+
let tsconfig = options.tsconfig.clone().map(|tc| tc.with_base(&cwd)).unwrap_or_default();
137+
let mut raw_resolve = options.resolve.clone().unwrap_or_default();
138+
if raw_resolve.condition_names.is_none() && matches!(platform, Platform::Browser | Platform::Node)
139+
{
140+
raw_resolve.condition_names = Some(vec!["module".to_string()]);
141+
}
142+
let factory = BundleFactory::new(BundleFactoryOptions {
143+
bundler_options: options.clone(),
144+
plugins: vec![],
145+
session: None,
146+
disable_tracing_setup: true,
147+
})
148+
.expect("Failed to create bundle factory");
149+
BenchContext { factory, mem_fs, cwd, platform, tsconfig, raw_resolve }
150+
}

crates/rolldown/src/bundle/bundle.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ use sugar_path::SugarPath;
2828
clippy::struct_field_names,
2929
reason = "`bundle_span` emphasizes this's a span for this bundle, not a session level span"
3030
)]
31-
pub struct Bundle {
32-
pub(crate) fs: OsFileSystem,
31+
pub struct Bundle<Fs: FileSystem + Clone + 'static = OsFileSystem> {
32+
pub(crate) fs: Fs,
3333
pub(crate) options: SharedOptions,
34-
pub(crate) resolver: SharedResolver,
34+
pub(crate) resolver: SharedResolver<Fs>,
3535
pub(crate) file_emitter: SharedFileEmitter,
3636
pub(crate) plugin_driver: SharedPluginDriver,
3737
pub(crate) warnings: Vec<BuildDiagnostic>,
3838
pub(crate) cache: ScanStageCache,
3939
pub(crate) bundle_span: Arc<tracing::Span>,
4040
}
4141

42-
impl Bundle {
42+
impl<Fs: FileSystem + Clone + 'static> Bundle<Fs> {
4343
#[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
4444
/// This method intentionally get the ownership of `self` to show that the method cannot be called multiple times.
4545
pub async fn write(mut self) -> BuildResult<BundleOutput> {

crates/rolldown/src/bundle/bundle_factory.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use rolldown_common::{
77
SharedModuleInfoDashMap,
88
};
99
use rolldown_error::{BuildDiagnostic, BuildResult, EventKindSwitcher};
10-
use rolldown_fs::OsFileSystem;
10+
use rolldown_fs::{FileSystem, OsFileSystem};
1111
use rolldown_plugin::{__inner::SharedPluginable, PluginDriverFactory};
1212
use rolldown_plugin_lazy_compilation::LazyCompilationContext;
1313
use rolldown_utils::dashmap::FxDashSet;
@@ -36,7 +36,7 @@ pub struct BundleFactory {
3636
pub plugin_driver_factory: PluginDriverFactory,
3737
pub fs: OsFileSystem,
3838
pub options: SharedOptions,
39-
pub resolver: SharedResolver,
39+
pub resolver: SharedResolver<OsFileSystem>,
4040
pub file_emitter: SharedFileEmitter,
4141
/// Warnings collected during bundle factory creation.
4242
/// These warnings are transferred to the first created `Bundle` via `create_bundle()` or `create_incremental_bundle()`.
@@ -110,9 +110,7 @@ impl BundleFactory {
110110
&mut self,
111111
bundle_mode: BundleMode,
112112
cache: Option<ScanStageCache>,
113-
) -> BuildResult<Bundle> {
114-
let bundle_span = self.generate_unique_bundle_span();
115-
113+
) -> BuildResult<Bundle<OsFileSystem>> {
116114
let cache = if bundle_mode.is_incremental() {
117115
if let Some(cache) = cache {
118116
cache
@@ -132,6 +130,29 @@ impl BundleFactory {
132130
// Also reset transform dependencies for full builds
133131
self.transform_dependencies_for_incremental_build = Arc::default();
134132
}
133+
134+
Ok(self.build_bundle(self.fs.clone(), Arc::clone(&self.resolver), cache))
135+
}
136+
137+
/// Create a bundle with a custom filesystem and resolver.
138+
/// This always performs a full build (no incremental cache).
139+
pub fn create_bundle_with_fs<Fs: FileSystem + Clone + 'static>(
140+
&mut self,
141+
fs: Fs,
142+
resolver: SharedResolver<Fs>,
143+
) -> Bundle<Fs> {
144+
self.module_infos_for_incremental_build = Arc::default();
145+
self.transform_dependencies_for_incremental_build = Arc::default();
146+
self.build_bundle(fs, resolver, ScanStageCache::default())
147+
}
148+
149+
fn build_bundle<Fs: FileSystem + Clone + 'static>(
150+
&mut self,
151+
fs: Fs,
152+
resolver: SharedResolver<Fs>,
153+
cache: ScanStageCache,
154+
) -> Bundle<Fs> {
155+
let bundle_span = self.generate_unique_bundle_span();
135156
let module_infos = Arc::clone(&self.module_infos_for_incremental_build);
136157
let transform_dependencies = Arc::clone(&self.transform_dependencies_for_incremental_build);
137158

@@ -144,17 +165,17 @@ impl BundleFactory {
144165
transform_dependencies,
145166
);
146167
let bundle = Bundle {
147-
fs: self.fs.clone(),
168+
fs,
148169
options: Arc::clone(&self.options),
149-
resolver: Arc::clone(&self.resolver),
170+
resolver,
150171
file_emitter: Arc::clone(&self.file_emitter),
151172
plugin_driver,
152173
warnings: std::mem::take(&mut self.warnings),
153174
bundle_span,
154175
cache,
155176
};
156177
self.last_bundle_handle = Some(bundle.context());
157-
Ok(bundle)
178+
bundle
158179
}
159180

160181
fn check_prefer_builtin_feature(

crates/rolldown/src/hmr/hmr_stage.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use rolldown_common::{
1515
use rolldown_ecmascript::{EcmaAst, EcmaCompiler, PrintCommentsOptions, PrintOptions};
1616
use rolldown_ecmascript_utils::AstSnippet;
1717
use rolldown_error::BuildResult;
18-
use rolldown_fs::OsFileSystem;
18+
use rolldown_fs::FileSystem;
1919
use rolldown_plugin::SharedPluginDriver;
2020
use rolldown_sourcemap::{Source, SourceJoiner, SourceMapSource};
2121
#[cfg(not(target_family = "wasm"))]
@@ -34,16 +34,16 @@ use crate::{
3434
utils::process_code_and_sourcemap::process_code_and_sourcemap,
3535
};
3636

37-
pub struct HmrStageInput<'a> {
37+
pub struct HmrStageInput<'a, Fs: FileSystem + Clone + 'static> {
3838
pub options: SharedOptions,
39-
pub fs: OsFileSystem,
40-
pub resolver: SharedResolver,
39+
pub fs: Fs,
40+
pub resolver: SharedResolver<Fs>,
4141
pub plugin_driver: SharedPluginDriver,
4242
pub cache: &'a mut ScanStageCache,
4343
pub next_hmr_patch_id: Arc<AtomicU32>,
4444
}
4545

46-
impl HmrStageInput<'_> {
46+
impl<Fs: FileSystem + Clone + 'static> HmrStageInput<'_, Fs> {
4747
pub fn module_table(&self) -> &ModuleTable {
4848
&self.cache.get_snapshot().module_table
4949
}
@@ -53,26 +53,26 @@ impl HmrStageInput<'_> {
5353
}
5454
}
5555

56-
pub struct HmrStage<'a> {
57-
pub(crate) input: HmrStageInput<'a>,
56+
pub struct HmrStage<'a, Fs: FileSystem + Clone + 'static> {
57+
pub(crate) input: HmrStageInput<'a, Fs>,
5858
}
5959

60-
impl<'a> Deref for HmrStage<'a> {
61-
type Target = HmrStageInput<'a>;
60+
impl<'a, Fs: FileSystem + Clone + 'static> Deref for HmrStage<'a, Fs> {
61+
type Target = HmrStageInput<'a, Fs>;
6262

6363
fn deref(&self) -> &Self::Target {
6464
&self.input
6565
}
6666
}
6767

68-
impl DerefMut for HmrStage<'_> {
68+
impl<Fs: FileSystem + Clone + 'static> DerefMut for HmrStage<'_, Fs> {
6969
fn deref_mut(&mut self) -> &mut Self::Target {
7070
&mut self.input
7171
}
7272
}
7373

74-
impl<'a> HmrStage<'a> {
75-
pub fn new(input: HmrStageInput<'a>) -> Self {
74+
impl<'a, Fs: FileSystem + Clone + 'static> HmrStage<'a, Fs> {
75+
pub fn new(input: HmrStageInput<'a, Fs>) -> Self {
7676
Self { input }
7777
}
7878

@@ -1234,7 +1234,7 @@ struct ModuleRenderInput {
12341234
pub ecma_ast: EcmaAst,
12351235
}
12361236

1237-
impl HmrStage<'_> {
1237+
impl<Fs: FileSystem + Clone + 'static> HmrStage<'_, Fs> {
12381238
fn collect_sync_dependencies_for_client(
12391239
&self,
12401240
proxy_entry_idx: ModuleIdx,

crates/rolldown/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ mod types;
1313
mod utils;
1414
use std::sync::Arc;
1515

16-
use rolldown_fs::OsFileSystem;
1716
use rolldown_resolver::Resolver;
1817

19-
pub(crate) type SharedResolver = Arc<Resolver<OsFileSystem>>;
18+
pub(crate) type SharedResolver<Fs> = Arc<Resolver<Fs>>;
2019
pub(crate) type SharedOptions = SharedNormalizedBundlerOptions;
2120

2221
pub use crate::{

0 commit comments

Comments
 (0)