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    ///
32    /// The mutex is only held briefly during the synchronous `pop_front`;
33    /// any async page construction happens after the lock is released.
34    pub async fn acquire(
35        &self,
36        profile: Option<StealthProfile>,
37    ) -> Result<Page, crate::page::PageError> {
38        // Try to reuse a pooled page (brief sync lock).
39        if let Some(mut page) = {
40            let mut pages = self.idle_pages.lock().unwrap_or_else(|e| e.into_inner());
41            pages.pop_front()
42        } {
43            page.reload_html("<html><head></head><body></body></html>", "about:blank");
44            return Ok(page);
45        }
46        // No pooled page available — create a new one (async, no lock held).
47        Page::from_html("<html><head></head><body></body></html>", profile.is_some()).await
48    }
49
50    /// Return a page to the pool.
51    pub fn release(&self, page: Page) {
52        let mut pages = self.idle_pages.lock().unwrap_or_else(|e| e.into_inner());
53        if pages.len() < self.max_size {
54            pages.push_back(page);
55        }
56    }
57
58    /// Acquire a warm Page and navigate it to `url`.
59    pub async fn navigate(
60        &self,
61        url: &str,
62        profile: StealthProfile,
63    ) -> Result<Page, crate::page::PageError> {
64        let mut page = self.acquire(Some(profile)).await?;
65        page.navigate_warm(url).await?;
66        Ok(page)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[tokio::test]
75    async fn pool_acquire_creates_page() {
76        let pool = PagePool::new(4);
77        let page = pool.acquire(None).await;
78        assert!(page.is_ok());
79    }
80
81    #[tokio::test]
82    async fn pool_release_and_reacquire() {
83        let pool = PagePool::new(4);
84        let page = pool.acquire(None).await.unwrap();
85        pool.release(page);
86        let page2 = pool.acquire(None).await;
87        assert!(page2.is_ok());
88    }
89
90    #[tokio::test]
91    async fn pool_respects_max_size() {
92        let pool = PagePool::new(1);
93        let p1 = pool.acquire(None).await.unwrap();
94        let p2 = pool.acquire(None).await.unwrap();
95        pool.release(p1);
96        pool.release(p2); // second release should be dropped (pool full)
97        let count = pool.idle_pages.lock().unwrap().len();
98        assert_eq!(count, 1);
99    }
100}