Skip to main content

hpx_browser/
pool.rs

1//! Page pool for concurrent browsing.
2//!
3//! Reusing pages skips V8 isolate creation and bootstrap JS execution.
4
5use std::{
6    collections::VecDeque,
7    sync::{Arc, Mutex},
8};
9
10use crate::{page::Page, stealth::StealthProfile};
11
12/// A pool of warm Page instances.
13pub struct PagePool {
14    idle_pages: Arc<Mutex<VecDeque<Page>>>,
15    max_size: usize,
16}
17
18impl PagePool {
19    #[allow(
20        clippy::arc_with_non_send_sync,
21        reason = "single-threaded page pool; Arc shares idle queue within one thread"
22    )]
23    pub fn new(max_size: usize) -> Self {
24        Self {
25            idle_pages: Arc::new(Mutex::new(VecDeque::with_capacity(max_size))),
26            max_size,
27        }
28    }
29
30    /// Acquire a page from the pool or create a new one.
31    #[allow(
32        clippy::await_holding_lock,
33        reason = "std Mutex guard dropped before any await; single-threaded executor"
34    )]
35    pub async fn acquire(
36        &self,
37        profile: Option<StealthProfile>,
38    ) -> Result<Page, crate::page::PageError> {
39        let mut pages = self.idle_pages.lock().unwrap_or_else(|e| e.into_inner());
40        if let Some(mut page) = pages.pop_front() {
41            page.reload_html("<html><head></head><body></body></html>", "about:blank");
42            return Ok(page);
43        }
44        Page::from_html("<html><head></head><body></body></html>", profile).await
45    }
46
47    /// Return a page to the pool.
48    pub fn release(&self, page: Page) {
49        let mut pages = self.idle_pages.lock().unwrap_or_else(|e| e.into_inner());
50        if pages.len() < self.max_size {
51            pages.push_back(page);
52        }
53    }
54
55    /// Acquire a warm Page and navigate it to `url`.
56    pub async fn navigate(
57        &self,
58        url: &str,
59        profile: StealthProfile,
60    ) -> Result<Page, crate::page::PageError> {
61        let mut page = self.acquire(Some(profile)).await?;
62        page.navigate_warm(url).await?;
63        Ok(page)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[tokio::test]
72    async fn pool_acquire_creates_page() {
73        let pool = PagePool::new(4);
74        let page = pool.acquire(None).await;
75        assert!(page.is_ok());
76    }
77
78    #[tokio::test]
79    async fn pool_release_and_reacquire() {
80        let pool = PagePool::new(4);
81        let page = pool.acquire(None).await.unwrap();
82        pool.release(page);
83        let page2 = pool.acquire(None).await;
84        assert!(page2.is_ok());
85    }
86
87    #[tokio::test]
88    async fn pool_respects_max_size() {
89        let pool = PagePool::new(1);
90        let p1 = pool.acquire(None).await.unwrap();
91        let p2 = pool.acquire(None).await.unwrap();
92        pool.release(p1);
93        pool.release(p2); // second release should be dropped (pool full)
94        let count = pool.idle_pages.lock().unwrap().len();
95        assert_eq!(count, 1);
96    }
97}