turbopack_bench/util/
mod.rs

1use std::{
2    io::{
3        BufRead, BufReader, Read, Write, {self},
4    },
5    panic::UnwindSafe,
6    process::Command,
7    time::{Duration, Instant},
8};
9
10use anyhow::Result;
11use chromiumoxide::{
12    browser::{Browser, BrowserConfig},
13    error::CdpError::Ws,
14};
15use criterion::{AsyncBencher, async_executor::AsyncExecutor, black_box, measurement::WallTime};
16use futures::{Future, StreamExt};
17pub use page_guard::PageGuard;
18use parking_lot::Mutex;
19pub use prepared_app::PreparedApp;
20use regex::Regex;
21use tungstenite::{Error::Protocol, error::ProtocolError::ResetWithoutClosingHandshake};
22use turbo_tasks::util::FormatDuration;
23use turbo_tasks_testing::retry::{retry, retry_async};
24use turbopack_create_test_app::test_app_builder::{
25    EffectMode, PackageJsonConfig, TestApp, TestAppBuilder,
26};
27
28use self::env::read_env_bool;
29use crate::bundlers::{Bundler, RenderType};
30
31pub mod env;
32pub mod module_picker;
33pub mod npm;
34mod page_guard;
35mod prepared_app;
36
37pub const BINDING_NAME: &str = "__turbopackBenchBinding";
38
39fn retry_default<A, F, R, E>(args: A, f: F) -> Result<R, E>
40where
41    F: Fn(&mut A) -> Result<R, E>,
42{
43    // waits 5, 10, 20, 40 seconds = 75 seconds total
44    retry(args, f, 3, Duration::from_secs(5))
45}
46
47async fn retry_async_default<A, F, Fut, R, E>(args: A, f: F) -> Result<R, E>
48where
49    F: Fn(&mut A) -> Fut,
50    Fut: Future<Output = Result<R, E>>,
51{
52    // waits 5, 10, 20, 40 seconds = 75 seconds total
53    retry_async(args, f, 3, Duration::from_secs(5)).await
54}
55
56pub fn build_test(module_count: usize, bundler: &dyn Bundler) -> TestApp {
57    let test_app = TestAppBuilder {
58        module_count,
59        directories_count: module_count / 20,
60        package_json: Some(PackageJsonConfig {
61            react_version: bundler.react_version().to_string(),
62        }),
63        effect_mode: match bundler.render_type() {
64            RenderType::ServerSideRenderedWithEvents => EffectMode::Component,
65            _ => EffectMode::Hook,
66        },
67        ..Default::default()
68    }
69    .build()
70    .unwrap();
71
72    let npm = command("npm")
73        .args(["install", "--loglevel=error"])
74        .current_dir(test_app.path())
75        .output()
76        .unwrap();
77
78    if !npm.status.success() {
79        io::stdout().write_all(&npm.stdout).unwrap();
80        io::stderr().write_all(&npm.stderr).unwrap();
81        panic!("npm install failed. See above.");
82    }
83
84    retry_default((), |_| bundler.prepare(test_app.path())).unwrap();
85
86    test_app
87}
88
89pub async fn create_browser() -> Browser {
90    let with_head = read_env_bool("TURBOPACK_BENCH_WITH_HEAD");
91    let with_devtools = read_env_bool("TURBOPACK_BENCH_DEVTOOLS");
92    let mut builder = BrowserConfig::builder();
93    builder = builder.no_sandbox();
94    if with_head {
95        builder = builder.with_head();
96    }
97    if with_devtools {
98        builder = builder.arg("--auto-open-devtools-for-tabs");
99    }
100    let (browser, mut handler) = retry_async(
101        builder.build().unwrap(),
102        |c| {
103            let c = c.clone();
104            Browser::launch(c)
105        },
106        3,
107        Duration::from_millis(100),
108    )
109    .await
110    .expect("Launching the browser failed");
111
112    // See https://crates.io/crates/chromiumoxide
113    tokio::task::spawn(async move {
114        loop {
115            if let Err(Ws(Protocol(ResetWithoutClosingHandshake))) = handler.next().await.unwrap() {
116                break;
117            }
118        }
119    });
120
121    browser
122}
123
124pub fn resume_on_error<F: FnOnce() + UnwindSafe>(f: F) {
125    let runs_as_bench = std::env::args().find(|a| a == "--bench");
126    let ignore_errors = read_env_bool("TURBOPACK_BENCH_IGNORE_ERRORS");
127
128    if runs_as_bench.is_some() || ignore_errors {
129        use std::panic::catch_unwind;
130        // panics are already printed to the console, so no need to handle the result.
131        let _ = catch_unwind(f);
132    } else {
133        f();
134    }
135}
136
137pub trait AsyncBencherExtension<A: AsyncExecutor> {
138    fn try_iter_custom<R, F>(&mut self, routine: R)
139    where
140        R: Fn(u64, WallTime) -> F,
141        F: Future<Output = Result<Duration>>;
142
143    fn try_iter_async<I, S, SF, R, F, T, TF>(
144        &mut self,
145        runner: A,
146        setup: S,
147        routine: R,
148        teardown: T,
149    ) where
150        S: Fn() -> SF,
151        SF: Future<Output = Result<I>>,
152        R: Fn(I, u64, WallTime, bool) -> F,
153        F: Future<Output = Result<(I, Duration)>>,
154        T: Fn(I) -> TF,
155        TF: Future<Output = ()>;
156}
157
158impl<A: AsyncExecutor> AsyncBencherExtension<A> for AsyncBencher<'_, '_, A, WallTime> {
159    fn try_iter_custom<R, F>(&mut self, routine: R)
160    where
161        R: Fn(u64, WallTime) -> F,
162        F: Future<Output = Result<Duration>>,
163    {
164        let log_progress = read_env_bool("TURBOPACK_BENCH_PROGRESS");
165
166        let routine = &routine;
167        self.iter_custom(|iters| async move {
168            let measurement = WallTime;
169            let value = routine(iters, measurement).await.expect("routine failed");
170            if log_progress {
171                eprint!(" {:?}/{}", FormatDuration(value / (iters as u32)), iters);
172            }
173            value
174        });
175    }
176
177    fn try_iter_async<I, S, SF, R, F, T, TF>(
178        &mut self,
179        runner: A,
180        setup: S,
181        routine: R,
182        teardown: T,
183    ) where
184        S: Fn() -> SF,
185        SF: Future<Output = Result<I>>,
186        R: Fn(I, u64, WallTime, bool) -> F,
187        F: Future<Output = Result<(I, Duration)>>,
188        T: Fn(I) -> TF,
189        TF: Future<Output = ()>,
190    {
191        let log_progress = read_env_bool("TURBOPACK_BENCH_PROGRESS");
192
193        let setup = &setup;
194        let routine = &routine;
195        let teardown = &teardown;
196        let input_mutex = &Mutex::new(Some(black_box(runner.block_on(async {
197            if log_progress {
198                eprint!(" setup...");
199            }
200            let start = Instant::now();
201            let input = retry_async_default((), |_| setup())
202                .await
203                .expect("failed to setup");
204            if log_progress {
205                let duration = start.elapsed();
206                eprint!(" [{:?}]", FormatDuration(duration));
207            }
208            input
209        }))));
210
211        self.iter_custom(|iters| async move {
212            let measurement = WallTime;
213
214            let input = input_mutex
215                .lock()
216                .take()
217                .expect("iter_custom only executes its closure once");
218
219            let (output, value) = routine(input, iters, measurement, log_progress)
220                .await
221                .expect("Routine failed");
222            let output = black_box(output);
223
224            if log_progress {
225                eprint!(" {:?}/{}", FormatDuration(value / (iters as u32)), iters);
226            }
227
228            input_mutex.lock().replace(output);
229
230            value
231        });
232
233        let input = input_mutex.lock().take().unwrap();
234        if log_progress {
235            eprint!(" teardown...");
236        }
237        let start = Instant::now();
238        runner.block_on(teardown(input));
239        let duration = start.elapsed();
240        if log_progress {
241            eprintln!(" [{:?}]", FormatDuration(duration));
242        }
243    }
244}
245
246pub fn command(bin: &str) -> Command {
247    if cfg!(windows) {
248        let mut command = Command::new("cmd.exe");
249        command.args(["/C", bin]);
250        command
251    } else {
252        Command::new(bin)
253    }
254}
255
256pub fn wait_for_match<R>(readable: R, re: Regex) -> Option<String>
257where
258    R: Read,
259{
260    // See https://docs.rs/async-process/latest/async_process/#examples
261    let mut line_reader = BufReader::new(readable).lines();
262    // Read until the match appears in the buffer
263    let mut matched: Option<String> = None;
264    while let Some(Ok(line)) = line_reader.next() {
265        if let Some(cap) = re.captures(&line) {
266            matched = Some(cap.get(1).unwrap().as_str().into());
267            break;
268        }
269    }
270
271    matched
272}