Skip to main content

turbopack_bench/
lib.rs

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