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 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 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 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 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 let mut line_reader = BufReader::new(readable).lines();
262 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}