turbopack_bench/
lib.rs

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                // For bundlers without server rendered html "startup" means time to hydration
63                // as they only render an empty screen without hydration. Since startup and
64                // hydration would be the same we skip the hydration benchmark for them.
65                if hydration {
66                    continue;
67                } else {
68                    true
69                }
70            }
71            RenderType::ServerSidePrerendered => hydration,
72            RenderType::ServerSideRenderedWithEvents => hydration,
73            RenderType::ServerSideRenderedWithoutInteractivity => {
74                // For bundlers without interactivity there is no hydration event to wait for
75                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                                // Defer the dropping of the guard.
109                                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    // Only capture one sample for warmup
149    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            // We can't measure evaluation time for these bundlers since it's not evaluated
163            // in the browser
164            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                                // There's a possible race condition between hydration and
210                                // connection to the HMR server. We attempt to make updates with an
211                                // exponential backoff until one succeeds.
212                                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                                // Once we know the HMR server is connected, we make a few warmup
239                                // changes.
240                                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                                            // We don't care about dropped updates during warmup.
255                                            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                                                // Some bundlers (e.g. Turbopack and Vite) can drop
292                                                // updates under certain conditions. We don't want
293                                                // to crash or stop the benchmark
294                                                // because of this. Instead, we keep going and
295                                                // report the number of dropped updates at the end.
296                                                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                                // Make sure that we are really measuring HMR and not accidentically
329                                // full refreshing the page
330                                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    // Keep the IO out of the measurement.
405    let commit = insert_code(module, bundler, &msg, location)?;
406
407    let start = measurement.start();
408
409    commit()?;
410
411    // Wait for the change introduced above to be reflected at runtime.
412    // This expects HMR or automatic reloading to occur.
413    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        // TODO(sokra) triggering HMR updates too fast can have weird effects on Linux
421        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                // For bundlers without server rendered html "startup" means time to hydration
458                // as they only render an empty screen without hydration. Since startup and
459                // hydration would be the same we skip the hydration benchmark for them.
460                if hydration {
461                    continue;
462                } else {
463                    true
464                }
465            }
466            RenderType::ServerSidePrerendered => hydration,
467            RenderType::ServerSideRenderedWithEvents => hydration,
468            RenderType::ServerSideRenderedWithoutInteractivity => {
469                // For bundlers without interactivity there is no hydration event to wait for
470                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                            // Run a complete build, shut down, and test running it again
490                            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                            // Give it 4 seconds time to store the cache
503                            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}