turbopack_bench/util/
prepared_app.rs

1use std::{
2    future::Future,
3    path::{Path, PathBuf},
4    process::Child,
5    sync::Arc,
6};
7
8use anyhow::{Context, Result, anyhow};
9use chromiumoxide::{
10    Browser, Page,
11    cdp::{
12        browser_protocol::network::EventResponseReceived,
13        js_protocol::runtime::{AddBindingParams, EventBindingCalled, EventExceptionThrown},
14    },
15};
16use futures::{FutureExt, StreamExt};
17use tokio::{sync::Semaphore, task::spawn_blocking};
18use url::Url;
19
20use crate::{BINDING_NAME, bundlers::Bundler, util::PageGuard};
21
22async fn copy_dir(from: PathBuf, to: PathBuf) -> anyhow::Result<()> {
23    copy_dir_inner(from, to, Arc::new(Semaphore::new(64))).await
24}
25
26// HACK: Needed so that `copy_dir`'s `Future` can be inferred as `Send`:
27// https://github.com/rust-lang/rust/issues/123072
28fn copy_dir_inner_send(
29    from: PathBuf,
30    to: PathBuf,
31    semaphore: Arc<Semaphore>,
32) -> impl Future<Output = anyhow::Result<()>> + Send {
33    copy_dir_inner(from, to, semaphore)
34}
35
36async fn copy_dir_inner(
37    from: PathBuf,
38    to: PathBuf,
39    semaphore: Arc<Semaphore>,
40) -> anyhow::Result<()> {
41    let mut jobs = Vec::new();
42    {
43        let _permit = semaphore
44            .acquire()
45            .await
46            .expect("semaphore is never closed");
47        let mut dir = spawn_blocking(|| std::fs::read_dir(from)).await??;
48        for entry in &mut dir {
49            let entry = entry?;
50            let ty = entry.file_type()?;
51            let to = to.join(entry.file_name());
52            if ty.is_dir() {
53                let semaphore = semaphore.clone();
54                jobs.push(tokio::spawn(async move {
55                    tokio::fs::create_dir(&to).await?;
56                    copy_dir_inner_send(entry.path(), to, semaphore).await
57                }));
58            } else if ty.is_file() {
59                let semaphore = semaphore.clone();
60                jobs.push(tokio::spawn(async move {
61                    let _permit = semaphore
62                        .acquire()
63                        .await
64                        .expect("semaphore is never closed");
65                    tokio::fs::copy(entry.path(), to).await?;
66                    Ok::<_, anyhow::Error>(())
67                }));
68            }
69        }
70    }
71
72    for job in jobs {
73        job.await??;
74    }
75
76    Ok(())
77}
78
79enum PreparedDir {
80    TempDir(tempfile::TempDir),
81    Path(PathBuf),
82}
83
84pub struct PreparedApp<'a> {
85    bundler: &'a dyn Bundler,
86    server: Option<(Child, String)>,
87    test_dir: PreparedDir,
88}
89
90impl<'a> PreparedApp<'a> {
91    pub async fn new(bundler: &'a dyn Bundler, template_dir: PathBuf) -> Result<PreparedApp<'a>> {
92        let test_dir = tempfile::tempdir()?;
93
94        tokio::fs::create_dir_all(&test_dir).await?;
95        copy_dir(template_dir, test_dir.path().to_path_buf()).await?;
96
97        Ok(Self {
98            bundler,
99            server: None,
100            test_dir: PreparedDir::TempDir(test_dir),
101        })
102    }
103
104    pub async fn new_without_copy(
105        bundler: &'a dyn Bundler,
106        template_dir: PathBuf,
107    ) -> Result<PreparedApp<'a>> {
108        Ok(Self {
109            bundler,
110            server: None,
111            test_dir: PreparedDir::Path(template_dir),
112        })
113    }
114
115    pub fn start_server(&mut self) -> Result<()> {
116        assert!(self.server.is_none(), "Server already started");
117
118        self.server = Some(self.bundler.start_server(self.path())?);
119
120        Ok(())
121    }
122
123    pub async fn with_page(self, browser: &Browser) -> Result<PageGuard<'a>> {
124        let server = self.server.as_ref().context("Server must be started")?;
125        let page = browser
126            .new_page("about:blank")
127            .await
128            .context("Unable to open about:blank")?;
129        // Bindings survive page reloads. Set them up as early as possible.
130        add_binding(&page)
131            .await
132            .context("Failed to add bindings to the browser tab")?;
133
134        let mut errors = page
135            .event_listener::<EventExceptionThrown>()
136            .await
137            .context("Unable to listen to exception events")?;
138        let binding_events = page
139            .event_listener::<EventBindingCalled>()
140            .await
141            .context("Unable to listen to binding events")?;
142        let mut network_response_events = page
143            .event_listener::<EventResponseReceived>()
144            .await
145            .context("Unable to listen to response received events")?;
146
147        let destination = Url::parse(&server.1)?.join(self.bundler.get_path())?;
148        // We can't use page.goto() here since this will wait for the naviation to be
149        // completed. A naviation would be complete when all sync script are
150        // evaluated, but the page actually can rendered earlier without JavaScript
151        // needing to be evaluated.
152        // So instead we navigate via JavaScript and wait only for the HTML response to
153        // be completed.
154        page.evaluate_expression(format!("window.location='{destination}'"))
155            .await
156            .context("Unable to evaluate javascript to navigate to target page")?;
157
158        // Wait for HTML response completed
159        loop {
160            match network_response_events.next().await {
161                Some(event) => {
162                    if event.response.url == destination.as_str() {
163                        break;
164                    }
165                }
166                None => return Err(anyhow!("event stream ended too early")),
167            }
168        }
169
170        // Make sure no runtime errors occurred when loading the page
171        assert!(errors.next().now_or_never().is_none());
172
173        let page_guard = PageGuard::new(page, binding_events, errors, self);
174
175        Ok(page_guard)
176    }
177
178    pub fn stop_server(&mut self) -> Result<()> {
179        let mut proc = self.server.take().expect("Server never started").0;
180        stop_process(&mut proc)?;
181        Ok(())
182    }
183
184    pub fn path(&self) -> &Path {
185        match self.test_dir {
186            PreparedDir::TempDir(ref dir) => dir.path(),
187            PreparedDir::Path(ref path) => path,
188        }
189    }
190}
191
192impl Drop for PreparedApp<'_> {
193    fn drop(&mut self) {
194        if let Some(mut server) = self.server.take() {
195            stop_process(&mut server.0).expect("failed to stop process");
196        }
197    }
198}
199
200/// Adds benchmark-specific bindings to the page.
201async fn add_binding(page: &Page) -> Result<()> {
202    page.execute(AddBindingParams::new(BINDING_NAME)).await?;
203    Ok(())
204}
205
206#[cfg(unix)]
207fn stop_process(proc: &mut Child) -> Result<()> {
208    use std::time::Duration;
209
210    use nix::{
211        sys::signal::{Signal, kill},
212        unistd::Pid,
213    };
214    use owo_colors::OwoColorize;
215
216    const KILL_DEADLINE: Duration = Duration::from_secs(5);
217    const KILL_DEADLINE_CHECK_STEPS: u32 = 10;
218
219    let pid = Pid::from_raw(proc.id() as _);
220    match kill(pid, Signal::SIGINT) {
221        Ok(()) => {
222            let expire = std::time::Instant::now() + KILL_DEADLINE;
223            while let Ok(None) = proc.try_wait() {
224                if std::time::Instant::now() > expire {
225                    break;
226                }
227                std::thread::sleep(KILL_DEADLINE / KILL_DEADLINE_CHECK_STEPS);
228            }
229            if let Ok(None) = proc.try_wait() {
230                eprintln!(
231                    "{event_type} - process {pid} did not exit after SIGINT, sending SIGKILL",
232                    event_type = "error".red(),
233                    pid = pid
234                );
235                kill_process(proc)?;
236            }
237        }
238        Err(_) => {
239            eprintln!(
240                "{event_type} - failed to send SIGINT to process {pid}, sending SIGKILL",
241                event_type = "error".red(),
242                pid = pid
243            );
244            kill_process(proc)?;
245        }
246    }
247    Ok(())
248}
249
250#[cfg(not(unix))]
251fn stop_process(proc: &mut Child) -> Result<()> {
252    kill_process(proc)
253}
254
255fn kill_process(proc: &mut Child) -> Result<()> {
256    proc.kill()?;
257    proc.wait()?;
258    Ok(())
259}