1use std::sync::atomic::{AtomicI64, AtomicU8, AtomicU32, Ordering};
17
18pub const MAX_EACH_BUCKETS: usize = 256;
20
21pub const MAX_EACH_KEYS: usize = 6;
23
24const EACH_MSG_LEN: usize = 32;
26
27pub const EACH_BUCKET_MEM_SIZE: usize = 8 + MAX_EACH_BUCKETS * std::mem::size_of::<EachBucket>();
29
30#[repr(C)]
35#[derive(Clone, Copy)]
36pub struct EachBucket {
37 pub site_hash: u32,
39 pub bucket_hash: u32,
41 pub split_triggered: u8,
43 pub num_keys: u8,
45 pub has_quality: u8,
47 pad: u8,
49 pub pass_count: u32,
51 pub best_score: i64,
53 pub key_values: [i64; MAX_EACH_KEYS],
55 pub msg: [u8; EACH_MSG_LEN],
57}
58
59impl EachBucket {
60 #[must_use]
62 pub fn msg_str(&self) -> &str {
63 let len = self
64 .msg
65 .iter()
66 .position(|&b| b == 0)
67 .unwrap_or(EACH_MSG_LEN);
68 std::str::from_utf8(&self.msg[..len]).unwrap_or("???")
69 }
70}
71
72use crate::assertion_slots::msg_hash;
73
74unsafe fn find_or_alloc_each_bucket(
83 ptr: *mut u8,
84 site_hash: u32,
85 bucket_hash: u32,
86 keys: &[(&str, i64)],
87 msg: &str,
88 has_quality: u8,
89) -> *mut EachBucket {
90 unsafe {
91 let next_atomic = &*ptr.cast::<()>().cast::<AtomicU32>();
92 let count = next_atomic.load(Ordering::Relaxed) as usize;
93 let base = ptr.add(8).cast::<()>().cast::<EachBucket>();
94
95 for i in 0..count.min(MAX_EACH_BUCKETS) {
97 let bucket = base.add(i);
98 if (*bucket).site_hash == site_hash && (*bucket).bucket_hash == bucket_hash {
99 return bucket;
100 }
101 }
102
103 let new_idx = next_atomic.fetch_add(1, Ordering::Relaxed) as usize;
105 if new_idx >= MAX_EACH_BUCKETS {
106 next_atomic.fetch_sub(1, Ordering::Relaxed);
107 return std::ptr::null_mut();
108 }
109
110 let bucket = base.add(new_idx);
111 let mut msg_buf = [0u8; EACH_MSG_LEN];
112 let n = msg.len().min(EACH_MSG_LEN - 1);
113 msg_buf[..n].copy_from_slice(&msg.as_bytes()[..n]);
114
115 let mut key_values = [0i64; MAX_EACH_KEYS];
116 let num_keys = keys.len().min(MAX_EACH_KEYS);
117 for (i, &(_, v)) in keys.iter().take(num_keys).enumerate() {
118 key_values[i] = v;
119 }
120
121 std::ptr::write(
122 bucket,
123 EachBucket {
124 site_hash,
125 bucket_hash,
126 split_triggered: 0,
127 num_keys: u8::try_from(num_keys).expect("num_keys capped at MAX_EACH_KEYS=6"),
128 has_quality,
129 pad: 0,
130 pass_count: 0,
131 best_score: i64::MIN,
132 key_values,
133 msg: msg_buf,
134 },
135 );
136 bucket
137 }
138}
139
140fn compute_each_bucket_index(base_ptr: *mut u8, bucket: *const EachBucket) -> usize {
142 if base_ptr.is_null() {
143 return 0;
144 }
145 let buckets_base = unsafe { base_ptr.add(8) } as usize;
146 let offset = (bucket as usize).saturating_sub(buckets_base);
147 offset / std::mem::size_of::<EachBucket>()
148}
149
150fn pack_quality(quality: &[(&str, i64)]) -> i64 {
156 let mut packed: i64 = 0;
157 let n = quality.len().min(4);
158 for (i, &(_, v)) in quality.iter().take(n).enumerate() {
159 let shift = (3 - i) * 16;
160 packed |= (v & 0xffff) << shift;
162 }
163 packed
164}
165
166#[must_use]
168pub fn unpack_quality(packed: i64, n: u8) -> Vec<i64> {
169 (0..n as usize)
170 .map(|i| {
171 let shift = (3 - i) * 16;
172 (packed >> shift) & 0xffff
174 })
175 .collect()
176}
177
178pub fn assertion_sometimes_each(msg: &str, keys: &[(&str, i64)], quality: &[(&str, i64)]) {
186 let ptr = crate::context::EACH_BUCKET_PTR.with(std::cell::Cell::get);
187 if ptr.is_null() {
188 return;
189 }
190
191 let site_hash = msg_hash(msg);
194 let mut bucket_hash = site_hash;
195 for &(_, val) in keys {
196 for b in val.to_le_bytes() {
197 bucket_hash ^= u32::from(b);
198 bucket_hash = bucket_hash.wrapping_mul(0x0100_0193);
199 }
200 }
201
202 let bm_ptr = crate::context::COVERAGE_BITMAP_PTR.with(std::cell::Cell::get);
206 if !bm_ptr.is_null() {
207 let bm = unsafe { crate::coverage::CoverageBitmap::new(bm_ptr) };
210 bm.set_bit(bucket_hash as usize);
211 }
212
213 let has_quality = u8::try_from(quality.len().min(4)).unwrap_or(4);
215 let score = if has_quality > 0 {
216 pack_quality(quality)
217 } else {
218 0
219 };
220
221 let bucket =
223 unsafe { find_or_alloc_each_bucket(ptr, site_hash, bucket_hash, keys, msg, has_quality) };
224 if bucket.is_null() {
225 return;
226 }
227
228 unsafe {
232 let count_atomic = &*(&raw const (*bucket).pass_count).cast::<AtomicU32>();
234 count_atomic.fetch_add(1, Ordering::Relaxed);
235
236 let ft = &*(&raw const (*bucket).split_triggered).cast::<AtomicU8>();
238 let first_discovery = ft
239 .compare_exchange(0, 1, Ordering::Relaxed, Ordering::Relaxed)
240 .is_ok();
241
242 if first_discovery {
243 if has_quality > 0 {
245 let bs_atomic = &*(&raw const (*bucket).best_score).cast::<AtomicI64>();
246 bs_atomic.store(score, Ordering::Relaxed);
247 }
248
249 let bucket_index = compute_each_bucket_index(ptr, bucket);
250 crate::split_loop::dispatch_split(
251 msg,
252 bucket_index % crate::assertion_slots::MAX_ASSERTION_SLOTS,
253 );
254 } else if has_quality > 0 {
255 let bs_atomic = &*(&raw const (*bucket).best_score).cast::<AtomicI64>();
258 let mut current = bs_atomic.load(Ordering::Relaxed);
259 loop {
260 if score <= current {
261 break;
262 }
263 match bs_atomic.compare_exchange_weak(
264 current,
265 score,
266 Ordering::Relaxed,
267 Ordering::Relaxed,
268 ) {
269 Ok(_) => {
270 let bucket_index = compute_each_bucket_index(ptr, bucket);
271 crate::split_loop::dispatch_split(
272 msg,
273 bucket_index % crate::assertion_slots::MAX_ASSERTION_SLOTS,
274 );
275 break;
276 }
277 Err(actual) => current = actual,
278 }
279 }
280 }
281 }
282}
283
284#[must_use]
288pub fn each_bucket_read_all() -> Vec<EachBucket> {
289 let ptr = crate::context::EACH_BUCKET_PTR.with(std::cell::Cell::get);
290 if ptr.is_null() {
291 return Vec::new();
292 }
293 unsafe {
299 let count = (*ptr.cast::<()>().cast::<u32>()) as usize;
300 let count = count.min(MAX_EACH_BUCKETS);
301 let base = ptr.add(8).cast::<()>().cast::<EachBucket>();
302 (0..count).map(|i| std::ptr::read(base.add(i))).collect()
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_msg_hash_deterministic() {
312 let h1 = msg_hash("test_assertion");
313 let h2 = msg_hash("test_assertion");
314 assert_eq!(h1, h2);
315 }
316
317 #[test]
318 fn test_msg_hash_different_inputs() {
319 let h1 = msg_hash("alpha");
320 let h2 = msg_hash("beta");
321 let h3 = msg_hash("gamma");
322 assert_ne!(h1, h2);
323 assert_ne!(h2, h3);
324 assert_ne!(h1, h3);
325 }
326
327 #[test]
328 fn test_pack_unpack_quality_roundtrip() {
329 let quality = &[("health", 100i64), ("armor", 50i64), ("mana", 200i64)];
330 let packed = pack_quality(quality);
331 let unpacked = unpack_quality(packed, 3);
332 assert_eq!(unpacked, vec![100, 50, 200]);
333 }
334
335 #[test]
336 fn test_pack_quality_single() {
337 let quality = &[("health", 42i64)];
338 let packed = pack_quality(quality);
339 let unpacked = unpack_quality(packed, 1);
340 assert_eq!(unpacked, vec![42]);
341 }
342
343 #[test]
344 fn test_each_bucket_size_stable() {
345 assert_eq!(std::mem::size_of::<EachBucket>(), 104);
348 }
349
350 #[test]
351 fn test_each_bucket_read_all_when_inactive() {
352 let buckets = each_bucket_read_all();
354 assert!(buckets.is_empty());
355 }
356
357 #[test]
358 fn test_assertion_sometimes_each_noop_when_inactive() {
359 assertion_sometimes_each("test", &[("key", 1)], &[]);
361 }
362}