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