use parking_lot::Mutex;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[cfg(feature = "cdp-backend")]
use crate::render::chrome::page::Page;
#[derive(Debug, Clone, Copy)]
pub struct PagePoolLimits {
pub max_pages_per_context: usize,
pub max_uses_per_page: u32,
pub page_ttl: Duration,
pub idle_ttl: Duration,
}
impl Default for PagePoolLimits {
fn default() -> Self {
Self {
max_pages_per_context: 4,
max_uses_per_page: 100,
page_ttl: Duration::from_secs(300),
idle_ttl: Duration::from_secs(30),
}
}
}
#[cfg(feature = "cdp-backend")]
pub struct PooledPage {
pub page: Page,
pub created_at: Instant,
pub last_used: Instant,
pub uses: u32,
pub ctx_key: String,
}
#[cfg(feature = "cdp-backend")]
impl PooledPage {
pub fn new(page: Page, ctx_key: String) -> Self {
let now = Instant::now();
Self {
page,
created_at: now,
last_used: now,
uses: 1,
ctx_key,
}
}
}
#[derive(Debug, Default)]
pub struct CtxCounters {
pub in_flight: AtomicUsize,
pub total_created: AtomicUsize,
pub total_reused: AtomicUsize,
}
#[cfg(feature = "cdp-backend")]
pub struct PagePool {
idle: Mutex<HashMap<String, VecDeque<PooledPage>>>,
counters: Mutex<HashMap<String, Arc<CtxCounters>>>,
limits: PagePoolLimits,
}
#[cfg(feature = "cdp-backend")]
impl PagePool {
pub fn new(limits: PagePoolLimits) -> Self {
Self {
idle: Mutex::new(HashMap::new()),
counters: Mutex::new(HashMap::new()),
limits,
}
}
pub fn limits(&self) -> PagePoolLimits {
self.limits
}
fn counters_for(&self, ctx_key: &str) -> Arc<CtxCounters> {
let mut guard = self.counters.lock();
if let Some(c) = guard.get(ctx_key) {
return c.clone();
}
let c = Arc::new(CtxCounters::default());
guard.insert(ctx_key.to_string(), c.clone());
c
}
pub fn try_acquire(&self, ctx_key: &str) -> Option<PooledPage> {
let now = Instant::now();
let mut guard = self.idle.lock();
let q = guard.get_mut(ctx_key)?;
while let Some(mut candidate) = q.pop_front() {
if candidate.uses >= self.limits.max_uses_per_page
|| now.duration_since(candidate.created_at) > self.limits.page_ttl
{
continue;
}
candidate.uses = candidate.uses.saturating_add(1);
candidate.last_used = now;
let ctrs = self.counters_for(ctx_key);
ctrs.in_flight.fetch_add(1, Ordering::Relaxed);
ctrs.total_reused.fetch_add(1, Ordering::Relaxed);
return Some(candidate);
}
None
}
pub fn register_fresh(&self, ctx_key: &str) {
let ctrs = self.counters_for(ctx_key);
ctrs.in_flight.fetch_add(1, Ordering::Relaxed);
ctrs.total_created.fetch_add(1, Ordering::Relaxed);
}
pub fn release(&self, pooled: PooledPage) -> bool {
let ctx_key = pooled.ctx_key.clone();
let ctrs = self.counters_for(&ctx_key);
ctrs.in_flight.fetch_sub(1, Ordering::Relaxed);
let now = Instant::now();
if pooled.uses >= self.limits.max_uses_per_page
|| now.duration_since(pooled.created_at) > self.limits.page_ttl
{
return false;
}
let mut guard = self.idle.lock();
let q = guard.entry(ctx_key).or_default();
if q.len() >= self.limits.max_pages_per_context {
return false;
}
q.push_back(pooled);
true
}
pub fn release_dirty_key(&self, ctx_key: &str) {
let ctrs = self.counters_for(ctx_key);
ctrs.in_flight.fetch_sub(1, Ordering::Relaxed);
}
pub fn cleanup_idle(&self) -> usize {
let now = Instant::now();
let ttl = self.limits.idle_ttl;
let mut guard = self.idle.lock();
let mut evicted = 0usize;
for q in guard.values_mut() {
let before = q.len();
q.retain(|p| now.duration_since(p.last_used) <= ttl);
evicted += before - q.len();
}
evicted
}
pub fn drop_context(&self, ctx_key: &str) {
self.idle.lock().remove(ctx_key);
self.counters.lock().remove(ctx_key);
}
pub fn total_in_flight(&self) -> usize {
self.counters
.lock()
.values()
.map(|c| c.in_flight.load(Ordering::Relaxed))
.sum()
}
pub fn total_idle(&self) -> usize {
self.idle.lock().values().map(|q| q.len()).sum()
}
pub fn totals(&self) -> (usize, usize) {
let guard = self.counters.lock();
let created: usize = guard
.values()
.map(|c| c.total_created.load(Ordering::Relaxed))
.sum();
let reused: usize = guard
.values()
.map(|c| c.total_reused.load(Ordering::Relaxed))
.sum();
(created, reused)
}
}
#[cfg(feature = "cdp-backend")]
pub struct PageLease {
pool: Arc<PagePool>,
pooled: Option<PooledPage>,
ctx_key: String,
consumed: bool,
}
#[cfg(feature = "cdp-backend")]
impl PageLease {
pub fn new(pool: Arc<PagePool>, pooled: PooledPage) -> Self {
let ctx_key = pooled.ctx_key.clone();
Self {
pool,
pooled: Some(pooled),
ctx_key,
consumed: false,
}
}
pub fn page(&self) -> &Page {
&self.pooled.as_ref().expect("pooled page present").page
}
pub fn release_clean(mut self) -> bool {
self.consumed = true;
if let Some(p) = self.pooled.take() {
self.pool.release(p)
} else {
false
}
}
pub fn release_dirty(mut self) {
self.consumed = true;
self.pooled = None;
self.pool.release_dirty_key(&self.ctx_key);
}
}
#[cfg(feature = "cdp-backend")]
impl Drop for PageLease {
fn drop(&mut self) {
if self.consumed {
return;
}
self.pooled.take();
self.pool.release_dirty_key(&self.ctx_key);
}
}
#[cfg(all(test, feature = "cdp-backend"))]
mod tests {
use super::*;
#[test]
fn limits_default_sane() {
let l = PagePoolLimits::default();
assert_eq!(l.max_pages_per_context, 4);
assert_eq!(l.max_uses_per_page, 100);
assert_eq!(l.page_ttl, Duration::from_secs(300));
assert_eq!(l.idle_ttl, Duration::from_secs(30));
}
#[test]
fn counters_isolated_per_ctx() {
let pool = PagePool::new(PagePoolLimits::default());
pool.register_fresh("ctx-a");
pool.register_fresh("ctx-a");
pool.register_fresh("ctx-b");
let a = pool.counters_for("ctx-a");
let b = pool.counters_for("ctx-b");
assert_eq!(a.in_flight.load(Ordering::Relaxed), 2);
assert_eq!(b.in_flight.load(Ordering::Relaxed), 1);
pool.release_dirty_key("ctx-a");
assert_eq!(a.in_flight.load(Ordering::Relaxed), 1);
}
}