1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/// Pool of reusable Chrome CDP tabs.
///
/// Lock-free design: uses a DashMap as a concurrent stack (push/pop by
/// atomic index). No Mutex, no RwLock.
pub struct TabPool {
/// Tabs stored by slot index. DashMap provides lock-free per-shard access.
slots: dashmap::DashMap<usize, chromiumoxide::Page>,
/// Next slot to write into (monotonically increasing).
head: std::sync::atomic::AtomicUsize,
/// Maximum pool capacity.
max_size: usize,
}
impl TabPool {
/// Create a new tab pool with the given maximum size.
pub fn new(max_size: usize) -> Self {
Self {
slots: dashmap::DashMap::with_capacity(max_size),
head: std::sync::atomic::AtomicUsize::new(0),
max_size,
}
}
/// Acquire a tab from the pool or create a new one.
///
/// Pops the most recently pooled tab (LIFO) if available, otherwise
/// creates a fresh tab via `browser.new_page("about:blank")`.
pub async fn acquire(
&self,
browser: &chromiumoxide::Browser,
) -> Result<chromiumoxide::Page, chromiumoxide::error::CdpError> {
// Try to pop from the stack (LIFO).
loop {
let current = self.head.load(std::sync::atomic::Ordering::Acquire);
if current == 0 {
break; // pool empty
}
let target = current - 1;
// CAS to claim this slot.
if self
.head
.compare_exchange(
current,
target,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Relaxed,
)
.is_ok()
{
// We won the slot — remove and return the tab.
if let Some((_, page)) = self.slots.remove(&target) {
return Ok(page);
}
// Slot was empty (shouldn't happen), continue to create new.
break;
}
// CAS failed — another thread popped; retry.
}
browser.new_page("about:blank").await
}
/// Release a tab back to the pool.
///
/// Navigates the tab to `about:blank` to clear state before pooling.
/// If the navigation hangs for more than 5 seconds the tab is dropped.
/// If the pool is already at capacity the tab is also dropped (closed).
pub async fn release(&self, page: chromiumoxide::Page) {
let current = self.head.load(std::sync::atomic::Ordering::Relaxed);
if current >= self.max_size {
return; // at capacity, drop the page
}
// Navigate to about:blank with a 5s timeout to clear state.
let ok = matches!(
tokio::time::timeout(std::time::Duration::from_secs(5), page.goto("about:blank")).await,
Ok(Ok(_))
);
if !ok {
return; // navigation failed/timed out, drop the page
}
// Try to push onto the stack.
loop {
let current = self.head.load(std::sync::atomic::Ordering::Acquire);
if current >= self.max_size {
return; // pool filled while we were navigating
}
if self
.head
.compare_exchange(
current,
current + 1,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Relaxed,
)
.is_ok()
{
self.slots.insert(current, page);
return;
}
// CAS failed — another thread pushed; retry.
}
}
/// Drop all pooled tabs by draining the map.
pub fn clear(&self) {
self.head.store(0, std::sync::atomic::Ordering::Release);
self.slots.clear();
}
/// Returns the approximate number of pooled (idle) tabs.
pub fn pool_size(&self) -> usize {
self.head.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_pool_is_empty() {
let pool = TabPool::new(5);
assert_eq!(pool.pool_size(), 0);
}
#[test]
fn test_pool_max_size() {
let pool = TabPool::new(0);
assert_eq!(pool.max_size, 0);
let pool = TabPool::new(100);
assert_eq!(pool.max_size, 100);
}
#[test]
fn test_clear_empty_pool() {
let pool = TabPool::new(5);
pool.clear();
assert_eq!(pool.pool_size(), 0);
}
}