1#![allow(missing_docs)]
8
9use crate::delta::TileVertexId;
10use crate::evidence::LogEValue;
11use core::mem::size_of;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[repr(u8)]
16pub enum TileStatus {
17 Idle = 0,
19 Processing = 1,
21 Complete = 2,
23 Error = 3,
25 Waiting = 4,
27 Checkpointing = 5,
29 Recovering = 6,
31 Shutdown = 7,
33}
34
35impl From<u8> for TileStatus {
36 fn from(v: u8) -> Self {
37 match v {
38 0 => TileStatus::Idle,
39 1 => TileStatus::Processing,
40 2 => TileStatus::Complete,
41 3 => TileStatus::Error,
42 4 => TileStatus::Waiting,
43 5 => TileStatus::Checkpointing,
44 6 => TileStatus::Recovering,
45 7 => TileStatus::Shutdown,
46 _ => TileStatus::Error,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, Default)]
56#[repr(C, align(8))]
57pub struct WitnessFragment {
58 pub seed: TileVertexId,
60 pub boundary_size: u16,
62 pub cardinality: u16,
64 pub hash: u16,
66 pub local_min_cut: u16,
68 pub component: u16,
70 pub _reserved: u16,
72}
73
74impl WitnessFragment {
75 #[inline]
77 pub const fn new(
78 seed: TileVertexId,
79 boundary_size: u16,
80 cardinality: u16,
81 local_min_cut: u16,
82 ) -> Self {
83 Self {
84 seed,
85 boundary_size,
86 cardinality,
87 hash: 0,
88 local_min_cut,
89 component: 0,
90 _reserved: 0,
91 }
92 }
93
94 pub fn compute_hash(&mut self) {
96 let mut h = self.seed as u32;
97 h = h.wrapping_mul(31).wrapping_add(self.boundary_size as u32);
98 h = h.wrapping_mul(31).wrapping_add(self.cardinality as u32);
99 h = h.wrapping_mul(31).wrapping_add(self.local_min_cut as u32);
100 self.hash = (h & 0xFFFF) as u16;
101 }
102
103 #[inline]
105 pub const fn is_empty(&self) -> bool {
106 self.cardinality == 0
107 }
108}
109
110#[derive(Debug, Clone, Copy)]
115#[repr(C, align(64))]
116pub struct TileReport {
117 pub tile_id: u8,
120 pub status: TileStatus,
122 pub generation: u16,
124 pub tick: u32,
126
127 pub num_vertices: u16,
130 pub num_edges: u16,
132 pub num_components: u16,
134 pub graph_flags: u16,
136
137 pub log_e_value: LogEValue,
140 pub obs_count: u16,
142 pub rejected_count: u16,
144
145 pub witness: WitnessFragment,
148
149 pub delta_time_us: u16,
152 pub tick_time_us: u16,
154 pub deltas_processed: u16,
156 pub memory_kb: u16,
158
159 pub ghost_vertices: u16,
162 pub ghost_edges: u16,
164 pub boundary_vertices: u16,
166 pub pending_sync: u16,
168
169 pub _reserved: [u8; 8],
172}
173
174impl Default for TileReport {
175 fn default() -> Self {
176 Self::new(0)
177 }
178}
179
180impl TileReport {
181 pub const GRAPH_CONNECTED: u16 = 0x0001;
183 pub const GRAPH_DIRTY: u16 = 0x0002;
185 pub const GRAPH_FULL: u16 = 0x0004;
187 pub const GRAPH_HAS_GHOSTS: u16 = 0x0008;
189
190 #[inline]
192 pub const fn new(tile_id: u8) -> Self {
193 Self {
194 tile_id,
195 status: TileStatus::Idle,
196 generation: 0,
197 tick: 0,
198 num_vertices: 0,
199 num_edges: 0,
200 num_components: 0,
201 graph_flags: 0,
202 log_e_value: 0,
203 obs_count: 0,
204 rejected_count: 0,
205 witness: WitnessFragment {
206 seed: 0,
207 boundary_size: 0,
208 cardinality: 0,
209 hash: 0,
210 local_min_cut: 0,
211 component: 0,
212 _reserved: 0,
213 },
214 delta_time_us: 0,
215 tick_time_us: 0,
216 deltas_processed: 0,
217 memory_kb: 0,
218 ghost_vertices: 0,
219 ghost_edges: 0,
220 boundary_vertices: 0,
221 pending_sync: 0,
222 _reserved: [0; 8],
223 }
224 }
225
226 #[inline]
228 pub fn set_complete(&mut self) {
229 self.status = TileStatus::Complete;
230 }
231
232 #[inline]
234 pub fn set_error(&mut self) {
235 self.status = TileStatus::Error;
236 }
237
238 #[inline]
240 pub fn set_connected(&mut self, connected: bool) {
241 if connected {
242 self.graph_flags |= Self::GRAPH_CONNECTED;
243 } else {
244 self.graph_flags &= !Self::GRAPH_CONNECTED;
245 }
246 }
247
248 #[inline]
250 pub const fn is_connected(&self) -> bool {
251 self.graph_flags & Self::GRAPH_CONNECTED != 0
252 }
253
254 #[inline]
256 pub const fn is_dirty(&self) -> bool {
257 self.graph_flags & Self::GRAPH_DIRTY != 0
258 }
259
260 pub fn e_value_approx(&self) -> f32 {
262 let log2_val = (self.log_e_value as f32) / 65536.0;
263 libm::exp2f(log2_val)
264 }
265
266 pub fn set_witness(&mut self, witness: WitnessFragment) {
268 self.witness = witness;
269 }
270
271 #[inline]
273 pub const fn get_witness(&self) -> &WitnessFragment {
274 &self.witness
275 }
276
277 #[inline]
279 pub const fn has_rejections(&self) -> bool {
280 self.rejected_count > 0
281 }
282
283 pub fn processing_rate(&self) -> f32 {
285 if self.tick_time_us == 0 {
286 0.0
287 } else {
288 (self.deltas_processed as f32) / (self.tick_time_us as f32)
289 }
290 }
291}
292
293#[derive(Debug, Clone, Copy, Default)]
295#[repr(C)]
296pub struct AggregatedReport {
297 pub total_vertices: u32,
299 pub total_edges: u32,
301 pub total_components: u16,
303 pub tiles_reporting: u16,
305 pub tiles_with_errors: u16,
307 pub tiles_with_rejections: u16,
309 pub global_log_e: i64,
311 pub global_min_cut: u16,
313 pub min_cut_tile: u8,
315 pub _reserved: u8,
317 pub total_time_us: u32,
319 pub tick: u32,
321}
322
323impl AggregatedReport {
324 pub const fn new(tick: u32) -> Self {
326 Self {
327 total_vertices: 0,
328 total_edges: 0,
329 total_components: 0,
330 tiles_reporting: 0,
331 tiles_with_errors: 0,
332 tiles_with_rejections: 0,
333 global_log_e: 0,
334 global_min_cut: u16::MAX,
335 min_cut_tile: 0,
336 _reserved: 0,
337 total_time_us: 0,
338 tick,
339 }
340 }
341
342 pub fn merge(&mut self, report: &TileReport) {
344 self.total_vertices += report.num_vertices as u32;
345 self.total_edges += report.num_edges as u32;
346 self.total_components += report.num_components;
347 self.tiles_reporting += 1;
348
349 if report.status == TileStatus::Error {
350 self.tiles_with_errors += 1;
351 }
352
353 if report.rejected_count > 0 {
354 self.tiles_with_rejections += 1;
355 }
356
357 self.global_log_e += report.log_e_value as i64;
358
359 if report.witness.local_min_cut < self.global_min_cut {
360 self.global_min_cut = report.witness.local_min_cut;
361 self.min_cut_tile = report.tile_id;
362 }
363
364 self.total_time_us = self.total_time_us.max(report.tick_time_us as u32);
365 }
366
367 pub fn all_complete(&self, expected_tiles: u16) -> bool {
369 self.tiles_reporting == expected_tiles && self.tiles_with_errors == 0
370 }
371
372 pub fn global_e_value(&self) -> f64 {
374 let log2_val = (self.global_log_e as f64) / 65536.0;
375 libm::exp2(log2_val)
376 }
377}
378
379const _: () = assert!(
381 size_of::<TileReport>() == 64,
382 "TileReport must be exactly 64 bytes"
383);
384const _: () = assert!(
385 size_of::<WitnessFragment>() == 16,
386 "WitnessFragment must be 16 bytes"
387);
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_tile_report_size() {
395 assert_eq!(size_of::<TileReport>(), 64);
396 }
397
398 #[test]
399 fn test_tile_report_alignment() {
400 assert_eq!(core::mem::align_of::<TileReport>(), 64);
401 }
402
403 #[test]
404 fn test_witness_fragment_size() {
405 assert_eq!(size_of::<WitnessFragment>(), 16);
406 }
407
408 #[test]
409 fn test_new_report() {
410 let report = TileReport::new(5);
411 assert_eq!(report.tile_id, 5);
412 assert_eq!(report.status, TileStatus::Idle);
413 assert_eq!(report.tick, 0);
414 }
415
416 #[test]
417 fn test_set_status() {
418 let mut report = TileReport::new(0);
419 report.set_complete();
420 assert_eq!(report.status, TileStatus::Complete);
421
422 report.set_error();
423 assert_eq!(report.status, TileStatus::Error);
424 }
425
426 #[test]
427 fn test_connected_flag() {
428 let mut report = TileReport::new(0);
429 assert!(!report.is_connected());
430
431 report.set_connected(true);
432 assert!(report.is_connected());
433
434 report.set_connected(false);
435 assert!(!report.is_connected());
436 }
437
438 #[test]
439 fn test_witness_fragment() {
440 let mut frag = WitnessFragment::new(10, 5, 20, 100);
441 assert_eq!(frag.seed, 10);
442 assert_eq!(frag.boundary_size, 5);
443 assert_eq!(frag.cardinality, 20);
444 assert_eq!(frag.local_min_cut, 100);
445
446 frag.compute_hash();
447 assert_ne!(frag.hash, 0);
448 }
449
450 #[test]
451 fn test_aggregated_report() {
452 let mut agg = AggregatedReport::new(1);
453
454 let mut report1 = TileReport::new(0);
455 report1.num_vertices = 50;
456 report1.num_edges = 100;
457 report1.witness.local_min_cut = 200;
458
459 let mut report2 = TileReport::new(1);
460 report2.num_vertices = 75;
461 report2.num_edges = 150;
462 report2.witness.local_min_cut = 150;
463
464 agg.merge(&report1);
465 agg.merge(&report2);
466
467 assert_eq!(agg.tiles_reporting, 2);
468 assert_eq!(agg.total_vertices, 125);
469 assert_eq!(agg.total_edges, 250);
470 assert_eq!(agg.global_min_cut, 150);
471 assert_eq!(agg.min_cut_tile, 1);
472 }
473
474 #[test]
475 fn test_tile_status_roundtrip() {
476 for i in 0..=7 {
477 let status = TileStatus::from(i);
478 assert_eq!(status as u8, i);
479 }
480 }
481
482 #[test]
483 fn test_processing_rate() {
484 let mut report = TileReport::new(0);
485 report.deltas_processed = 100;
486 report.tick_time_us = 50;
487
488 assert!((report.processing_rate() - 2.0).abs() < 0.01);
489 }
490}