Skip to main content

tango_bench/
commpage.rs

1//! Shared memory commpage for lock-step synchronization.
2//!
3//! The commpage is a single shared memory region accessible to all three processes
4//! (Runner, Candidate, Baseline). It contains two cursors -- one per child -- used
5//! to synchronize lock-step execution. Measurement data flows via JSON-RPC responses,
6//! not through the commpage.
7
8use shared_memory::{Shmem, ShmemConf};
9use std::{
10    mem::size_of,
11    ops::{Deref, DerefMut},
12    sync::atomic::{AtomicU32, AtomicU64, Ordering},
13};
14use thiserror::Error;
15
16/// Magic bytes: "TANGOCMP" as a u64
17const MAGIC: u64 = 0x54414E47_4F434D50;
18
19/// Protocol version
20const VERSION: u32 = 4;
21
22/// Bit 63 of cursor -- set when the child exits the measurement loop
23const DONE_BIT: u64 = 1 << 63;
24
25/// Bit 0 of flags -- set by runner to signal early termination
26const STOP_FLAG: u32 = 1;
27
28/// The commpage memory layout: header + two cursors laid out contiguously.
29///
30/// ```text
31/// Offset  Size    Field
32/// 0x000   8       magic
33/// 0x008   4       version
34/// 0x00C   4       flags (AtomicU32)
35/// 0x010   8       cursor_c (AtomicU64)
36/// 0x018   8       cursor_b (AtomicU64)
37/// ```
38#[repr(C)]
39struct CommpageLayout {
40    magic: u64,
41    version: u32,
42    flags: AtomicU32,
43    /// Cursors are stored in independent cache lines to get rid of false-sharing
44    cursor_c: CacheLineAligned<Cursor>,
45    cursor_b: CacheLineAligned<Cursor>,
46}
47
48#[repr(C)]
49struct Cursor(AtomicU64);
50
51/// ## Apple Silicon Notes
52///
53/// Although Apple Silicon has 128-byte L2 cache-line, there is no evidence that false sharing is affecting
54/// performance if [`Cursor`] is aligned on 64-byte boundary. Probably it's due to following facts:
55///
56/// - L1 cache lines are still 64B aligned;
57/// - L2 is not a core private cache, but rather shared by the whole P/E-cluster.
58///
59/// see: Apple Silicon CPU Optimization Guide, sections 5.6.3 and 5.6.4
60#[repr(C, align(64))]
61struct CacheLineAligned<T>(T);
62
63impl<T> Deref for CacheLineAligned<T> {
64    type Target = T;
65
66    fn deref(&self) -> &Self::Target {
67        &self.0
68    }
69}
70
71impl<T> DerefMut for CacheLineAligned<T> {
72    fn deref_mut(&mut self) -> &mut Self::Target {
73        &mut self.0
74    }
75}
76
77impl Cursor {
78    fn set_value(&mut self, value: u64) {
79        self.0.store(value & !DONE_BIT, Ordering::Release);
80    }
81
82    fn load(&self) -> (bool, u64) {
83        let val = self.0.load(Ordering::Acquire);
84        (val & DONE_BIT != 0, val & !DONE_BIT)
85    }
86
87    fn set_done(&mut self) {
88        self.0.fetch_or(DONE_BIT, Ordering::AcqRel);
89    }
90
91    fn reset(&mut self) {
92        self.0.store(0, Ordering::Release);
93    }
94}
95
96impl CommpageLayout {
97    fn init(&mut self) {
98        self.magic = MAGIC;
99        self.version = VERSION;
100        *self.flags.get_mut() = 0;
101        self.cursor_c.reset();
102        self.cursor_b.reset();
103    }
104
105    fn validate(&self) -> Result<(), CommpageError> {
106        if self.magic != MAGIC {
107            return Err(CommpageError::BadMagic);
108        }
109        if self.version != VERSION {
110            return Err(CommpageError::BadVersion {
111                got: self.version,
112                expected: VERSION,
113            });
114        }
115        Ok(())
116    }
117
118    fn cursor(&self, role: Role) -> &Cursor {
119        match role {
120            Role::Candidate => &self.cursor_c,
121            Role::Baseline => &self.cursor_b,
122        }
123    }
124
125    fn cursor_mut(&mut self, role: Role) -> &mut Cursor {
126        match role {
127            Role::Candidate => &mut self.cursor_c,
128            Role::Baseline => &mut self.cursor_b,
129        }
130    }
131}
132
133/// Which role a child process plays.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
135#[serde(rename_all = "lowercase")]
136pub enum Role {
137    Candidate,
138    Baseline,
139}
140
141impl Role {
142    pub fn peer(self) -> Self {
143        match self {
144            Self::Candidate => Self::Baseline,
145            Self::Baseline => Self::Candidate,
146        }
147    }
148}
149
150/// Handle to a commpage backed by shared memory.
151pub struct Commpage {
152    shmem: Shmem,
153}
154
155impl Commpage {
156    /// Create a new commpage with a unique OS name. Initializes header and both cursors.
157    pub fn create() -> Result<Self, CommpageError> {
158        let shmem = ShmemConf::new()
159            .size(size_of::<CommpageLayout>())
160            .create()
161            .map_err(CommpageError::Shmem)?;
162
163        let layout = unsafe { &mut *(shmem.as_ptr() as *mut CommpageLayout) };
164        layout.init();
165
166        Ok(Commpage { shmem })
167    }
168
169    /// Open an existing commpage by its OS identifier. Validates magic and version.
170    pub fn open(name: &str) -> Result<Self, CommpageError> {
171        let shmem = ShmemConf::new()
172            .os_id(name)
173            .open()
174            .map_err(CommpageError::Shmem)?;
175
176        let layout = unsafe { &*(shmem.as_ptr() as *const CommpageLayout) };
177        layout.validate()?;
178
179        Ok(Commpage { shmem })
180    }
181
182    /// Get the OS identifier for this shared memory region.
183    pub fn os_id(&self) -> &str {
184        self.shmem.get_os_id()
185    }
186
187    fn layout(&self) -> &CommpageLayout {
188        unsafe { &*(self.shmem.as_ptr() as *const CommpageLayout) }
189    }
190
191    fn layout_mut(&mut self) -> &mut CommpageLayout {
192        unsafe { &mut *(self.shmem.as_ptr() as *mut CommpageLayout) }
193    }
194
195    /// Advance this role's cursor after completing a sample.
196    pub fn set_cursor_value(&mut self, role: Role, sample_no: u64) {
197        self.layout_mut().cursor_mut(role).set_value(sample_no);
198    }
199
200    /// Spin-wait until the peer's cursor reaches at least `target`, or the peer sets DONE.
201    ///
202    /// Returns `true` if the peer caught up, `false` if the peer exited early (DONE).
203    pub fn wait_for_cursor_value(&self, role: Role, target: u64) -> bool {
204        let cursor = self.layout().cursor(role);
205        loop {
206            let (stop, val) = cursor.load();
207            if stop {
208                return false;
209            }
210            if val >= target {
211                return true;
212            } else {
213                std::hint::spin_loop();
214            }
215        }
216    }
217
218    /// Mark this role's cursor as done.
219    pub fn mark_done(&mut self, role: Role) {
220        self.layout_mut().cursor_mut(role).set_done();
221    }
222
223    /// Check if this role's cursor has the DONE bit set.
224    pub fn is_done(&self, role: Role) -> bool {
225        let (done, _) = self.layout().cursor(role).load();
226        done
227    }
228
229    /// Either candidate or baseline has DONE bit set.
230    pub fn is_some_done(&self) -> bool {
231        self.is_done(Role::Candidate) || self.is_done(Role::Baseline)
232    }
233
234    /// Read the sample counter for a given role (cursor value without the DONE bit).
235    /// Sample counter is the number of a sample the children is collecting right now. 0 means
236    /// that measurement has not start yet.
237    pub fn load_cursor_value(&self, role: Role) -> usize {
238        let (_, value) = self.layout().cursor(role).load();
239        value as usize
240    }
241
242    /// Set the STOP flag. Called by runner to signal time-budget exhaustion.
243    pub fn set_stop(&mut self) {
244        self.layout().flags.fetch_or(STOP_FLAG, Ordering::Release);
245    }
246
247    /// Check if the STOP flag is set.
248    pub fn is_stopped(&self) -> bool {
249        self.layout().flags.load(Ordering::Acquire) & STOP_FLAG != 0
250    }
251
252    /// Reset the commpage for a new benchmark run.
253    pub fn reset(&mut self) {
254        self.layout_mut().flags.store(0, Ordering::Release);
255        self.layout_mut().cursor_c.reset();
256        self.layout_mut().cursor_b.reset();
257    }
258}
259
260#[derive(Debug, Error)]
261pub enum CommpageError {
262    #[error("shared memory error: {0}")]
263    Shmem(shared_memory::ShmemError),
264
265    #[error("invalid commpage magic number")]
266    BadMagic,
267
268    #[error("unsupported commpage version: {got} (expected {expected})")]
269    BadVersion { got: u32, expected: u32 },
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use std::thread;
276
277    #[test]
278    fn create_and_open() {
279        let cp = Commpage::create().unwrap();
280        let id = cp.os_id().to_string();
281        let cp2 = Commpage::open(&id).unwrap();
282        assert!(!cp2.is_stopped());
283    }
284
285    #[test]
286    fn stop_flag() {
287        let mut cp = Commpage::create().unwrap();
288        assert!(!cp.is_stopped());
289        cp.set_stop();
290        assert!(cp.is_stopped());
291    }
292
293    #[test]
294    fn done_bit() {
295        let mut cp = Commpage::create().unwrap();
296        assert!(!cp.is_done(Role::Candidate));
297        cp.set_cursor_value(Role::Candidate, 1);
298        cp.mark_done(Role::Candidate);
299
300        assert!(cp.is_done(Role::Candidate));
301        assert_eq!(cp.load_cursor_value(Role::Candidate), 1);
302    }
303
304    #[test]
305    fn peer_synchronization_two_threads() {
306        let mut cp = Commpage::create().unwrap();
307        let id = cp.os_id().to_string();
308
309        let num_samples = 64u64;
310
311        let handle = thread::spawn(move || {
312            let mut cp = Commpage::open(&id).unwrap();
313
314            for i in 0..num_samples {
315                cp.set_cursor_value(Role::Candidate, i);
316                cp.wait_for_cursor_value(Role::Baseline, i);
317            }
318            cp.mark_done(Role::Candidate);
319        });
320
321        for i in 0..num_samples {
322            cp.set_cursor_value(Role::Baseline, i);
323            cp.wait_for_cursor_value(Role::Candidate, i);
324        }
325        cp.mark_done(Role::Baseline);
326
327        handle.join().unwrap();
328
329        assert_eq!(
330            cp.load_cursor_value(Role::Baseline),
331            (num_samples - 1) as usize
332        );
333        assert_eq!(
334            cp.load_cursor_value(Role::Candidate),
335            (num_samples - 1) as usize
336        );
337    }
338}