1use std::sync::atomic::{AtomicI64, AtomicU8, AtomicU32, Ordering};
20
21pub const MAX_EACH_BUCKETS: usize = 256;
23
24pub const MAX_EACH_KEYS: usize = 6;
26
27const EACH_MSG_LEN: usize = 32;
29
30pub const EACH_BUCKET_MEM_SIZE: usize = 8 + MAX_EACH_BUCKETS * std::mem::size_of::<EachBucket>();
32
33#[repr(C)]
38#[derive(Clone, Copy)]
39pub struct EachBucket {
40 pub site_hash: u32,
42 pub bucket_hash: u32,
44 pub split_triggered: u8,
46 pub num_keys: u8,
48 pub has_quality: u8,
50 pad: u8,
52 pub pass_count: u32,
54 pub best_score: i64,
56 pub key_values: [i64; MAX_EACH_KEYS],
58 pub msg: [u8; EACH_MSG_LEN],
60}
61
62impl EachBucket {
63 #[must_use]
65 pub fn msg_str(&self) -> &str {
66 let len = self
67 .msg
68 .iter()
69 .position(|&b| b == 0)
70 .unwrap_or(EACH_MSG_LEN);
71 std::str::from_utf8(&self.msg[..len]).unwrap_or("???")
72 }
73}
74
75use crate::slots::msg_hash;
76
77unsafe fn find_or_alloc_each_bucket(
86 ptr: *mut u8,
87 site_hash: u32,
88 bucket_hash: u32,
89 keys: &[(&str, i64)],
90 msg: &str,
91 has_quality: u8,
92) -> *mut EachBucket {
93 unsafe {
94 let next_atomic = &*ptr.cast::<()>().cast::<AtomicU32>();
95 let count = next_atomic.load(Ordering::Relaxed) as usize;
96 let base = ptr.add(8).cast::<()>().cast::<EachBucket>();
97
98 for i in 0..count.min(MAX_EACH_BUCKETS) {
100 let bucket = base.add(i);
101 if (*bucket).site_hash == site_hash && (*bucket).bucket_hash == bucket_hash {
102 return bucket;
103 }
104 }
105
106 let new_idx = next_atomic.fetch_add(1, Ordering::Relaxed) as usize;
108 if new_idx >= MAX_EACH_BUCKETS {
109 next_atomic.fetch_sub(1, Ordering::Relaxed);
110 return std::ptr::null_mut();
111 }
112
113 let bucket = base.add(new_idx);
114 let mut msg_buf = [0u8; EACH_MSG_LEN];
115 let n = msg.len().min(EACH_MSG_LEN - 1);
116 msg_buf[..n].copy_from_slice(&msg.as_bytes()[..n]);
117
118 let mut key_values = [0i64; MAX_EACH_KEYS];
119 let num_keys = keys.len().min(MAX_EACH_KEYS);
120 for (i, &(_, v)) in keys.iter().take(num_keys).enumerate() {
121 key_values[i] = v;
122 }
123
124 std::ptr::write(
125 bucket,
126 EachBucket {
127 site_hash,
128 bucket_hash,
129 split_triggered: 0,
130 num_keys: u8::try_from(num_keys).expect("num_keys capped at MAX_EACH_KEYS=6"),
131 has_quality,
132 pad: 0,
133 pass_count: 0,
134 best_score: i64::MIN,
135 key_values,
136 msg: msg_buf,
137 },
138 );
139 bucket
140 }
141}
142
143fn compute_each_bucket_index(base_ptr: *mut u8, bucket: *const EachBucket) -> usize {
145 if base_ptr.is_null() {
146 return 0;
147 }
148 let buckets_base = unsafe { base_ptr.add(8) } as usize;
149 let offset = (bucket as usize).saturating_sub(buckets_base);
150 offset / std::mem::size_of::<EachBucket>()
151}
152
153fn pack_quality(quality: &[(&str, i64)]) -> i64 {
159 let mut packed: i64 = 0;
160 let n = quality.len().min(4);
161 for (i, &(_, v)) in quality.iter().take(n).enumerate() {
162 let shift = (3 - i) * 16;
163 packed |= (v & 0xffff) << shift;
165 }
166 packed
167}
168
169#[must_use]
171pub fn unpack_quality(packed: i64, n: u8) -> Vec<i64> {
172 (0..n as usize)
173 .map(|i| {
174 let shift = (3 - i) * 16;
175 (packed >> shift) & 0xffff
177 })
178 .collect()
179}
180
181pub fn assertion_sometimes_each(msg: &str, keys: &[(&str, i64)], quality: &[(&str, i64)]) {
189 let ptr = crate::region::each_bucket_ptr();
190 if ptr.is_null() {
191 return;
192 }
193
194 let site_hash = msg_hash(msg);
197 let mut bucket_hash = site_hash;
198 for &(_, val) in keys {
199 for b in val.to_le_bytes() {
200 bucket_hash ^= u32::from(b);
201 bucket_hash = bucket_hash.wrapping_mul(0x0100_0193);
202 }
203 }
204
205 crate::hooks::on_bucket_mark(bucket_hash);
209
210 let has_quality = u8::try_from(quality.len().min(4)).unwrap_or(4);
212 let score = if has_quality > 0 {
213 pack_quality(quality)
214 } else {
215 0
216 };
217
218 let bucket =
220 unsafe { find_or_alloc_each_bucket(ptr, site_hash, bucket_hash, keys, msg, has_quality) };
221 if bucket.is_null() {
222 return;
223 }
224
225 unsafe {
229 let count_atomic = &*(&raw const (*bucket).pass_count).cast::<AtomicU32>();
231 count_atomic.fetch_add(1, Ordering::Relaxed);
232
233 let ft = &*(&raw const (*bucket).split_triggered).cast::<AtomicU8>();
235 let first_discovery = ft
236 .compare_exchange(0, 1, Ordering::Relaxed, Ordering::Relaxed)
237 .is_ok();
238
239 if first_discovery {
240 if has_quality > 0 {
242 let bs_atomic = &*(&raw const (*bucket).best_score).cast::<AtomicI64>();
243 bs_atomic.store(score, Ordering::Relaxed);
244 }
245
246 let bucket_index = compute_each_bucket_index(ptr, bucket);
247 crate::hooks::on_bucket_split(msg, bucket_index % crate::slots::MAX_ASSERTION_SLOTS);
248 } else if has_quality > 0 {
249 let bs_atomic = &*(&raw const (*bucket).best_score).cast::<AtomicI64>();
252 let mut current = bs_atomic.load(Ordering::Relaxed);
253 loop {
254 if score <= current {
255 break;
256 }
257 match bs_atomic.compare_exchange_weak(
258 current,
259 score,
260 Ordering::Relaxed,
261 Ordering::Relaxed,
262 ) {
263 Ok(_) => {
264 let bucket_index = compute_each_bucket_index(ptr, bucket);
265 crate::hooks::on_bucket_split(
266 msg,
267 bucket_index % crate::slots::MAX_ASSERTION_SLOTS,
268 );
269 break;
270 }
271 Err(actual) => current = actual,
272 }
273 }
274 }
275 }
276}
277
278#[must_use]
282pub fn each_bucket_read_all() -> Vec<EachBucket> {
283 let ptr = crate::region::each_bucket_ptr();
284 if ptr.is_null() {
285 return Vec::new();
286 }
287 unsafe {
293 let count = (*ptr.cast::<()>().cast::<u32>()) as usize;
294 let count = count.min(MAX_EACH_BUCKETS);
295 let base = ptr.add(8).cast::<()>().cast::<EachBucket>();
296 (0..count).map(|i| std::ptr::read(base.add(i))).collect()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_msg_hash_deterministic() {
306 let h1 = msg_hash("test_assertion");
307 let h2 = msg_hash("test_assertion");
308 assert_eq!(h1, h2);
309 }
310
311 #[test]
312 fn test_msg_hash_different_inputs() {
313 let h1 = msg_hash("alpha");
314 let h2 = msg_hash("beta");
315 let h3 = msg_hash("gamma");
316 assert_ne!(h1, h2);
317 assert_ne!(h2, h3);
318 assert_ne!(h1, h3);
319 }
320
321 #[test]
322 fn test_pack_unpack_quality_roundtrip() {
323 let quality = &[("health", 100i64), ("armor", 50i64), ("mana", 200i64)];
324 let packed = pack_quality(quality);
325 let unpacked = unpack_quality(packed, 3);
326 assert_eq!(unpacked, vec![100, 50, 200]);
327 }
328
329 #[test]
330 fn test_pack_quality_single() {
331 let quality = &[("health", 42i64)];
332 let packed = pack_quality(quality);
333 let unpacked = unpack_quality(packed, 1);
334 assert_eq!(unpacked, vec![42]);
335 }
336
337 #[test]
338 fn test_each_bucket_size_stable() {
339 assert_eq!(std::mem::size_of::<EachBucket>(), 104);
342 }
343
344 #[test]
345 fn test_each_bucket_read_all_when_inactive() {
346 let buckets = each_bucket_read_all();
348 assert!(buckets.is_empty());
349 }
350
351 #[test]
352 fn test_assertion_sometimes_each_noop_when_inactive() {
353 assertion_sometimes_each("test", &[("key", 1)], &[]);
355 }
356}