1use std::collections::HashMap;
19use std::sync::Mutex;
20
21use pagesift_stat::beta::{BetaCounts, Prior};
22use pagesift_stat::rng::Rng;
23use pagesift_stat::{Reader, Stat, Writer};
24
25const TABLE_VERSION: u32 = 1;
27
28#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct RenderWhy {
31 pub learned: bool,
33 pub p: f64,
34 pub value: f64,
35 pub charge: f64,
36}
37
38pub type PlanObserver = std::sync::Arc<dyn Fn(u64, bool) + Send + Sync>;
40
41pub struct RenderPlanner {
42 shards: Box<[Mutex<HashMap<u64, BetaCounts>>]>,
43 observer: std::sync::OnceLock<PlanObserver>,
44}
45
46impl Default for RenderPlanner {
47 fn default() -> Self {
48 let n = std::thread::available_parallelism()
50 .map_or(1, |n| n.get())
51 .next_power_of_two();
52 Self {
53 shards: (0..n).map(|_| Mutex::new(HashMap::new())).collect(),
54 observer: std::sync::OnceLock::new(),
55 }
56 }
57}
58
59thread_local! {
60 static RNG: std::cell::RefCell<Rng> = std::cell::RefCell::new(Rng::from_entropy());
61}
62
63impl RenderPlanner {
64 fn with<R>(&self, family: &str, f: impl FnOnce(&mut HashMap<u64, BetaCounts>, u64) -> R) -> R {
65 let key = family_key(family);
66 let shard = &self.shards[(key as usize) & (self.shards.len() - 1)];
67 f(&mut shard.lock().unwrap_or_else(|e| e.into_inner()), key)
68 }
69
70 pub fn decide(
73 &self,
74 family: &str,
75 heuristic: bool,
76 http_quality: f64,
77 render_secs: f64,
78 remaining_secs: f64,
79 browser_price: f64,
80 ) -> (bool, RenderWhy) {
81 let counts = self.with(family, |m, k| m.get(&k).copied());
82 let value = 1.0 - http_quality.clamp(0.0, 1.0);
83 let charge = render_secs * (browser_price + 1.0 / remaining_secs.max(f64::MIN_POSITIVE));
84 match counts.filter(|c| c.trials() > 0.0) {
85 None => (
86 heuristic,
87 RenderWhy {
88 learned: false,
89 p: f64::from(u8::from(heuristic)),
90 value,
91 charge,
92 },
93 ),
94 Some(c) => {
95 let p = RNG.with(|r| c.posterior(Prior::UNIFORM).sample(&mut r.borrow_mut()));
96 (
97 p * value > charge,
98 RenderWhy {
99 learned: true,
100 p,
101 value,
102 charge,
103 },
104 )
105 }
106 }
107 }
108
109 pub fn record(&self, family: &str, gain: bool) {
111 let key = self.with(family, |m, k| {
112 m.entry(k).or_default().observe(gain);
113 k
114 });
115 if let Some(o) = self.observer.get() {
116 o(key, gain);
117 }
118 }
119
120 pub fn observe_with(&self, o: PlanObserver) -> bool {
122 self.observer.set(o).is_ok()
123 }
124
125 pub fn import(&self, key: u64, counts: BetaCounts) {
128 let shard = &self.shards[(key as usize) & (self.shards.len() - 1)];
129 let mut m = shard.lock().unwrap_or_else(|e| e.into_inner());
130 let e = m.entry(key).or_default();
131 if e.trials() == 0.0 {
132 *e = counts;
133 }
134 }
135
136 pub fn mean(&self, family: &str) -> Option<f64> {
138 self.with(family, |m, k| m.get(&k).copied())
139 .filter(|c| c.trials() > 0.0)
140 .map(|c| c.posterior(Prior::UNIFORM).mean())
141 }
142
143 pub fn encode(&self) -> Vec<u8> {
145 let mut w = Writer::new();
146 w.u32(TABLE_VERSION);
147 let entries: Vec<(u64, BetaCounts)> = self
148 .shards
149 .iter()
150 .flat_map(|s| {
151 let m = s.lock().unwrap_or_else(|e| e.into_inner());
152 m.iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>()
153 })
154 .collect();
155 w.u64(entries.len() as u64);
156 for (k, v) in &entries {
157 w.u64(*k);
158 v.encode(&mut w);
159 }
160 w.into_inner()
161 }
162
163 pub fn decode_merge(&self, buf: &[u8]) {
165 let mut r = Reader::new(buf);
166 let read = |r: &mut Reader<'_>| -> Option<Vec<(u64, BetaCounts)>> {
167 if r.u32().ok()? != TABLE_VERSION {
168 return None;
169 }
170 let n = r.u64().ok()?;
171 (0..n)
172 .map(|_| Some((r.u64().ok()?, BetaCounts::decode(r).ok()?)))
173 .collect()
174 };
175 for (k, v) in read(&mut r).unwrap_or_default() {
176 let shard = &self.shards[(k as usize) & (self.shards.len() - 1)];
177 shard
178 .lock()
179 .unwrap_or_else(|e| e.into_inner())
180 .entry(k)
181 .or_default()
182 .merge(&v);
183 }
184 }
185}
186
187pub fn family_key(family: &str) -> u64 {
188 let h = blake3::hash(family.as_bytes());
189 let (head, _) = h
191 .as_bytes()
192 .split_first_chunk::<8>()
193 .expect("BLAKE3 digest is 32 bytes");
194 u64::from_le_bytes(*head)
195}
196
197pub fn render_family(url: &str, client_code: bool) -> String {
200 format!(
201 "{}#{}",
202 family(url),
203 if client_code { "js" } else { "static" }
204 )
205}
206
207pub fn fallback_family(url: &str, engine: usize) -> String {
209 format!("{}#after{engine}", family(url))
210}
211
212pub fn family(raw: &str) -> String {
214 let Ok(url) = url::Url::parse(raw) else {
215 return raw.to_string();
216 };
217 let section = url
218 .path()
219 .trim_start_matches('/')
220 .split('/')
221 .next()
222 .unwrap_or("");
223 format!("{}/{}", url.origin().ascii_serialization(), section)
224}
225
226pub fn quality(doc: &pagesift_content::Document, html_bytes: usize) -> Option<f64> {
228 let model = crate::quality::QualityModel::builtin()?;
229 let weights = crate::script::ScriptWeights::builtin();
230 Some(model.predict(&crate::quality::features(doc, html_bytes, weights)))
231}