1use std::fs::{File, OpenOptions};
57use std::mem::size_of;
58use std::path::Path;
59use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
60
61use memmap2::{MmapMut, MmapOptions};
62
63pub const TOPOLOGY_MAGIC: u64 = 0x4150_544F_504F_3031;
64
65pub const DEFAULT_FAN_OUT_THRESHOLD: u32 = 3;
68pub const DEFAULT_FAN_IN_THRESHOLD: u32 = 3;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[repr(u32)]
73pub enum TopologyKind {
74 PointToPoint = 0,
75 BroadcastTree = 1,
76 AllToAllMesh = 2,
77}
78
79impl TopologyKind {
80 pub fn from_u32(v: u32) -> Self {
81 match v {
82 1 => Self::BroadcastTree,
83 2 => Self::AllToAllMesh,
84 _ => Self::PointToPoint,
85 }
86 }
87}
88
89#[repr(C, align(64))]
90pub struct TopologyHeader {
91 pub magic: u64,
92 pub n_nodes: u32,
93 pub fan_out_threshold: AtomicU32,
94 pub fan_in_threshold: AtomicU32,
95 _pad1: u32,
96 pub total_msgs: AtomicU64,
97 pub recommendation: AtomicU32,
98 pub recommendation_epoch: AtomicU64,
99 pub broadcast_root: AtomicU32,
100 _pad2: u32,
101 _pad3: [u8; 8],
102}
103
104const _: () = {
105 assert!(size_of::<TopologyHeader>() == 64);
106};
107
108pub const fn topology_file_size(n_nodes: usize) -> usize {
109 size_of::<TopologyHeader>() + n_nodes * n_nodes * size_of::<AtomicU64>()
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum TopologyError {
114 NodeIndexOutOfBounds,
115 LayoutMismatch,
116 IoError(std::io::ErrorKind),
117}
118
119impl From<std::io::Error> for TopologyError {
120 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct TopologyStats {
125 pub total_msgs: u64,
126 pub max_fan_out: u32,
127 pub max_fan_in: u32,
128 pub max_fan_out_src: u32,
129 pub max_fan_in_dst: u32,
130 pub current_recommendation: TopologyKind,
131 pub recommendation_epoch: u64,
132}
133
134pub struct SharedTopologyMap {
135 _file: File,
136 mmap: MmapMut,
137 n_nodes: usize,
138 header_sidecar: subetha_core::HandshakeHeader,
139 ring_sidecar: Box<subetha_core::ObservationRing>,
140}
141
142unsafe impl Send for SharedTopologyMap {}
143unsafe impl Sync for SharedTopologyMap {}
144
145impl subetha_sidecar::AdaptiveInstance for SharedTopologyMap {
146 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
147 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
148 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
149 Box::new(subetha_sidecar::NoMigrationPolicy)
150 }
151}
152
153impl SharedTopologyMap {
154 pub fn create(
155 path: impl AsRef<Path>,
156 n_nodes: usize,
157 ) -> Result<Self, TopologyError> {
158 Self::create_with_thresholds(
159 path, n_nodes,
160 DEFAULT_FAN_OUT_THRESHOLD, DEFAULT_FAN_IN_THRESHOLD,
161 )
162 }
163
164 pub fn create_with_thresholds(
171 path: impl AsRef<Path>,
172 n_nodes: usize,
173 fan_out_threshold: u32,
174 fan_in_threshold: u32,
175 ) -> Result<Self, TopologyError> {
176 assert!(n_nodes >= 1);
177 assert!(n_nodes <= u32::MAX as usize);
178 let (file, mmap) = crate::mmf_attach::create_or_attach(
179 path.as_ref(),
180 topology_file_size(n_nodes),
181 |ptr| unsafe {
182 Self::init_region(ptr, n_nodes, fan_out_threshold, fan_in_threshold)
183 },
184 |ptr| unsafe { (*(ptr as *const TopologyHeader)).magic == TOPOLOGY_MAGIC },
185 )?;
186 Self::from_region(file, mmap, n_nodes)
187 }
188
189 pub fn reset(
193 path: impl AsRef<Path>,
194 n_nodes: usize,
195 fan_out_threshold: u32,
196 fan_in_threshold: u32,
197 ) -> Result<Self, TopologyError> {
198 assert!(n_nodes >= 1);
199 assert!(n_nodes <= u32::MAX as usize);
200 let (file, mmap) = crate::mmf_attach::reset(
201 path.as_ref(),
202 topology_file_size(n_nodes),
203 |ptr| unsafe {
204 Self::init_region(ptr, n_nodes, fan_out_threshold, fan_in_threshold)
205 },
206 )?;
207 Self::from_region(file, mmap, n_nodes)
208 }
209
210 unsafe fn init_region(
218 ptr: *mut u8,
219 n_nodes: usize,
220 fan_out_threshold: u32,
221 fan_in_threshold: u32,
222 ) {
223 let hdr = ptr as *mut TopologyHeader;
224 unsafe {
225 (*hdr).n_nodes = n_nodes as u32;
226 std::ptr::write(
227 &raw mut (*hdr).fan_out_threshold,
228 AtomicU32::new(fan_out_threshold),
229 );
230 std::ptr::write(
231 &raw mut (*hdr).fan_in_threshold,
232 AtomicU32::new(fan_in_threshold),
233 );
234 std::ptr::write(
235 &raw mut (*hdr).recommendation,
236 AtomicU32::new(TopologyKind::PointToPoint as u32),
237 );
238 std::ptr::write_volatile(&raw mut (*hdr).magic, TOPOLOGY_MAGIC);
239 }
240 }
241
242 fn from_region(
245 file: File,
246 mmap: MmapMut,
247 n_nodes: usize,
248 ) -> Result<Self, TopologyError> {
249 let hdr = unsafe { &*(mmap.as_ptr() as *const TopologyHeader) };
250 if hdr.magic != TOPOLOGY_MAGIC || hdr.n_nodes != n_nodes as u32 {
251 return Err(TopologyError::LayoutMismatch);
252 }
253 Ok(Self {
254 _file: file, mmap, n_nodes,
255 header_sidecar: subetha_core::HandshakeHeader::new(),
256 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
257 })
258 }
259
260 pub fn open(
261 path: impl AsRef<Path>,
262 expected_n_nodes: usize,
263 ) -> Result<Self, TopologyError> {
264 let total = topology_file_size(expected_n_nodes);
265 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
266 if file.metadata()?.len() < total as u64 {
267 return Err(TopologyError::LayoutMismatch);
268 }
269 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
270 Self::from_region(file, mmap, expected_n_nodes)
271 }
272
273 #[inline]
274 pub fn n_nodes(&self) -> usize { self.n_nodes }
275
276 fn header(&self) -> &TopologyHeader {
277 unsafe { &*(self.mmap.as_ptr() as *const TopologyHeader) }
278 }
279
280 fn edge(&self, src: u32, dst: u32) -> &AtomicU64 {
281 let idx = (src as usize) * self.n_nodes + (dst as usize);
282 let base = unsafe { self.mmap.as_ptr().add(size_of::<TopologyHeader>()) };
283 unsafe { &*(base.add(idx * size_of::<AtomicU64>()) as *const AtomicU64) }
284 }
285
286 pub fn record_send(&self, src: u32, dst: u32) -> Result<u64, TopologyError> {
289 if src as usize >= self.n_nodes || dst as usize >= self.n_nodes {
290 self.ring_sidecar
291 .push_op(crate::sidecar_ops::topology::OP_RECORD, 1);
292 return Err(TopologyError::NodeIndexOutOfBounds);
293 }
294 let prev = self.edge(src, dst).fetch_add(1, Ordering::AcqRel);
295 self.header().total_msgs.fetch_add(1, Ordering::AcqRel);
296 self.ring_sidecar
297 .push_op(crate::sidecar_ops::topology::OP_RECORD, 0);
298 Ok(prev + 1)
299 }
300
301 pub fn fan_out(&self, src: u32) -> u32 {
303 if src as usize >= self.n_nodes {
304 self.ring_sidecar
305 .push_op(crate::sidecar_ops::topology::OP_FAN_OUT, 1);
306 return 0;
307 }
308 let mut count = 0u32;
309 for d in 0..self.n_nodes as u32 {
310 if self.edge(src, d).load(Ordering::Acquire) > 0 {
311 count += 1;
312 }
313 }
314 self.ring_sidecar
315 .push_op(crate::sidecar_ops::topology::OP_FAN_OUT, 0);
316 count
317 }
318
319 pub fn fan_in(&self, dst: u32) -> u32 {
321 if dst as usize >= self.n_nodes {
322 self.ring_sidecar
323 .push_op(crate::sidecar_ops::topology::OP_FAN_IN, 1);
324 return 0;
325 }
326 let mut count = 0u32;
327 for s in 0..self.n_nodes as u32 {
328 if self.edge(s, dst).load(Ordering::Acquire) > 0 {
329 count += 1;
330 }
331 }
332 self.ring_sidecar
333 .push_op(crate::sidecar_ops::topology::OP_FAN_IN, 0);
334 count
335 }
336
337 pub fn max_fan_out(&self) -> (u32, u32) {
339 let mut max = 0u32;
340 let mut who = 0u32;
341 for s in 0..self.n_nodes as u32 {
342 let fo = self.fan_out(s);
343 if fo > max { max = fo; who = s; }
344 }
345 (max, who)
346 }
347
348 pub fn max_fan_in(&self) -> (u32, u32) {
350 let mut max = 0u32;
351 let mut who = 0u32;
352 for d in 0..self.n_nodes as u32 {
353 let fi = self.fan_in(d);
354 if fi > max { max = fi; who = d; }
355 }
356 (max, who)
357 }
358
359 pub fn recommend(&self) -> TopologyKind {
364 let (max_fan_out, _) = self.max_fan_out();
365 let (max_fan_in, _) = self.max_fan_in();
366 let fo_threshold = self.header().fan_out_threshold.load(Ordering::Acquire);
367 let fi_threshold = self.header().fan_in_threshold.load(Ordering::Acquire);
368 let kind = if max_fan_out >= fo_threshold && max_fan_in >= fi_threshold {
369 TopologyKind::AllToAllMesh
370 } else if max_fan_out >= fo_threshold {
371 TopologyKind::BroadcastTree
372 } else {
373 TopologyKind::PointToPoint
374 };
375 self.ring_sidecar
376 .push_op(crate::sidecar_ops::topology::OP_RECOMMEND, 0);
377 kind
378 }
379
380 pub fn publish_recommendation(&self) -> TopologyKind {
385 let kind = self.recommend();
386 let hdr = self.header();
387 hdr.recommendation.store(kind as u32, Ordering::Release);
388 hdr.recommendation_epoch.fetch_add(1, Ordering::Release);
389 if kind == TopologyKind::BroadcastTree {
392 let (_, root) = self.max_fan_out();
393 hdr.broadcast_root.store(root, Ordering::Release);
394 }
395 kind
396 }
397
398 pub fn read_recommendation(&self) -> TopologyKind {
400 TopologyKind::from_u32(
401 self.header().recommendation.load(Ordering::Acquire)
402 )
403 }
404
405 pub fn broadcast_root(&self) -> u32 {
409 self.header().broadcast_root.load(Ordering::Acquire)
410 }
411
412 pub fn recommendation_epoch(&self) -> u64 {
416 self.header().recommendation_epoch.load(Ordering::Acquire)
417 }
418
419 pub fn total_msgs(&self) -> u64 {
421 self.header().total_msgs.load(Ordering::Acquire)
422 }
423
424 pub fn stats(&self) -> TopologyStats {
426 let (max_fan_out, max_fan_out_src) = self.max_fan_out();
427 let (max_fan_in, max_fan_in_dst) = self.max_fan_in();
428 TopologyStats {
429 total_msgs: self.total_msgs(),
430 max_fan_out, max_fan_out_src,
431 max_fan_in, max_fan_in_dst,
432 current_recommendation: self.read_recommendation(),
433 recommendation_epoch: self.recommendation_epoch(),
434 }
435 }
436
437 pub fn reset_observations(&self) {
442 for s in 0..self.n_nodes as u32 {
443 for d in 0..self.n_nodes as u32 {
444 self.edge(s, d).store(0, Ordering::Release);
445 }
446 }
447 self.header().total_msgs.store(0, Ordering::Release);
448 }
449
450 pub fn set_thresholds(&self, fan_out: u32, fan_in: u32) {
453 self.header().fan_out_threshold.store(fan_out, Ordering::Release);
454 self.header().fan_in_threshold.store(fan_in, Ordering::Release);
455 }
456
457 pub fn flush(&self) -> Result<(), TopologyError> {
458 self.mmap.flush()?;
459 Ok(())
460 }
461
462 pub fn flush_async(&self) -> Result<(), TopologyError> {
466 self.mmap.flush_async()?;
467 Ok(())
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use std::sync::Arc;
475 use std::thread;
476
477 fn tmp(name: &str) -> std::path::PathBuf {
478 let mut p = std::env::temp_dir();
479 let pid = std::process::id();
480 p.push(format!("subetha-topology-{name}-{pid}.bin"));
481 p
482 }
483
484 #[test]
487 fn second_create_attaches_and_keeps_edges() {
488 let p = tmp("attach");
489 std::fs::remove_file(&p).ok();
490 let t = SharedTopologyMap::create(&p, 4).unwrap();
491 t.record_send(0, 1).unwrap();
492
493 let t2 = SharedTopologyMap::create(&p, 4).unwrap();
494 assert_eq!(t2.total_msgs(), 1, "attach zeroed observed edges");
495 assert!(matches!(
496 SharedTopologyMap::create(&p, 2),
497 Err(TopologyError::LayoutMismatch),
498 ));
499
500 drop(t);
503 drop(t2);
504 let fresh = SharedTopologyMap::reset(
505 &p, 4, DEFAULT_FAN_OUT_THRESHOLD, DEFAULT_FAN_IN_THRESHOLD,
506 ).unwrap();
507 assert_eq!(fresh.total_msgs(), 0, "reset kept an edge");
508 drop(fresh);
509 std::fs::remove_file(&p).ok();
510 }
511
512 #[test]
513 fn create_initial_state() {
514 let p = tmp("init");
515 let t = SharedTopologyMap::create(&p, 4).unwrap();
516 assert_eq!(t.n_nodes(), 4);
517 assert_eq!(t.total_msgs(), 0);
518 assert_eq!(t.max_fan_out(), (0, 0));
519 assert_eq!(t.max_fan_in(), (0, 0));
520 assert_eq!(t.recommend(), TopologyKind::PointToPoint);
521 std::fs::remove_file(&p).ok();
522 }
523
524 #[test]
525 fn record_send_increments_edge_and_total() {
526 let p = tmp("record");
527 let t = SharedTopologyMap::create(&p, 4).unwrap();
528 let new_count = t.record_send(0, 1).unwrap();
529 assert_eq!(new_count, 1);
530 assert_eq!(t.total_msgs(), 1);
531 t.record_send(0, 1).unwrap();
532 assert_eq!(t.total_msgs(), 2);
533 std::fs::remove_file(&p).ok();
534 }
535
536 #[test]
537 fn fan_out_in_count_distinct_edges() {
538 let p = tmp("fan");
539 let t = SharedTopologyMap::create(&p, 4).unwrap();
540 t.record_send(0, 1).unwrap();
542 t.record_send(0, 2).unwrap();
543 t.record_send(0, 3).unwrap();
544 assert_eq!(t.fan_out(0), 3);
545 assert_eq!(t.fan_in(0), 0);
546 t.record_send(0, 1).unwrap();
548 t.record_send(0, 1).unwrap();
549 assert_eq!(t.fan_out(0), 3);
550 assert_eq!(t.fan_in(2), 1);
552 std::fs::remove_file(&p).ok();
553 }
554
555 #[test]
556 fn out_of_bounds_record_returns_error() {
557 let p = tmp("oob");
558 let t = SharedTopologyMap::create(&p, 4).unwrap();
559 assert_eq!(t.record_send(4, 0).err(), Some(TopologyError::NodeIndexOutOfBounds));
560 assert_eq!(t.record_send(0, 99).err(), Some(TopologyError::NodeIndexOutOfBounds));
561 std::fs::remove_file(&p).ok();
562 }
563
564 #[test]
565 fn recommend_point_to_point_when_fan_low() {
566 let p = tmp("rec-p2p");
567 let t = SharedTopologyMap::create(&p, 4).unwrap();
568 for _ in 0..100 { t.record_send(0, 1).unwrap(); }
570 assert_eq!(t.recommend(), TopologyKind::PointToPoint);
571 std::fs::remove_file(&p).ok();
572 }
573
574 #[test]
575 fn recommend_broadcast_when_fan_out_high_only() {
576 let p = tmp("rec-bcast");
577 let t = SharedTopologyMap::create(&p, 5).unwrap();
578 for d in 1..5 { t.record_send(0, d).unwrap(); }
580 assert_eq!(t.fan_out(0), 4);
581 assert_eq!(t.max_fan_in(), (1, 1)); assert_eq!(t.recommend(), TopologyKind::BroadcastTree);
583 std::fs::remove_file(&p).ok();
584 }
585
586 #[test]
587 fn recommend_all_to_all_when_both_high() {
588 let p = tmp("rec-mesh");
589 let t = SharedTopologyMap::create(&p, 5).unwrap();
590 for s in 0..5u32 {
592 for d in 0..5u32 {
593 if s != d { t.record_send(s, d).unwrap(); }
594 }
595 }
596 assert_eq!(t.max_fan_out().0, 4);
597 assert_eq!(t.max_fan_in().0, 4);
598 assert_eq!(t.recommend(), TopologyKind::AllToAllMesh);
599 std::fs::remove_file(&p).ok();
600 }
601
602 #[test]
603 fn publish_recommendation_caches_for_o1_reads() {
604 let p = tmp("publish");
605 let t = SharedTopologyMap::create(&p, 5).unwrap();
606 for d in 1..5 { t.record_send(0, d).unwrap(); }
607 assert_eq!(t.recommendation_epoch(), 0);
608 let published = t.publish_recommendation();
609 assert_eq!(published, TopologyKind::BroadcastTree);
610 assert_eq!(t.read_recommendation(), TopologyKind::BroadcastTree);
611 assert_eq!(t.recommendation_epoch(), 1);
612 assert_eq!(t.broadcast_root(), 0);
614 std::fs::remove_file(&p).ok();
615 }
616
617 #[test]
618 fn cross_handle_observation_visible() {
619 let p = tmp("cross-handle");
620 let writer = SharedTopologyMap::create(&p, 4).unwrap();
621 let observer = SharedTopologyMap::open(&p, 4).unwrap();
622 writer.record_send(0, 1).unwrap();
623 writer.record_send(0, 2).unwrap();
624 writer.record_send(0, 3).unwrap();
625 assert_eq!(observer.fan_out(0), 3);
626 writer.publish_recommendation();
627 assert_eq!(observer.read_recommendation(), TopologyKind::BroadcastTree);
628 std::fs::remove_file(&p).ok();
629 }
630
631 #[test]
632 fn reset_observations_clears_all_edges() {
633 let p = tmp("reset");
634 let t = SharedTopologyMap::create(&p, 4).unwrap();
635 for s in 0..4u32 {
636 for d in 0..4u32 {
637 t.record_send(s, d).unwrap();
638 }
639 }
640 assert_eq!(t.total_msgs(), 16);
641 t.reset_observations();
642 assert_eq!(t.total_msgs(), 0);
643 assert_eq!(t.fan_out(0), 0);
644 assert_eq!(t.fan_in(0), 0);
645 std::fs::remove_file(&p).ok();
646 }
647
648 #[test]
649 fn concurrent_record_sends_count_correctly() {
650 let p = tmp("concurrent");
651 let t = Arc::new(SharedTopologyMap::create(&p, 4).unwrap());
652 let n_threads = 4;
653 let per_thread = 100;
654 let mut handles = vec![];
655 for src in 0..n_threads as u32 {
656 let t = t.clone();
657 handles.push(thread::spawn(move || {
658 for _ in 0..per_thread {
659 for dst in 0..4u32 {
660 if src != dst { t.record_send(src, dst).unwrap(); }
661 }
662 }
663 }));
664 }
665 for h in handles { h.join().unwrap(); }
666 assert_eq!(t.total_msgs(), (n_threads * 3 * per_thread) as u64);
668 for s in 0..n_threads as u32 {
670 assert_eq!(t.fan_out(s), 3, "src {s} should have fan_out 3");
671 }
672 assert_eq!(t.recommend(), TopologyKind::AllToAllMesh);
674 std::fs::remove_file(&p).ok();
675 }
676
677 #[test]
678 fn set_thresholds_adjusts_recommendation_policy() {
679 let p = tmp("thresholds");
680 let t = SharedTopologyMap::create(&p, 5).unwrap();
681 t.record_send(0, 1).unwrap();
683 t.record_send(0, 2).unwrap();
684 assert_eq!(t.recommend(), TopologyKind::PointToPoint);
685 t.set_thresholds(2, 3);
687 assert_eq!(t.recommend(), TopologyKind::BroadcastTree);
688 std::fs::remove_file(&p).ok();
689 }
690
691 #[test]
692 fn stats_snapshot_returns_full_picture() {
693 let p = tmp("stats");
694 let t = SharedTopologyMap::create(&p, 5).unwrap();
695 for d in 1..5u32 { t.record_send(0, d).unwrap(); }
696 t.publish_recommendation();
697 let s = t.stats();
698 assert_eq!(s.total_msgs, 4);
699 assert_eq!(s.max_fan_out, 4);
700 assert_eq!(s.max_fan_out_src, 0);
701 assert_eq!(s.max_fan_in, 1);
702 assert_eq!(s.current_recommendation, TopologyKind::BroadcastTree);
703 assert_eq!(s.recommendation_epoch, 1);
704 std::fs::remove_file(&p).ok();
705 }
706
707 #[test]
708 fn topology_kind_from_u32_round_trip() {
709 assert_eq!(TopologyKind::from_u32(0), TopologyKind::PointToPoint);
710 assert_eq!(TopologyKind::from_u32(1), TopologyKind::BroadcastTree);
711 assert_eq!(TopologyKind::from_u32(2), TopologyKind::AllToAllMesh);
712 assert_eq!(TopologyKind::from_u32(999), TopologyKind::PointToPoint);
714 }
715
716 #[test]
717 fn disk_persistence_survives_reopen() {
718 let p = tmp("disk");
719 {
720 let t = SharedTopologyMap::create(&p, 4).unwrap();
721 for d in 1..4u32 { t.record_send(0, d).unwrap(); }
722 t.publish_recommendation();
723 t.flush().unwrap();
724 }
725 let t2 = SharedTopologyMap::open(&p, 4).unwrap();
726 assert_eq!(t2.total_msgs(), 3);
727 assert_eq!(t2.fan_out(0), 3);
728 assert_eq!(t2.read_recommendation(), TopologyKind::BroadcastTree);
729 std::fs::remove_file(&p).ok();
730 }
731
732 #[test]
733 fn recommendation_demotes_when_observations_drop() {
734 let p = tmp("demote");
735 let t = SharedTopologyMap::create(&p, 5).unwrap();
736 for d in 1..5u32 { t.record_send(0, d).unwrap(); }
738 assert_eq!(t.recommend(), TopologyKind::BroadcastTree);
739 t.reset_observations();
741 for _ in 0..10 { t.record_send(0, 1).unwrap(); }
742 assert_eq!(t.recommend(), TopologyKind::PointToPoint);
743 std::fs::remove_file(&p).ok();
744 }
745}