diff --git a/Cargo.lock b/Cargo.lock index 58d70d348d9ad..13d23ebaaa303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1824,6 +1824,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" + [[package]] name = "inotify" version = "0.7.1" @@ -2419,6 +2425,7 @@ dependencies = [ "owo-colors", "parking_lot", "portpicker", + "rand", "regex", "serde", "serde_json", @@ -5475,6 +5482,8 @@ version = "0.1.0" dependencies = [ "anyhow", "clap 4.0.18", + "indoc", + "pathdiff", "serde_json", "tempfile", ] diff --git a/crates/next-dev/Cargo.toml b/crates/next-dev/Cargo.toml index 8c76058588092..4ddfc104abf18 100644 --- a/crates/next-dev/Cargo.toml +++ b/crates/next-dev/Cargo.toml @@ -61,6 +61,7 @@ fs_extra = "1.2.0" lazy_static = "1.4.0" once_cell = "1.13.0" parking_lot = "0.12.1" +rand = "0.8.5" regex = "1.6.0" tempfile = "3.3.0" test-generator = "0.3.0" diff --git a/crates/next-dev/benches/bundlers/mod.rs b/crates/next-dev/benches/bundlers/mod.rs index fef726fb5dc1c..8b62378bc87f1 100644 --- a/crates/next-dev/benches/bundlers/mod.rs +++ b/crates/next-dev/benches/bundlers/mod.rs @@ -9,6 +9,7 @@ use self::{ vite::Vite, webpack::Webpack, }; +use crate::util::env::read_env; mod nextjs; mod parcel; @@ -59,16 +60,16 @@ pub trait Bundler { } pub fn get_bundlers() -> Vec> { - let config = std::env::var("TURBOPACK_BENCH_BUNDLERS").ok(); + let config: String = read_env("TURBOPACK_BENCH_BUNDLERS", String::from("turbopack")).unwrap(); let mut turbopack = false; let mut others = false; - match config.as_deref() { - Some("all") => { + match config.as_ref() { + "all" => { turbopack = true; others = true } - Some("others") => others = true, - None | Some("") => { + "others" => others = true, + "turbopack" => { turbopack = true; } _ => panic!("Invalid value for TURBOPACK_BENCH_BUNDLERS"), diff --git a/crates/next-dev/benches/bundlers/vite/mod.rs b/crates/next-dev/benches/bundlers/vite/mod.rs index 81780d218251c..213e8a51ed671 100644 --- a/crates/next-dev/benches/bundlers/vite/mod.rs +++ b/crates/next-dev/benches/bundlers/vite/mod.rs @@ -46,7 +46,7 @@ impl Bundler for Vite { fn prepare(&self, install_dir: &Path) -> Result<()> { let mut packages = vec![NpmPackage::new("vite", "^3.2.4")]; if self.swc { - packages.push(NpmPackage::new("vite-plugin-swc-react-refresh", "^2.2.0")); + packages.push(NpmPackage::new("vite-plugin-swc-react-refresh", "^2.2.1")); } else { packages.push(NpmPackage::new("@vitejs/plugin-react", "^2.2.0")); }; diff --git a/crates/next-dev/benches/bundlers/webpack/mod.rs b/crates/next-dev/benches/bundlers/webpack/mod.rs index 1fe7add999217..400f4fcb1598d 100644 --- a/crates/next-dev/benches/bundlers/webpack/mod.rs +++ b/crates/next-dev/benches/bundlers/webpack/mod.rs @@ -27,6 +27,7 @@ impl Bundler for Webpack { &[ NpmPackage::new("@pmmmwh/react-refresh-webpack-plugin", "^0.5.7"), NpmPackage::new("@swc/core", "^1.2.249"), + NpmPackage::new("@swc/helpers", "^0.4.13"), NpmPackage::new("react-refresh", "^0.14.0"), NpmPackage::new("swc-loader", "^0.2.3"), NpmPackage::new("webpack", "^5.75.0"), diff --git a/crates/next-dev/benches/mod.rs b/crates/next-dev/benches/mod.rs index ae060159ff893..99a5cfbaa6f91 100644 --- a/crates/next-dev/benches/mod.rs +++ b/crates/next-dev/benches/mod.rs @@ -2,6 +2,10 @@ use std::{ fs::{self}, panic::AssertUnwindSafe, path::Path, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, time::Duration, }; @@ -18,7 +22,12 @@ use tokio::{ time::{sleep, timeout}, }; use turbo_tasks::util::FormatDuration; -use util::{build_test, create_browser, AsyncBencherExtension, PreparedApp, BINDING_NAME}; +use util::{ + build_test, create_browser, + env::{read_env, read_env_bool, read_env_list}, + module_picker::ModulePicker, + AsyncBencherExtension, PreparedApp, BINDING_NAME, +}; use self::{bundlers::RenderType, util::resume_on_error}; use crate::{bundlers::Bundler, util::PageGuard}; @@ -138,6 +147,7 @@ fn bench_hmr_internal(mut g: BenchmarkGroup, location: CodeLocation) { let runtime = Runtime::new().unwrap(); let browser = Lazy::new(|| runtime.block_on(create_browser())); + let hmr_warmup = read_env("TURBOPACK_BENCH_HMR_WARMUP", 10).unwrap(); for bundler in get_bundlers() { if matches!( @@ -153,105 +163,19 @@ fn bench_hmr_internal(mut g: BenchmarkGroup, location: CodeLocation) { for module_count in get_module_counts() { let test_app = Lazy::new(|| build_test(module_count, bundler.as_ref())); let input = (bundler.as_ref(), &test_app); + let module_picker = + Lazy::new(|| Arc::new(ModulePicker::new(test_app.modules().to_vec()))); + resume_on_error(AssertUnwindSafe(|| { g.bench_with_input( BenchmarkId::new(bundler.get_name(), format!("{} modules", module_count)), &input, |b, &(bundler, test_app)| { let test_app = &**test_app; + let modules = test_app.modules(); + let module_picker = &*module_picker; let browser = &*browser; - fn add_code( - bundler: &dyn Bundler, - app_path: &Path, - msg: &str, - location: CodeLocation, - ) -> Result Result<()>> { - let triangle_path = app_path.join("src/triangle.jsx"); - let mut contents = fs::read_to_string(&triangle_path)?; - const INSERTED_CODE_COMMENT: &str = "// Inserted Code:\n"; - const COMPONENT_START: &str = "function Container({ style }) {\n"; - const DETECTOR_START: &str = " { - let a = contents - .find(DETECTOR_START) - .ok_or_else(|| anyhow!("unable to find detector start"))?; - let b = a + contents[a..] - .find(DETECTOR_END) - .ok_or_else(|| anyhow!("unable to find detector end"))?; - contents.replace_range( - a..b, - &format!("{DETECTOR_START}message=\"{msg}\" "), - ); - } - ( - CodeLocation::Evaluation, - RenderType::ClientSideRendered - | RenderType::ServerSidePrerendered, - ) => { - let b = contents - .find(COMPONENT_START) - .ok_or_else(|| anyhow!("unable to find component start"))?; - let code = format!( - "globalThis.{BINDING_NAME} && \ - globalThis.{BINDING_NAME}('{msg}');" - ); - if let Some(a) = contents.find(INSERTED_CODE_COMMENT) { - contents.replace_range( - a..b, - &format!("{INSERTED_CODE_COMMENT}{code}\n"), - ); - } else { - contents.insert_str( - b, - &format!("{INSERTED_CODE_COMMENT}{code}\n"), - ); - } - } - ( - CodeLocation::Evaluation, - RenderType::ServerSideRenderedWithEvents - | RenderType::ServerSideRenderedWithoutInteractivity, - ) => { - panic!( - "evaluation can't be measured for bundlers which evaluate \ - on server side" - ); - } - } - Ok(move || Ok(fs::write(&triangle_path, contents)?)) - } - static CHANGE_TIMEOUT_MESSAGE: &str = - "update was not registered by bundler"; - async fn make_change( - bundler: &dyn Bundler, - guard: &mut PageGuard<'_>, - location: CodeLocation, - m: &WallTime, - ) -> Result { - let msg = - format!("TURBOPACK_BENCH_CHANGE_{}", guard.app_mut().counter()); - let commit = add_code(bundler, guard.app().path(), &msg, location)?; - - let start = m.start(); - commit()?; - - // Wait for the change introduced above to be reflected at - // runtime. This expects HMR or automatic reloading to occur. - timeout(MAX_UPDATE_TIMEOUT, guard.wait_for_binding(&msg)) - .await - .context(CHANGE_TIMEOUT_MESSAGE)??; - - let duration = m.end(start); - - // TODO(sokra) triggering HMR updates too fast can have weird effects - tokio::time::sleep(std::cmp::max(duration, Duration::from_millis(100))) - .await; - - Ok(duration) - } b.to_async(&runtime).try_iter_async( &runtime, || async { @@ -276,36 +200,81 @@ fn bench_hmr_internal(mut g: BenchmarkGroup, location: CodeLocation) { flag", )?; - // TODO(alexkirsz) Turbopack takes a few ms to start listening on - // HMR, and we don't send updates retroactively, so we need to wait - // before starting to make changes. - // This should not be required. - tokio::time::sleep(Duration::from_millis(5000)).await; + // There's a possible race condition between hydration and + // connection to the HMR server. We attempt to make updates with an + // exponential backoff until one succeeds. + let mut exponential_duration = Duration::from_millis(100); + loop { + match make_change( + &modules[0].0, + bundler, + &mut guard, + location, + exponential_duration, + &WallTime, + ) + .await + { + Ok(_) => { + break; + } + Err(e) => { + exponential_duration *= 2; + if exponential_duration > MAX_UPDATE_TIMEOUT { + return Err( + e.context("failed to make warmup change") + ); + } + } + } + } - // Make a warmup change - make_change(bundler, &mut guard, location, &WallTime).await?; + // Once we know the HMR server is connected, we make a few warmup + // changes. + for _ in 0..hmr_warmup { + make_change( + &modules[0].0, + bundler, + &mut guard, + location, + MAX_UPDATE_TIMEOUT, + &WallTime, + ) + .await?; + } Ok(guard) }, - |mut guard, iters, m, verbose| async move { - let mut value = m.zero(); - for iter in 0..iters { - let duration = - make_change(bundler, &mut guard, location, &m).await?; - value = m.add(&value, &duration); - - let i: u64 = iter + 1; - if verbose && i != iters && i.count_ones() == 1 { - eprint!( - " [{:?} {:?}/{}]", - duration, - FormatDuration(value / (i as u32)), - i - ); + |mut guard, iters, m, verbose| { + let module_picker = Arc::clone(module_picker); + async move { + let mut value = m.zero(); + for iter in 0..iters { + let module = module_picker.pick(); + let duration = make_change( + &module, + bundler, + &mut guard, + location, + MAX_UPDATE_TIMEOUT, + &m, + ) + .await?; + value = m.add(&value, &duration); + + let i: u64 = iter + 1; + if verbose && i != iters && i.count_ones() == 1 { + eprint!( + " [{:?} {:?}/{}]", + duration, + FormatDuration(value / (i as u32)), + i + ); + } } - } - Ok((guard, value)) + Ok((guard, value)) + } }, |guard| async move { let hmr_is_happening = guard @@ -325,6 +294,92 @@ fn bench_hmr_internal(mut g: BenchmarkGroup, location: CodeLocation) { } } +fn insert_code( + path: &Path, + bundler: &dyn Bundler, + message: &str, + location: CodeLocation, +) -> Result Result<()>> { + let mut contents = fs::read_to_string(path)?; + + const PRAGMA_EVAL_START: &str = "/* @turbopack-bench:eval-start */"; + const PRAGMA_EVAL_END: &str = "/* @turbopack-bench:eval-end */"; + + let eval_start = contents + .find(PRAGMA_EVAL_START) + .ok_or_else(|| anyhow!("unable to find effect start pragma in {}", contents))?; + let eval_end = contents + .find(PRAGMA_EVAL_END) + .ok_or_else(|| anyhow!("unable to find effect end pragma in {}", contents))?; + + match (location, bundler.render_type()) { + (CodeLocation::Effect, _) => { + contents.replace_range( + eval_start + PRAGMA_EVAL_START.len()..eval_end, + &format!("\nDETECTOR_PROPS.message = \"{message}\";\n"), + ); + } + ( + CodeLocation::Evaluation, + RenderType::ClientSideRendered | RenderType::ServerSidePrerendered, + ) => { + let code = format!( + "\nglobalThis.{BINDING_NAME} && globalThis.{BINDING_NAME}(\"{message}\");\n" + ); + contents.replace_range(eval_start + PRAGMA_EVAL_START.len()..eval_end, &code); + } + ( + CodeLocation::Evaluation, + RenderType::ServerSideRenderedWithEvents + | RenderType::ServerSideRenderedWithoutInteractivity, + ) => { + panic!("evaluation can't be measured for bundlers which evaluate on server side"); + } + } + + let path = path.to_owned(); + Ok(move || Ok(fs::write(&path, contents)?)) +} + +static CHANGE_TIMEOUT_MESSAGE: &str = "update was not registered by bundler"; + +async fn make_change<'a>( + module: &Path, + bundler: &dyn Bundler, + guard: &mut PageGuard<'a>, + location: CodeLocation, + timeout_duration: Duration, + measurement: &WallTime, +) -> Result { + static CHANGE_COUNTER: AtomicUsize = AtomicUsize::new(0); + + let msg = format!( + "TURBOPACK_BENCH_CHANGE_{}", + CHANGE_COUNTER.fetch_add(1, Ordering::Relaxed) + ); + + // Keep the IO out of the measurement. + let commit = insert_code(module, bundler, &msg, location)?; + + let start = measurement.start(); + + commit()?; + + // Wait for the change introduced above to be reflected at runtime. + // This expects HMR or automatic reloading to occur. + timeout(timeout_duration, guard.wait_for_binding(&msg)) + .await + .context(CHANGE_TIMEOUT_MESSAGE)??; + + let duration = measurement.end(start); + + if cfg!(target_os = "linux") { + // TODO(sokra) triggering HMR updates too fast can have weird effects on Linux + tokio::time::sleep(std::cmp::max(duration, Duration::from_millis(100))).await; + } + Ok(duration) +} + fn bench_startup_cached(c: &mut Criterion) { let mut g = c.benchmark_group("bench_startup_cached"); g.sample_size(10); @@ -342,11 +397,7 @@ fn bench_hydration_cached(c: &mut Criterion) { } fn bench_startup_cached_internal(mut g: BenchmarkGroup, hydration: bool) { - let config = std::env::var("TURBOPACK_BENCH_CACHED").ok(); - if matches!( - config.as_deref(), - None | Some("") | Some("no") | Some("false") - ) { + if !read_env_bool("TURBOPACK_BENCH_CACHED") { return; } @@ -432,16 +483,7 @@ fn bench_startup_cached_internal(mut g: BenchmarkGroup, hydration: boo } fn get_module_counts() -> Vec { - let config = std::env::var("TURBOPACK_BENCH_COUNTS").ok(); - match config.as_deref() { - None | Some("") => { - vec![1_000] - } - Some(config) => config - .split(',') - .map(|s| s.parse().expect("Invalid value for TURBOPACK_BENCH_COUNTS")) - .collect(), - } + read_env_list("TURBOPACK_BENCH_COUNTS", vec![1_000usize]).unwrap() } criterion_group!( diff --git a/crates/next-dev/benches/util/env.rs b/crates/next-dev/benches/util/env.rs new file mode 100644 index 0000000000000..5d0678da837c2 --- /dev/null +++ b/crates/next-dev/benches/util/env.rs @@ -0,0 +1,47 @@ +use std::{error::Error, str::FromStr}; + +use anyhow::{anyhow, Context, Result}; + +/// Reads an environment variable. +pub fn read_env(name: &str, default: T) -> Result +where + T: FromStr, + ::Err: Error + Send + Sync + 'static, +{ + let config = std::env::var(name).ok(); + match config.as_deref() { + None | Some("") => Ok(default), + Some(config) => config + .parse() + .with_context(|| anyhow!("Invalid value for {}", name)), + } +} + +/// Reads an boolean-like environment variable, where any value but "0", "no", +/// or "false" is are considered true. +pub fn read_env_bool(name: &str) -> bool { + let config = std::env::var(name).ok(); + match config.as_deref() { + None | Some("") | Some("0") | Some("no") | Some("false") => false, + _ => true, + } +} + +/// Reads a comma-separated environment variable as a vector. +pub fn read_env_list(name: &str, default: Vec) -> Result> +where + T: FromStr, + ::Err: Error + Send + Sync + 'static, +{ + let config = std::env::var(name).ok(); + match config.as_deref() { + None | Some("") => Ok(default), + Some(config) => config + .split(',') + .map(|s| { + s.parse() + .with_context(|| anyhow!("Invalid value for {}", name)) + }) + .collect(), + } +} diff --git a/crates/next-dev/benches/util/mod.rs b/crates/next-dev/benches/util/mod.rs index f8d917b137086..1f075dfdea2f6 100644 --- a/crates/next-dev/benches/util/mod.rs +++ b/crates/next-dev/benches/util/mod.rs @@ -21,8 +21,11 @@ use turbo_tasks::util::FormatDuration; use turbo_tasks_testing::retry::{retry, retry_async}; use turbopack_create_test_app::test_app_builder::{PackageJsonConfig, TestApp, TestAppBuilder}; +use self::env::read_env_bool; use crate::bundlers::Bundler; +pub mod env; +pub mod module_picker; pub mod npm; mod page_guard; mod prepared_app; @@ -76,14 +79,8 @@ pub fn build_test(module_count: usize, bundler: &dyn Bundler) -> TestApp { } pub async fn create_browser() -> Browser { - let with_head = !matches!( - std::env::var("TURBOPACK_BENCH_HEAD").ok().as_deref(), - None | Some("") | Some("no") | Some("false") - ); - let with_devtools = !matches!( - std::env::var("TURBOPACK_BENCH_DEVTOOLS").ok().as_deref(), - None | Some("") | Some("no") | Some("false") - ); + let with_head = read_env_bool("TURBOPACK_BENCH_WITH_HEAD"); + let with_devtools = read_env_bool("TURBOPACK_BENCH_DEVTOOLS"); let mut builder = BrowserConfig::builder(); if with_head { builder = builder.with_head(); @@ -117,12 +114,7 @@ pub async fn create_browser() -> Browser { pub fn resume_on_error(f: F) { let runs_as_bench = std::env::args().find(|a| a == "--bench"); - let ignore_errors = !matches!( - std::env::var("TURBOPACK_BENCH_IGNORE_ERRORS") - .ok() - .as_deref(), - None | Some("") | Some("no") | Some("false") - ); + let ignore_errors = read_env_bool("TURBOPACK_BENCH_IGNORE_ERRORS"); if runs_as_bench.is_some() || ignore_errors { use std::panic::catch_unwind; @@ -160,10 +152,7 @@ impl<'a, 'b, A: AsyncExecutor> AsyncBencherExtension for AsyncBencher<'a, 'b, R: Fn(u64, WallTime) -> F, F: Future>, { - let log_progress = !matches!( - std::env::var("TURBOPACK_BENCH_PROGRESS").ok().as_deref(), - None | Some("") | Some("no") | Some("false") - ); + let log_progress = read_env_bool("TURBOPACK_BENCH_PROGRESS"); let routine = &routine; self.iter_custom(|iters| async move { @@ -190,10 +179,7 @@ impl<'a, 'b, A: AsyncExecutor> AsyncBencherExtension for AsyncBencher<'a, 'b, T: Fn(I) -> TF, TF: Future, { - let log_progress = !matches!( - std::env::var("TURBOPACK_BENCH_PROGRESS").ok().as_deref(), - None | Some("") | Some("no") | Some("false") - ); + let log_progress = read_env_bool("TURBOPACK_BENCH_PROGRESS"); let setup = &setup; let routine = &routine; diff --git a/crates/next-dev/benches/util/module_picker.rs b/crates/next-dev/benches/util/module_picker.rs new file mode 100644 index 0000000000000..d2b1e71979de0 --- /dev/null +++ b/crates/next-dev/benches/util/module_picker.rs @@ -0,0 +1,46 @@ +use std::{collections::HashMap, path::PathBuf}; + +use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; + +/// Picks modules at random, but with a fixed seed so runs are somewhat +/// reproducible. +/// +/// This must be initialized outside of `bench_with_input` so we don't repeat +/// the same sequence in different samples. +pub struct ModulePicker { + depths: Vec, + modules_by_depth: HashMap>, + rng: parking_lot::Mutex, +} + +impl ModulePicker { + /// Creates a new module picker. + pub fn new(mut modules: Vec<(PathBuf, usize)>) -> Self { + let rng = StdRng::seed_from_u64(42); + + // Ensure the module order is deterministic. + modules.sort(); + + let mut modules_by_depth: HashMap<_, Vec<_>> = HashMap::new(); + for (module, depth) in modules { + modules_by_depth.entry(depth).or_default().push(module); + } + let mut depths: Vec<_> = modules_by_depth.keys().copied().collect(); + // Ensure the depth order is deterministic. + depths.sort(); + + Self { + depths, + modules_by_depth, + rng: parking_lot::Mutex::new(rng), + } + } + + /// Picks a random module with a uniform distribution over all depths. + pub fn pick(&self) -> &PathBuf { + let mut rng = self.rng.lock(); + // Sample from all depths uniformly. + let depth = self.depths.choose(&mut *rng).unwrap(); + &self.modules_by_depth[depth].choose(&mut *rng).unwrap() + } +} diff --git a/crates/next-dev/benches/util/page_guard.rs b/crates/next-dev/benches/util/page_guard.rs index 5959fd8bf79af..5848d9085780e 100644 --- a/crates/next-dev/benches/util/page_guard.rs +++ b/crates/next-dev/benches/util/page_guard.rs @@ -44,24 +44,12 @@ impl<'a> PageGuard<'a> { } } - /// Returns a reference to the app. - pub fn app(&self) -> &PreparedApp<'a> { - // Invariant: app is always Some while the guard is alive. - self.app.as_ref().unwrap() - } - /// Returns a reference to the page. pub fn page(&self) -> &Page { // Invariant: page is always Some while the guard is alive. self.page.as_ref().unwrap() } - /// Returns a mutable reference to the app. - pub fn app_mut(&mut self) -> &mut PreparedApp<'a> { - // Invariant: app is always Some while the guard is alive. - self.app.as_mut().unwrap() - } - /// Closes the page, returns the app. pub async fn close_page(mut self) -> Result> { // Invariant: the page is always Some while the guard is alive. diff --git a/crates/next-dev/benches/util/prepared_app.rs b/crates/next-dev/benches/util/prepared_app.rs index 3d6b6d8df57fa..23a73edf82d9e 100644 --- a/crates/next-dev/benches/util/prepared_app.rs +++ b/crates/next-dev/benches/util/prepared_app.rs @@ -67,7 +67,6 @@ pub struct PreparedApp<'a> { bundler: &'a dyn Bundler, server: Option<(Child, String)>, test_dir: PreparedDir, - counter: usize, } impl<'a> PreparedApp<'a> { @@ -81,7 +80,6 @@ impl<'a> PreparedApp<'a> { bundler, server: None, test_dir: PreparedDir::TempDir(test_dir), - counter: 0, }) } @@ -93,15 +91,9 @@ impl<'a> PreparedApp<'a> { bundler, server: None, test_dir: PreparedDir::Path(template_dir), - counter: 0, }) } - pub fn counter(&mut self) -> usize { - self.counter += 1; - self.counter - } - pub fn start_server(&mut self) -> Result<()> { assert!(self.server.is_none(), "Server already started"); diff --git a/crates/turbopack-create-test-app/Cargo.toml b/crates/turbopack-create-test-app/Cargo.toml index aad8978b2ccda..8ce8dee06d896 100644 --- a/crates/turbopack-create-test-app/Cargo.toml +++ b/crates/turbopack-create-test-app/Cargo.toml @@ -19,5 +19,7 @@ bench = false [dependencies] anyhow = "1.0.47" clap = { version = "4.0.18", features = ["derive"] } +indoc = "1.0" +pathdiff = "0.2.1" serde_json = "1.0.85" tempfile = "3.3.0" diff --git a/crates/turbopack-create-test-app/src/test_app_builder.rs b/crates/turbopack-create-test-app/src/test_app_builder.rs index e7260e83df0c4..42a52dbff7a2e 100644 --- a/crates/turbopack-create-test-app/src/test_app_builder.rs +++ b/crates/turbopack-create-test-app/src/test_app_builder.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::{Context, Result}; +use indoc::{formatdoc, indoc}; use serde_json::json; use tempfile::TempDir; @@ -31,6 +32,13 @@ fn decide_early(remaining: usize, min_remaining_decisions: usize) -> bool { } } +fn write_file>(name: &str, path: P, content: &[u8]) -> Result<()> { + File::create(path) + .with_context(|| format!("creating {name}"))? + .write_all(content) + .with_context(|| format!("writing {name}")) +} + #[derive(Debug)] pub struct TestAppBuilder { pub target: Option, @@ -54,12 +62,19 @@ impl Default for TestAppBuilder { } } -fn write_file>(name: &str, path: P, content: &[u8]) -> Result<()> { - File::create(path) - .with_context(|| format!("creating {name}"))? - .write_all(content) - .with_context(|| format!("writing {name}")) -} +const SETUP_IMPORTS: &str = indoc! {r#" +import React from "react"; +"#}; +const SETUP_DETECTOR: &str = indoc! {r#" +let DETECTOR_PROPS = {}; +"#}; +const SETUP_EVAL: &str = indoc! {r#" +/* @turbopack-bench:eval-start */ +/* @turbopack-bench:eval-end */ +"#}; +const DETECTOR_ELEMENT: &str = indoc! {r#" + +"#}; impl TestAppBuilder { pub fn build(&self) -> Result { @@ -68,8 +83,8 @@ impl TestAppBuilder { } else { TestAppTarget::Temp(tempfile::tempdir().context("creating tempdir")?) }; - let app = TestApp { target }; - let path = app.path(); + let path = target.path(); + let mut modules = vec![]; let src = path.join("src"); create_dir_all(&src).context("creating src dir")?; @@ -78,29 +93,52 @@ impl TestAppBuilder { let mut remaining_dynamic_imports = self.dynamic_import_count; let mut queue = VecDeque::new(); - queue.push_back(src.join("triangle.jsx")); + queue.push_back((src.join("triangle.jsx"), 0)); remaining_modules -= 1; let mut is_root = true; - while let Some(file) = queue.pop_front() { + let detector_path = src.join("detector.jsx"); + + while let Some((file, depth)) = queue.pop_front() { + modules.push((file.clone(), depth)); + + let relative_detector = if detector_path.parent() == file.parent() { + "./detector.jsx".to_string() + } else { + pathdiff::diff_paths(&detector_path, file.parent().unwrap()) + .unwrap() + .display() + .to_string() + }; + let import_detector = formatdoc! {r#" + import Detector from "{relative_detector}"; + "#}; + let leaf = remaining_modules == 0 || (!queue.is_empty() && (queue.len() + remaining_modules) % (self.flatness + 1) == 0); if leaf { - File::create(file) - .context("creating file")? - .write_all( - r#"import React from "react"; - -function Triangle({ style }) { - return ; -} - -export default React.memo(Triangle); -"# - .as_bytes(), - ) - .context("writing file")?; + write_file( + &format!("leaf file {}", file.display()), + &file, + formatdoc! {r#" + {SETUP_IMPORTS} + {import_detector} + + {SETUP_DETECTOR} + {SETUP_EVAL} + + function Triangle({{ style }}) {{ + return <> + + {DETECTOR_ELEMENT} + ; + }} + + export default React.memo(Triangle); + "#} + .as_bytes(), + )?; } else { let in_subdirectory = decide(remaining_directories, remaining_modules / 3); @@ -127,7 +165,7 @@ export default React.memo(Triangle); f.file_name().unwrap().to_str().unwrap(), i )); - queue.push_back(f); + queue.push_back((f, depth + 1)); } remaining_modules = remaining_modules.saturating_sub(3); @@ -156,106 +194,111 @@ export default React.memo(Triangle); }) .collect::>() { - let (extra_imports, extra) = if is_root { + let setup_hydration = if is_root { is_root = false; - ( - "import Detector from \"./detector.jsx\";\n", - "\n ", - ) + "\nDETECTOR_PROPS.hydration = true;" } else { - ("", "") + "" }; - File::create(&file) - .with_context(|| format!("creating file with children {}", file.display()))? - .write_all( - format!( - r#"import React from "react"; -{a} -{b} -{c} -{extra_imports} -function Container({{ style }}) {{ - return <> - - {a_} - - - {b_} - - - {c_} - {extra} - ; -}} - -export default React.memo(Container); -"# - ) - .as_bytes(), - ) - .with_context(|| { - format!("writing file with children {}", file.display()) - })?; + write_file( + &format!("file with children {}", file.display()), + &file, + formatdoc! {r#" + {SETUP_IMPORTS} + {import_detector} + {a} + {b} + {c} + + {SETUP_DETECTOR}{setup_hydration} + {SETUP_EVAL} + + function Container({{ style }}) {{ + return <> + + {a_} + + + {b_} + + + {c_} + + {DETECTOR_ELEMENT} + ; + }} + + export default React.memo(Container); + "#} + .as_bytes(), + )?; } else { unreachable!() } } } - let bootstrap = r#"import React from "react"; -import { createRoot } from "react-dom/client"; -import Triangle from "./triangle.jsx"; + let bootstrap = indoc! {r#" + import React from "react"; + import { createRoot } from "react-dom/client"; + import Triangle from "./triangle.jsx"; -function App() { - return - - -} + function App() { + return + + + } -document.body.style.backgroundColor = "black"; -let root = document.createElement("main"); -document.body.appendChild(root); -createRoot(root).render(); -"#; - write_file("bootrap file", src.join("index.jsx"), bootstrap.as_bytes())?; + document.body.style.backgroundColor = "black"; + let root = document.createElement("main"); + document.body.appendChild(root); + createRoot(root).render(); + "#}; + write_file( + "bootstrap file", + src.join("index.jsx"), + bootstrap.as_bytes(), + )?; let pages = src.join("pages"); create_dir_all(&pages)?; // The page is e. g. used by Next.js - let bootstrap_page = r#"import React from "react"; -import Triangle from "../triangle.jsx"; - -export default function Page() { - return - - -} -"#; + let bootstrap_page = indoc! {r#" + import React from "react"; + import Triangle from "../triangle.jsx"; + + export default function Page() { + return + + + } + "#}; write_file( - "bootrap page", + "bootstrap page", pages.join("page.jsx"), bootstrap_page.as_bytes(), )?; // The page is e. g. used by Next.js - let bootstrap_static_page = r#"import React from "react"; -import Triangle from "../triangle.jsx"; - -export default function Page() { - return - - -} + let bootstrap_static_page = indoc! {r#" + import React from "react"; + import Triangle from "../triangle.jsx"; + + export default function Page() { + return + + + } -export function getStaticProps() { - return { - props: {} - }; -} -"#; + export function getStaticProps() { + return { + props: {} + }; + } + "#}; write_file( - "bootrap static page", + "bootstrap static page", pages.join("static.jsx"), bootstrap_static_page.as_bytes(), )?; @@ -265,109 +308,121 @@ export function getStaticProps() { create_dir_all(app_dir.join("client"))?; // The page is e. g. used by Next.js - let bootstrap_app_page = r#"import React from "react"; -import Triangle from "../../triangle.jsx"; - -export default function Page() { - return - - -} -"#; - File::create(app_dir.join("app/page.jsx")) - .context("creating bootstrap app page")? - .write_all(bootstrap_app_page.as_bytes()) - .context("writing bootstrap app page")?; + let bootstrap_app_page = indoc! {r#" + import React from "react"; + import Triangle from "../../triangle.jsx"; + + export default function Page() { + return + + + } + "#}; + write_file( + "bootstrap app page", + app_dir.join("app/page.jsx"), + bootstrap_app_page.as_bytes(), + )?; // The component is used to measure hydration and commit time for app/page.jsx - let detector_component = r#""use client"; - -import React from "react"; - -export default function Detector({ message }) { - React.useEffect(() => { - globalThis.__turbopackBenchBinding && globalThis.__turbopackBenchBinding("Hydration done"); - }); - React.useEffect(() => { - message && globalThis.__turbopackBenchBinding && globalThis.__turbopackBenchBinding(message); - }, [message]); - return null; -} -"#; - File::create(src.join("detector.jsx")) - .context("creating detector component")? - .write_all(detector_component.as_bytes()) - .context("writing detector component")?; + let detector_component = indoc! {r#" + "use client"; + + import React from "react"; + + export default function Detector({ message, hydration }) { + React.useEffect(() => { + if (hydration) { + globalThis.__turbopackBenchBinding && globalThis.__turbopackBenchBinding("Hydration done"); + } + if (message) { + globalThis.__turbopackBenchBinding && globalThis.__turbopackBenchBinding(message); + } + }, [message, hydration]); + return null; + } + "#}; + write_file( + "detector component", + src.join("detector.jsx"), + detector_component.as_bytes(), + )?; // The page is e. g. used by Next.js - let bootstrap_app_client_page = r#""use client"; -import React from "react"; -import Triangle from "../../triangle.jsx"; - -export default function Page() { - return - - -} -"#; - File::create(app_dir.join("client/page.jsx")) - .context("creating bootstrap app client page")? - .write_all(bootstrap_app_client_page.as_bytes()) - .context("writing bootstrap app client page")?; + let bootstrap_app_client_page = indoc! {r#" + "use client"; + import React from "react"; + import Triangle from "../../triangle.jsx"; + + export default function Page() { + return + + + } + "#}; + write_file( + "bootstrap app client page", + app_dir.join("client/page.jsx"), + bootstrap_app_client_page.as_bytes(), + )?; // This root layout is e. g. used by Next.js - let bootstrap_layout = r#"export default function RootLayout({ children }) { - return ( - - - - - Turbopack Test App - - - {children} - - - ); -} - "#; - File::create(app_dir.join("layout.jsx")) - .context("creating bootstrap html in root")? - .write_all(bootstrap_layout.as_bytes()) - .context("writing bootstrap html in root")?; + let bootstrap_layout = indoc! {r#" + export default function RootLayout({ children }) { + return ( + + + + + Turbopack Test App + + + {children} + + + ); + } + "#}; + write_file( + "bootstrap layout", + app_dir.join("layout.jsx"), + bootstrap_layout.as_bytes(), + )?; // This HTML is used e. g. by Vite - let bootstrap_html = r#" - - - - - Turbopack Test App - - - - - -"#; + let bootstrap_html = indoc! {r#" + + + + + + Turbopack Test App + + + + + + "#}; write_file( - "bootstrap html", + "bootstrap html in root", path.join("index.html"), bootstrap_html.as_bytes(), )?; // This HTML is used e. g. by webpack - let bootstrap_html2 = r#" - - - - - Turbopack Test App - - - - - -"#; + let bootstrap_html2 = indoc! {r#" + + + + + + Turbopack Test App + + + + + + "#}; let public = path.join("public"); create_dir_all(&public).context("creating public dir")?; @@ -405,13 +460,14 @@ export default function Page() { "react-dom": package_json.react_version.clone(), } }); - File::create(path.join("package.json")) - .context("creating package.json")? - .write_all(format!("{:#}", package_json).as_bytes()) - .context("writing package.json")?; + write_file( + "package.json", + path.join("package.json"), + format!("{:#}", package_json).as_bytes(), + )?; } - Ok(app) + Ok(TestApp { target, modules }) } } @@ -436,17 +492,30 @@ enum TestAppTarget { Temp(TempDir), } +impl TestAppTarget { + /// Returns the path to the directory containing the app. + fn path(&self) -> &Path { + match &self { + TestAppTarget::Set(target) => target.as_path(), + TestAppTarget::Temp(target) => target.path(), + } + } +} + #[derive(Debug)] pub struct TestApp { target: TestAppTarget, + modules: Vec<(PathBuf, usize)>, } impl TestApp { /// Returns the path to the directory containing the app. pub fn path(&self) -> &Path { - match &self.target { - TestAppTarget::Set(target) => target.as_path(), - TestAppTarget::Temp(target) => target.path(), - } + self.target.path() + } + + /// Returns the list of modules and their depth in this app. + pub fn modules(&self) -> &[(PathBuf, usize)] { + &self.modules } }