1use std::{
2 fs::{self},
3 panic::AssertUnwindSafe,
4 path::Path,
5 sync::{
6 Arc, LazyLock,
7 atomic::{AtomicUsize, Ordering},
8 },
9 time::Duration,
10};
11
12use anyhow::{Context, Result, anyhow};
13use criterion::{
14 BenchmarkGroup, BenchmarkId, Criterion,
15 measurement::{Measurement, WallTime},
16};
17use tokio::{
18 runtime::Runtime,
19 time::{sleep, timeout},
20};
21use turbo_tasks::util::FormatDuration;
22use util::{
23 AsyncBencherExtension, BINDING_NAME, PreparedApp, build_test, create_browser,
24 env::{read_env, read_env_bool, read_env_list},
25 module_picker::ModulePicker,
26};
27
28use self::{bundlers::RenderType, util::resume_on_error};
29use crate::{bundlers::Bundler, util::PageGuard};
30
31pub mod bundlers;
32pub mod util;
33
34pub fn bench_startup(c: &mut Criterion, bundlers: &[Box<dyn Bundler>]) {
35 let mut g = c.benchmark_group("bench_startup");
36 g.sample_size(10);
37 g.measurement_time(Duration::from_secs(60));
38
39 bench_startup_internal(g, false, bundlers);
40}
41
42pub fn bench_hydration(c: &mut Criterion, bundlers: &[Box<dyn Bundler>]) {
43 let mut g = c.benchmark_group("bench_hydration");
44 g.sample_size(10);
45 g.measurement_time(Duration::from_secs(60));
46
47 bench_startup_internal(g, true, bundlers);
48}
49
50fn bench_startup_internal(
51 mut g: BenchmarkGroup<WallTime>,
52 hydration: bool,
53 bundlers: &[Box<dyn Bundler>],
54) {
55 let runtime = Runtime::new().unwrap();
56 let browser = LazyLock::new(|| runtime.block_on(create_browser()));
57
58 for bundler in bundlers {
59 let wait_for_hydration = match bundler.render_type() {
60 RenderType::ClientSideRendered => {
61 if hydration {
65 continue;
66 } else {
67 true
68 }
69 }
70 RenderType::ServerSidePrerendered => hydration,
71 RenderType::ServerSideRenderedWithEvents => hydration,
72 RenderType::ServerSideRenderedWithoutInteractivity => {
73 if hydration {
75 continue;
76 } else {
77 false
78 }
79 }
80 };
81 for module_count in get_module_counts() {
82 let test_app = LazyLock::new(|| build_test(module_count, bundler.as_ref()));
83 let input = (bundler.as_ref(), &test_app);
84 resume_on_error(AssertUnwindSafe(|| {
85 g.bench_with_input(
86 BenchmarkId::new(bundler.get_name(), format!("{module_count} modules")),
87 &input,
88 |b, &(bundler, test_app)| {
89 let test_app = &**test_app;
90 let browser = &*browser;
91 b.to_async(&runtime).try_iter_custom(|iters, m| async move {
92 let mut value = m.zero();
93
94 for _ in 0..iters {
95 let mut app =
96 PreparedApp::new(bundler, test_app.path().to_path_buf())
97 .await?;
98 let start = m.start();
99 app.start_server()?;
100 let mut guard = app.with_page(browser).await?;
101 if wait_for_hydration {
102 guard.wait_for_hydration().await?;
103 }
104 let duration = m.end(start);
105 value = m.add(&value, &duration);
106
107 drop(guard);
109 }
110 Ok(value)
111 });
112 },
113 );
114 }));
115 }
116 }
117 g.finish();
118}
119
120#[derive(Copy, Clone)]
121enum CodeLocation {
122 Effect,
123 Evaluation,
124}
125
126pub fn bench_hmr_to_eval(c: &mut Criterion, bundlers: &[Box<dyn Bundler>]) {
127 let mut g = c.benchmark_group("bench_hmr_to_eval");
128 g.sample_size(10);
129 g.measurement_time(Duration::from_secs(60));
130
131 bench_hmr_internal(g, CodeLocation::Evaluation, bundlers);
132}
133
134pub fn bench_hmr_to_commit(c: &mut Criterion, bundlers: &[Box<dyn Bundler>]) {
135 let mut g = c.benchmark_group("bench_hmr_to_commit");
136 g.sample_size(10);
137 g.measurement_time(Duration::from_secs(60));
138
139 bench_hmr_internal(g, CodeLocation::Effect, bundlers);
140}
141
142fn bench_hmr_internal(
143 mut g: BenchmarkGroup<WallTime>,
144 location: CodeLocation,
145 bundlers: &[Box<dyn Bundler>],
146) {
147 g.warm_up_time(Duration::from_millis(1));
149
150 let runtime = Runtime::new().unwrap();
151 let browser = LazyLock::new(|| runtime.block_on(create_browser()));
152 let hmr_warmup = read_env("TURBOPACK_BENCH_HMR_WARMUP", 10).unwrap();
153
154 for bundler in bundlers {
155 if matches!(
156 bundler.render_type(),
157 RenderType::ServerSideRenderedWithEvents
158 | RenderType::ServerSideRenderedWithoutInteractivity
159 ) && matches!(location, CodeLocation::Evaluation)
160 {
161 continue;
164 }
165 for module_count in get_module_counts() {
166 let test_app = LazyLock::new(|| build_test(module_count, bundler.as_ref()));
167 let input = (bundler.as_ref(), &test_app);
168 let module_picker =
169 LazyLock::new(|| Arc::new(ModulePicker::new(test_app.modules().to_vec())));
170
171 resume_on_error(AssertUnwindSafe(|| {
172 g.bench_with_input(
173 BenchmarkId::new(bundler.get_name(), format!("{module_count} modules")),
174 &input,
175 |b, &(bundler, test_app)| {
176 let test_app = &**test_app;
177 let modules = test_app.modules();
178 let module_picker = &*module_picker;
179 let browser = &*browser;
180
181 let max_init_update_timeout = bundler.max_init_update_timeout(module_count);
182 let max_update_timeout = bundler.max_update_timeout(module_count);
183
184 b.to_async(&runtime).try_iter_async(
185 &runtime,
186 || async {
187 let mut app = PreparedApp::new_without_copy(
188 bundler,
189 test_app.path().to_path_buf(),
190 )
191 .await?;
192 app.start_server()?;
193 let mut guard = app.with_page(browser).await?;
194 if bundler.has_hydration_event() {
195 guard.wait_for_hydration().await?;
196 } else {
197 guard.page().wait_for_navigation().await?;
198 }
199 guard
200 .page()
201 .evaluate_expression("globalThis.HMR_IS_HAPPENING = true")
202 .await
203 .context(
204 "Unable to evaluate JavaScript in the page for HMR check \
205 flag",
206 )?;
207
208 let mut exponential_duration = Duration::from_millis(100);
212 loop {
213 match make_change(
214 &modules[0].0,
215 bundler,
216 &mut guard,
217 location,
218 exponential_duration,
219 &WallTime,
220 )
221 .await
222 {
223 Ok(_) => {
224 break;
225 }
226 Err(e) => {
227 exponential_duration *= 2;
228 if exponential_duration > max_init_update_timeout {
229 return Err(
230 e.context("failed to make warmup change")
231 );
232 }
233 }
234 }
235 }
236
237 let mut hmr_warmup_iter = 0;
240 let mut hmr_warmup_dropped = 0;
241 while hmr_warmup_iter < hmr_warmup {
242 match make_change(
243 &modules[0].0,
244 bundler,
245 &mut guard,
246 location,
247 max_update_timeout,
248 &WallTime,
249 )
250 .await
251 {
252 Err(_) => {
253 hmr_warmup_dropped += 1;
255
256 if hmr_warmup_dropped >= hmr_warmup {
257 anyhow::bail!(
258 "failed to make warmup change {} times",
259 hmr_warmup_dropped
260 )
261 }
262 }
263 Ok(_) => {
264 hmr_warmup_iter += 1;
265 }
266 }
267 }
268
269 Ok(guard)
270 },
271 |mut guard, iters, m, verbose| {
272 let module_picker = Arc::clone(module_picker);
273 async move {
274 let mut value = m.zero();
275 let mut dropped = 0;
276 let mut iter = 0;
277 while iter < iters {
278 let module = module_picker.pick();
279 let duration = match make_change(
280 module,
281 bundler,
282 &mut guard,
283 location,
284 max_update_timeout,
285 &m,
286 )
287 .await
288 {
289 Err(_) => {
290 dropped += 1;
296 continue;
297 }
298 Ok(duration) => duration,
299 };
300 value = m.add(&value, &duration);
301
302 iter += 1;
303 if verbose && iter != iters && iter.is_power_of_two() {
304 eprint!(
305 " [{:?} {:?}/{}{}]",
306 duration,
307 FormatDuration(value / (iter as u32)),
308 iter,
309 if dropped > 0 {
310 format!(" ({dropped} dropped)")
311 } else {
312 "".to_string()
313 }
314 );
315 }
316 }
317
318 Ok((guard, value))
319 }
320 },
321 |guard| async move {
322 let hmr_is_happening = guard
323 .page()
324 .evaluate_expression("globalThis.HMR_IS_HAPPENING")
325 .await
326 .unwrap();
327 assert!(hmr_is_happening.value().unwrap().as_bool().unwrap());
330 },
331 );
332 },
333 );
334 }));
335 }
336 }
337}
338
339fn insert_code(
340 path: &Path,
341 bundler: &dyn Bundler,
342 message: &str,
343 location: CodeLocation,
344) -> Result<impl FnOnce() -> Result<()>> {
345 let mut contents = fs::read_to_string(path)?;
346
347 const PRAGMA_EVAL_START: &str = "/* @turbopack-bench:eval-start */";
348 const PRAGMA_EVAL_END: &str = "/* @turbopack-bench:eval-end */";
349
350 let eval_start = contents
351 .find(PRAGMA_EVAL_START)
352 .ok_or_else(|| anyhow!("unable to find effect start pragma in {}", contents))?;
353 let eval_end = contents
354 .find(PRAGMA_EVAL_END)
355 .ok_or_else(|| anyhow!("unable to find effect end pragma in {}", contents))?;
356
357 match (location, bundler.render_type()) {
358 (CodeLocation::Effect, _) => {
359 contents.replace_range(
360 eval_start + PRAGMA_EVAL_START.len()..eval_end,
361 &format!("\nEFFECT_PROPS.message = \"{message}\";\n"),
362 );
363 }
364 (
365 CodeLocation::Evaluation,
366 RenderType::ClientSideRendered | RenderType::ServerSidePrerendered,
367 ) => {
368 let code = format!(
369 "\nglobalThis.{BINDING_NAME} && globalThis.{BINDING_NAME}(\"{message}\");\n"
370 );
371 contents.replace_range(eval_start + PRAGMA_EVAL_START.len()..eval_end, &code);
372 }
373 (
374 CodeLocation::Evaluation,
375 RenderType::ServerSideRenderedWithEvents
376 | RenderType::ServerSideRenderedWithoutInteractivity,
377 ) => {
378 panic!("evaluation can't be measured for bundlers which evaluate on server side");
379 }
380 }
381
382 let path = path.to_owned();
383 Ok(move || Ok(fs::write(&path, contents)?))
384}
385
386static CHANGE_TIMEOUT_MESSAGE: &str = "update was not registered by bundler";
387
388async fn make_change(
389 module: &Path,
390 bundler: &dyn Bundler,
391 guard: &mut PageGuard<'_>,
392 location: CodeLocation,
393 timeout_duration: Duration,
394 measurement: &WallTime,
395) -> Result<Duration> {
396 static CHANGE_COUNTER: AtomicUsize = AtomicUsize::new(0);
397
398 let msg = format!(
399 "TURBOPACK_BENCH_CHANGE_{}",
400 CHANGE_COUNTER.fetch_add(1, Ordering::Relaxed)
401 );
402
403 let commit = insert_code(module, bundler, &msg, location)?;
405
406 let start = measurement.start();
407
408 commit()?;
409
410 timeout(timeout_duration, guard.wait_for_binding(&msg))
413 .await
414 .context(CHANGE_TIMEOUT_MESSAGE)??;
415
416 let duration = measurement.end(start);
417
418 if cfg!(target_os = "linux") {
419 tokio::time::sleep(std::cmp::max(duration, Duration::from_millis(100))).await;
421 }
422 Ok(duration)
423}
424
425pub fn bench_startup_cached(c: &mut Criterion, bundlers: &[Box<dyn Bundler>]) {
426 let mut g = c.benchmark_group("bench_startup_cached");
427 g.sample_size(10);
428 g.measurement_time(Duration::from_secs(60));
429
430 bench_startup_cached_internal(g, false, bundlers);
431}
432
433pub fn bench_hydration_cached(c: &mut Criterion, bundlers: &[Box<dyn Bundler>]) {
434 let mut g = c.benchmark_group("bench_hydration_cached");
435 g.sample_size(10);
436 g.measurement_time(Duration::from_secs(60));
437
438 bench_startup_cached_internal(g, true, bundlers);
439}
440
441fn bench_startup_cached_internal(
442 mut g: BenchmarkGroup<WallTime>,
443 hydration: bool,
444 bundlers: &[Box<dyn Bundler>],
445) {
446 if !read_env_bool("TURBOPACK_BENCH_CACHED") {
447 return;
448 }
449
450 let runtime = Runtime::new().unwrap();
451 let browser = LazyLock::new(|| runtime.block_on(create_browser()));
452
453 for bundler in bundlers {
454 let wait_for_hydration = match bundler.render_type() {
455 RenderType::ClientSideRendered => {
456 if hydration {
460 continue;
461 } else {
462 true
463 }
464 }
465 RenderType::ServerSidePrerendered => hydration,
466 RenderType::ServerSideRenderedWithEvents => hydration,
467 RenderType::ServerSideRenderedWithoutInteractivity => {
468 if hydration {
470 continue;
471 } else {
472 false
473 }
474 }
475 };
476 for module_count in get_module_counts() {
477 let test_app = LazyLock::new(|| build_test(module_count, bundler.as_ref()));
478 let input = (bundler.as_ref(), &test_app);
479
480 resume_on_error(AssertUnwindSafe(|| {
481 g.bench_with_input(
482 BenchmarkId::new(bundler.get_name(), format!("{module_count} modules")),
483 &input,
484 |b, &(bundler, test_app)| {
485 let test_app = &**test_app;
486 let browser = &*browser;
487 b.to_async(&runtime).try_iter_custom(|iters, m| async move {
488 let mut app =
490 PreparedApp::new(bundler, test_app.path().to_path_buf()).await?;
491 app.start_server()?;
492 let mut guard = app.with_page(browser).await?;
493 if bundler.has_hydration_event() {
494 guard.wait_for_hydration().await?;
495 } else {
496 guard.page().wait_for_navigation().await?;
497 }
498
499 let mut app = guard.close_page().await?;
500
501 sleep(Duration::from_secs(4)).await;
503
504 app.stop_server()?;
505
506 let mut value = m.zero();
507 for _ in 0..iters {
508 let start = m.start();
509 app.start_server()?;
510 let mut guard = app.with_page(browser).await?;
511 if wait_for_hydration {
512 guard.wait_for_hydration().await?;
513 }
514 let duration = m.end(start);
515 value = m.add(&value, &duration);
516
517 app = guard.close_page().await?;
518 app.stop_server()?;
519 }
520
521 drop(app);
522 Ok(value)
523 });
524 },
525 );
526 }));
527 }
528 }
529}
530
531fn get_module_counts() -> Vec<usize> {
532 read_env_list("TURBOPACK_BENCH_COUNTS", vec![1_000usize]).unwrap()
533}