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