1use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::Rc;
7use std::time::{Duration, Instant};
8
9use dpi::PhysicalSize;
10use servo::Servo;
11
12use crate::config::{BaoConfig, PageConfig};
13use crate::delegate::BaoServoDelegate;
14use crate::error::BrowserError;
15use crate::page::PageHandle;
16
17pub struct PoolStats {
18 pub active: usize,
19 pub idle: usize,
20 pub total_created: usize,
21 pub total_destroyed: usize,
22}
23
24struct IdleEntry {
25 page: PageHandle,
26 idle_since: Instant,
27}
28
29pub struct PagePool {
30 servo: Rc<Servo>,
31 servo_delegate: Rc<BaoServoDelegate>,
32 active_pages: RefCell<HashMap<usize, PageHandle>>,
33 idle_pages: RefCell<HashMap<usize, IdleEntry>>,
34 max_total: usize,
35 idle_ttl: Duration,
36 default_viewport: PhysicalSize<u32>,
37 next_id: RefCell<usize>,
38 total_created: RefCell<usize>,
39 total_destroyed: RefCell<usize>,
40}
41
42impl PagePool {
43 pub fn new(servo: Rc<Servo>, servo_delegate: Rc<BaoServoDelegate>, config: &BaoConfig) -> Self {
44 PagePool {
45 servo,
46 servo_delegate,
47 active_pages: RefCell::new(HashMap::new()),
48 idle_pages: RefCell::new(HashMap::new()),
49 max_total: config.max_pages,
50 idle_ttl: config.idle_ttl,
51 default_viewport: PhysicalSize::new(
52 config.default_viewport_width,
53 config.default_viewport_height,
54 ),
55 next_id: RefCell::new(1),
56 total_created: RefCell::new(0),
57 total_destroyed: RefCell::new(0),
58 }
59 }
60
61 pub fn create_page(&self, config: &PageConfig) -> Result<PageHandle, BrowserError> {
62 let total = self.active_pages.borrow().len() + self.idle_pages.borrow().len();
63 if total >= self.max_total {
64 return Err(BrowserError::Init(format!(
65 "page limit exceeded: {total}/{}",
66 self.max_total
67 )));
68 }
69
70 let id = {
71 let mut next = self.next_id.borrow_mut();
72 let id = *next;
73 *next += 1;
74 id
75 };
76
77 let page = PageHandle::new(
78 Rc::clone(&self.servo),
79 Rc::clone(&self.servo_delegate),
80 config,
81 self.default_viewport,
82 id,
83 )?;
84
85 page.wait_for_pipeline_ready(Duration::from_secs(10))?;
87 let stealth = config.stealth_profile.is_some();
88 crate::runtime_bridge::inject_all(&page, stealth)?;
89
90 self.active_pages.borrow_mut().insert(id, page.clone());
91 *self.total_created.borrow_mut() += 1;
92
93 Ok(page)
94 }
95
96 pub fn get_page(&self, id: usize) -> Option<PageHandle> {
97 if let Some(page) = self.active_pages.borrow().get(&id) {
98 return Some(page.clone());
99 }
100 if let Some(entry) = self.idle_pages.borrow_mut().remove(&id) {
101 self.active_pages
102 .borrow_mut()
103 .insert(id, entry.page.clone());
104 return Some(entry.page);
105 }
106 None
107 }
108
109 pub fn close_page(&self, id: usize) -> Result<(), BrowserError> {
110 if let Some(page) = self.active_pages.borrow_mut().remove(&id) {
111 page.close()?;
112 *self.total_destroyed.borrow_mut() += 1;
113 return Ok(());
114 }
115 if let Some(entry) = self.idle_pages.borrow_mut().remove(&id) {
116 entry.page.close()?;
117 *self.total_destroyed.borrow_mut() += 1;
118 return Ok(());
119 }
120 Err(BrowserError::Init(format!("page {id} not found")))
121 }
122
123 pub fn release_page(&self, id: usize) {
124 if let Some(page) = self.active_pages.borrow_mut().remove(&id) {
125 self.idle_pages.borrow_mut().insert(
126 id,
127 IdleEntry {
128 page,
129 idle_since: Instant::now(),
130 },
131 );
132 }
133 }
134
135 pub fn check_idle_pages(&self) -> usize {
136 let mut reclaimed = 0;
137 let expired: Vec<usize> = self
138 .idle_pages
139 .borrow()
140 .iter()
141 .filter(|(_, entry)| entry.idle_since.elapsed() > self.idle_ttl)
142 .map(|(id, _)| *id)
143 .collect();
144
145 for id in expired {
146 if let Some(entry) = self.idle_pages.borrow_mut().remove(&id) {
147 let _ = entry.page.close();
148 *self.total_destroyed.borrow_mut() += 1;
149 reclaimed += 1;
150 }
151 }
152
153 reclaimed
154 }
155
156 pub fn stats(&self) -> PoolStats {
157 PoolStats {
158 active: self.active_pages.borrow().len(),
159 idle: self.idle_pages.borrow().len(),
160 total_created: *self.total_created.borrow(),
161 total_destroyed: *self.total_destroyed.borrow(),
162 }
163 }
164
165 pub fn close_all(&self) {
166 for (_, page) in self.active_pages.borrow_mut().drain() {
167 let _ = page.close();
168 *self.total_destroyed.borrow_mut() += 1;
169 }
170 for (_, entry) in self.idle_pages.borrow_mut().drain() {
171 let _ = entry.page.close();
172 *self.total_destroyed.borrow_mut() += 1;
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn pool_stats_construction() {
183 let stats = PoolStats {
184 active: 3,
185 idle: 2,
186 total_created: 10,
187 total_destroyed: 5,
188 };
189 assert_eq!(stats.active, 3);
190 assert_eq!(stats.idle, 2);
191 assert_eq!(stats.total_created, 10);
192 assert_eq!(stats.total_destroyed, 5);
193 }
194
195 #[test]
196 fn pool_stats_zero() {
197 let stats = PoolStats {
198 active: 0,
199 idle: 0,
200 total_created: 0,
201 total_destroyed: 0,
202 };
203 assert_eq!(stats.active + stats.idle, 0);
204 }
205
206 #[test]
207 fn pool_stats_invariant() {
208 let stats = PoolStats {
210 active: 5,
211 idle: 3,
212 total_created: 20,
213 total_destroyed: 12,
214 };
215 assert!(stats.total_created >= stats.total_destroyed);
216 assert_eq!(
217 stats.active + stats.idle,
218 stats.total_created - stats.total_destroyed
219 );
220 }
221
222 #[test]
223 fn pool_stats_all_active() {
224 let stats = PoolStats {
225 active: 8,
226 idle: 0,
227 total_created: 8,
228 total_destroyed: 0,
229 };
230 assert_eq!(stats.idle, 0);
231 assert_eq!(stats.active, stats.total_created);
232 }
233
234 #[test]
235 fn pool_stats_all_idle() {
236 let stats = PoolStats {
237 active: 0,
238 idle: 4,
239 total_created: 4,
240 total_destroyed: 0,
241 };
242 assert_eq!(stats.active, 0);
243 assert_eq!(stats.idle, stats.total_created);
244 }
245}