use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use pagesift_cdp::EngineKind;
use pagesift_cdp::learn::{Sharded, WaitCap};
use pagesift_flow::{Admission, TabPermit};
use pagesift_stat::Delta;
use pagesift_stat::hazard::WindowQuantile;
use pagesift_stat::shrink::significant;
use pagesift_stat::survival::Survival;
use pagesift_stat::window::WindowRatio;
pub(super) const LAUNCH_RETRY: Duration = Duration::from_secs(600);
pub(super) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub(super) const MIN_ATTEMPT: Duration = Duration::from_millis(500);
#[derive(Clone)]
struct Shard {
window: WindowQuantile,
n: u64,
next: u64,
}
struct Quantile {
p: f64,
upper: bool,
shards: Sharded<Option<Shard>>,
value: AtomicU64,
busy: AtomicBool,
}
impl Quantile {
fn new(p: f64, upper: bool) -> Self {
Self {
p,
upper,
shards: Sharded::default(),
value: AtomicU64::new(f64::NAN.to_bits()),
busy: AtomicBool::new(false),
}
}
fn get(&self) -> Option<Duration> {
let v = f64::from_bits(self.value.load(Ordering::Relaxed));
v.is_finite().then(|| Duration::from_secs_f64(v.max(0.0)))
}
fn push(&self, x: Duration) {
let (p, shards) = (self.p, self.shards.len() as f64);
let due = self.shards.try_with(|s| {
let s = s.get_or_insert_with(|| Shard {
window: WindowQuantile::new(p),
n: 0,
next: 1,
});
s.window.add(x.as_secs_f64());
s.n += 1;
if s.n >= s.next {
s.next = s.n + ((s.n as f64 * shards).sqrt().ceil() as u64).max(1);
true
} else {
false
}
});
if due == Some(true) {
self.recompute();
}
}
fn recompute(&self) {
if self
.busy
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return;
}
let delta = Delta::DEFAULT;
let mut all = WindowQuantile::new(self.p);
self.shards.for_each(|s| {
if let Some(s) = s {
all.merge(&s.window);
}
});
if let Some(p) = all.ready(delta) {
let new = if self.upper { p.upper } else { p.lower };
let cur = f64::from_bits(self.value.load(Ordering::Relaxed));
let se = (p.upper - p.lower) / (2.0 * delta.z());
if !cur.is_finite() || significant(cur, new, se, delta) {
self.value.store(new.to_bits(), Ordering::Relaxed);
}
self.shards.for_each(|s| {
if let Some(s) = s {
s.window = WindowQuantile::new(self.p);
}
});
}
self.busy.store(false, Ordering::Release);
}
}
#[derive(Default)]
struct Outages {
ended: Survival,
open: Vec<Option<Instant>>,
}
pub struct RenderStats {
navigation: Quantile,
useful_total: Quantile,
useless_total: Quantile,
first_content: Quantile,
first_byte: [Quantile; 3],
default_timeout: WaitCap,
outages: Mutex<Outages>,
any_outage: AtomicBool,
launch_retry: AtomicU64,
tabs: Admission,
tab_limit: AtomicUsize,
extract_rate: Sharded<WindowRatio>,
extract_per_byte: AtomicU64,
}
impl Default for RenderStats {
fn default() -> Self {
let d = Delta::DEFAULT.get();
Self {
navigation: Quantile::new(1.0 - d, true),
useful_total: Quantile::new(1.0 - d, true),
useless_total: Quantile::new(1.0 - d, true),
first_content: Quantile::new(1.0 - d, true),
first_byte: std::array::from_fn(|_| Quantile::new(d, false)),
default_timeout: WaitCap::new(DEFAULT_TIMEOUT),
outages: Mutex::new(Outages::default()),
any_outage: AtomicBool::new(false),
launch_retry: AtomicU64::new(LAUNCH_RETRY.as_secs_f64().to_bits()),
tabs: Admission::new(1),
tab_limit: AtomicUsize::new(1),
extract_rate: Sharded::default(),
extract_per_byte: AtomicU64::new(f64::NAN.to_bits()),
}
}
}
fn engine_slot(e: EngineKind) -> usize {
match e {
EngineKind::Lightpanda => 0,
EngineKind::Moli => 1,
EngineKind::Chrome => 2,
}
}
impl RenderStats {
pub fn new() -> Self {
Self::default()
}
pub fn note_render(
&self,
navigation: Duration,
wait: Duration,
total: Duration,
first_content: Option<Duration>,
gained: bool,
) {
let _ = wait;
self.navigation.push(navigation);
if gained {
self.useful_total.push(total);
if let Some(t) = first_content {
self.first_content.push(t);
}
} else {
self.useless_total.push(total);
}
}
pub fn nav_patience(&self) -> Option<Duration> {
self.navigation.get()
}
pub fn early_stop_after(&self) -> Option<Duration> {
self.first_content.get()
}
pub fn budget(&self) -> Option<Duration> {
self.useful_total.get().or_else(|| self.useless_total.get())
}
pub fn note_first_byte(&self, engine: EngineKind, t: Duration) {
self.first_byte[engine_slot(engine)].push(t);
}
pub fn min_attempt(&self, engine: EngineKind) -> Duration {
self.first_byte[engine_slot(engine)]
.get()
.unwrap_or(MIN_ATTEMPT)
}
pub fn default_timeout(&self) -> Duration {
self.default_timeout.cap()
}
pub fn note_default_timeout(&self, elapsed: Duration, finished: bool) {
if finished {
self.default_timeout.done(elapsed, DEFAULT_TIMEOUT);
} else {
self.default_timeout.cut(elapsed, DEFAULT_TIMEOUT);
}
}
pub async fn tab_permit(&self, capacity: usize) -> TabPermit {
let capacity = capacity.max(1);
if self.tab_limit.swap(capacity, Ordering::Relaxed) != capacity {
self.tabs.set_tab_limit(capacity);
}
self.tabs.tab().await
}
pub fn tab_utilization(&self) -> f64 {
self.tabs.tab_utilization()
}
pub fn note_extract(&self, bytes: usize, t: Duration) {
if bytes == 0 {
return;
}
let published = self
.extract_rate
.try_with(|w| w.push(bytes as f64, t.as_secs_f64(), Delta::DEFAULT))
.flatten();
if let Some(r) = published {
self.extract_per_byte.store(r.to_bits(), Ordering::Relaxed);
}
}
pub fn extract_estimate(&self, bytes: usize) -> Option<Duration> {
let r = f64::from_bits(self.extract_per_byte.load(Ordering::Relaxed));
(r.is_finite() && r > 0.0).then(|| Duration::from_secs_f64(r * bytes as f64))
}
pub fn launch_retry(&self) -> Duration {
Duration::from_secs_f64(f64::from_bits(self.launch_retry.load(Ordering::Relaxed)))
}
pub fn note_launch_failure(&self, i: usize) {
let mut o = self.outages.lock().unwrap_or_else(|e| e.into_inner());
if o.open.len() <= i {
o.open.resize(i + 1, None);
}
o.open[i].get_or_insert_with(Instant::now);
self.any_outage.store(true, Ordering::Relaxed);
}
pub fn note_launch_ok(&self, i: usize) {
if !self.any_outage.load(Ordering::Relaxed) {
return;
}
let mut o = self.outages.lock().unwrap_or_else(|e| e.into_inner());
let Some(start) = o.open.get_mut(i).and_then(Option::take) else {
return;
};
o.ended.event(start.elapsed().as_secs_f64());
self.any_outage
.store(o.open.iter().any(Option::is_some), Ordering::Relaxed);
let mut all = o.ended.clone();
for s in o.open.iter().flatten() {
all.censor(s.elapsed().as_secs_f64());
}
drop(o);
let delta = Delta::DEFAULT;
let cur = self.launch_retry().as_secs_f64();
let (s, se) = all.survival_with_se(cur);
if let Some(t) = pagesift_stat::decide::retry_after(&all, delta)
&& significant(1.0 - delta.get(), 1.0 - s, se, delta)
&& t > 0.0
{
self.launch_retry.store(t.to_bits(), Ordering::Relaxed);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pagesift_stat::rng::Rng;
#[test]
fn stats_stay_disabled_until_the_quantile_bound_is_precise() {
let stats = RenderStats::new();
assert!(stats.nav_patience().is_none());
assert!(stats.early_stop_after().is_none());
assert!(stats.budget().is_none());
for i in 0..40 {
stats.note_render(
Duration::from_millis(50 + i),
Duration::from_millis(100 + i),
Duration::from_millis(200 + i),
Some(Duration::from_millis(20 + i)),
true,
);
}
assert!(stats.nav_patience().is_none());
}
#[test]
fn upper_bound_tracks_the_quantile() {
let stats = RenderStats::new();
let mut rng = Rng::seed(3);
let mut xs = Vec::new();
for _ in 0..50_000 {
let t = 0.3 * (0.8 * rng.normal()).exp();
xs.push(t);
stats.note_render(
Duration::from_secs_f64(t),
Duration::ZERO,
Duration::from_secs_f64(t),
None,
true,
);
}
let exact = 0.3 * (0.8 * 1.6449f64).exp();
let got = stats.nav_patience().expect("enough samples").as_secs_f64();
assert!(got > exact * 0.9 && got < exact * 1.3, "{got} vs {exact}");
assert!(stats.budget().is_some());
assert!(stats.early_stop_after().is_none());
}
#[test]
fn cold_start_equals_the_081_values() {
let stats = RenderStats::new();
assert_eq!(stats.launch_retry(), LAUNCH_RETRY);
assert_eq!(stats.min_attempt(EngineKind::Chrome), MIN_ATTEMPT);
let t: Vec<_> = (0..40).map(|_| stats.default_timeout()).collect();
assert!(t.iter().all(|&d| d == DEFAULT_TIMEOUT));
stats.note_launch_failure(1);
stats.note_launch_ok(0);
assert_eq!(stats.launch_retry(), LAUNCH_RETRY);
}
#[test]
fn launch_retry_follows_recovery_times() {
let stats = RenderStats::new();
let mut o = stats.outages.lock().unwrap();
for i in 0..200 {
o.ended.event(30.0 + (i % 60) as f64);
}
drop(o);
stats.note_launch_failure(0);
stats.note_launch_ok(0);
let r = stats.launch_retry().as_secs_f64();
assert!(r > 60.0 && r < 100.0, "{r}");
}
#[test]
fn first_byte_lower_bound_replaces_the_prior_when_precise() {
let stats = RenderStats::new();
let mut rng = Rng::seed(5);
for _ in 0..50_000 {
let t = 1.0 + 0.2 * rng.normal().abs();
stats.note_first_byte(EngineKind::Chrome, Duration::from_secs_f64(t));
}
let m = stats.min_attempt(EngineKind::Chrome).as_secs_f64();
assert!(m > 0.9 && m < 1.05, "{m}");
assert_eq!(stats.min_attempt(EngineKind::Moli), MIN_ATTEMPT);
}
}