Skip to main content

ghostlink_core/
ring.rs

1//! Zero-Copy SPSC Ring Buffer with AF_XDP Integration
2//!
3//! This implementation uses pinned allocations and proper memory ordering
4//! for single-producer/single-consumer DMA-style hand-off.
5//!
6//! Optimizations:
7//! - 4x larger capacity (4096) for better batching
8//! - Prefetching in hot paths for cache-friendly access
9//! - Optimal cache-line padding (128 bytes) to avoid false sharing
10
11use std::cell::UnsafeCell;
12use std::mem::MaybeUninit;
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15/// Ring buffer configuration with backpressure thresholds
16#[derive(Clone, Copy, Debug)]
17pub struct RingConfig {
18    /// Maximum number of elements the ring can hold
19    pub capacity: usize,
20    /// Backpressure threshold at 70% capacity
21    pub backpressure_threshold: usize,
22}
23
24impl Default for RingConfig {
25    fn default() -> Self {
26        Self {
27            capacity: 4095,               // 4x larger for better batching
28            backpressure_threshold: 2867, // ~70% of 4095
29        }
30    }
31}
32
33/// Fixed capacity used for DMA alignment (power of 2 for fast modulo)
34const RING_CAPACITY: usize = 4096;
35const CACHE_LINE: usize = 128; // 128-byte cache lines on modern x86/ARM
36
37/// Prefetch hint for read access (equivalent to `prefetcht0` on x86)
38#[inline(always)]
39#[allow(dead_code)]
40fn prefetch_read<T>(ptr: *const T) {
41    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
42    unsafe {
43        std::arch::x86_64::_mm_prefetch(ptr as *const i8, std::arch::x86_64::_MM_HINT_T0);
44    }
45    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
46    {
47        // On other platforms, hint to compiler/CPU
48        std::hint::black_box(ptr);
49    }
50}
51
52/// Prefetch hint for write access (equivalent to `prefetchw` on x86)
53#[inline(always)]
54#[allow(dead_code)]
55fn prefetch_write<T>(ptr: *mut T) {
56    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
57    unsafe {
58        std::arch::x86_64::_mm_prefetch(ptr as *const i8, std::arch::x86_64::_MM_HINT_ET1);
59    }
60    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
61    {
62        std::hint::black_box(ptr);
63    }
64}
65
66/// Zero-copy SPSC ring buffer with proper memory ordering
67///
68/// # Safety
69/// This type is only safe to use when:
70/// - Only one producer thread exists
71/// - Only one consumer thread exists
72/// - Memory is properly aligned for the element type T
73#[derive(Debug)]
74pub struct SpscRingBuffer<T> {
75    /// Buffer of uninitialized elements (aligned to cache line)
76    buffer: UnsafeCell<[MaybeUninit<T>; RING_CAPACITY]>,
77
78    /// Padding to prevent false sharing between buffer and head
79    _pad1: [u8; CACHE_LINE],
80    /// Current head position (consumer reads here)
81    head: AtomicUsize,
82    /// Cached tail for consumer to avoid reading shared tail
83    cached_tail: AtomicUsize,
84
85    /// Padding to prevent false sharing between head and tail
86    _pad2: [u8; CACHE_LINE],
87    /// Current tail position (producer writes here)
88    tail: AtomicUsize,
89    /// Cached head for producer to avoid reading shared head
90    cached_head: AtomicUsize,
91
92    /// Padding to prevent false sharing between tail and other counters
93    _pad3: [u8; CACHE_LINE],
94    /// Overflow counter for backpressure monitoring
95    overflow_count: AtomicUsize,
96    /// Configuration for this ring buffer
97    config: RingConfig,
98}
99
100impl<T> SpscRingBuffer<T> {
101    const CAPACITY: usize = RING_CAPACITY;
102
103    /// Create a new SPSC ring buffer with the given configuration
104    pub fn new(config: RingConfig) -> Self {
105        assert!(
106            config.capacity < Self::CAPACITY,
107            "capacity must be strictly less than RING_CAPACITY"
108        );
109
110        // Safety: MaybeUninit does not require initialization
111        let buffer = UnsafeCell::new(unsafe {
112            MaybeUninit::<[MaybeUninit<T>; RING_CAPACITY]>::uninit().assume_init()
113        });
114
115        Self {
116            buffer,
117            _pad1: [0; CACHE_LINE],
118            head: AtomicUsize::new(0),
119            cached_tail: AtomicUsize::new(0),
120            _pad2: [0; CACHE_LINE],
121            tail: AtomicUsize::new(0),
122            cached_head: AtomicUsize::new(0),
123            _pad3: [0; CACHE_LINE],
124            overflow_count: AtomicUsize::new(0),
125            config,
126        }
127    }
128
129    /// Push an element into the ring buffer
130    ///
131    /// Returns `Ok(())` on success, or `Err(value)` if the ring is full.
132    /// When full, the producer should wait/backpressure until space is available.
133    pub fn push(&self, value: T) -> Result<(), T> {
134        // Producer loads its own tail with Relaxed.
135        let tail = self.tail.load(Ordering::Relaxed);
136
137        // First check against cached head to avoid atomic contention.
138        let mut head = self.cached_head.load(Ordering::Relaxed);
139        let mut current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
140
141        if current_len >= self.config.capacity {
142            // Only if cached head suggests we are full, refresh from shared head.
143            head = self.head.load(Ordering::Acquire);
144            self.cached_head.store(head, Ordering::Relaxed);
145            current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
146
147            if current_len >= self.config.capacity {
148                self.overflow_count.fetch_add(1, Ordering::Relaxed);
149                return Err(value);
150            }
151        }
152
153        let next_tail = Self::increment(tail);
154
155        // Write the value at tail position
156        unsafe {
157            let buf = &mut *self.buffer.get();
158            buf[tail].write(value);
159
160            // Release store to make write visible to consumer
161            self.tail.store(next_tail, Ordering::Release);
162        }
163
164        Ok(())
165    }
166
167    /// Pop an element from the ring buffer
168    ///
169    /// Returns `Some(value)` on success, or `None` if the ring is empty.
170    pub fn pop(&self) -> Option<T> {
171        // Consumer loads its own head with Relaxed.
172        let head = self.head.load(Ordering::Relaxed);
173
174        // First check against cached tail to avoid atomic contention.
175        let mut tail = self.cached_tail.load(Ordering::Relaxed);
176
177        if head == tail {
178            // Only if cached tail suggests we are empty, refresh from shared tail.
179            tail = self.tail.load(Ordering::Acquire);
180            self.cached_tail.store(tail, Ordering::Relaxed);
181
182            if head == tail {
183                return None;
184            }
185        }
186
187        // Read the value at head position
188        let value = unsafe {
189            let buf = &mut *self.buffer.get();
190            let val = buf[head].assume_init_read();
191
192            // Release store to make read visible to producer
193            self.head.store(Self::increment(head), Ordering::Release);
194
195            val
196        };
197
198        Some(value)
199    }
200
201    /// Get the current number of elements in the ring
202    pub fn len(&self) -> usize {
203        let head = self.head.load(Ordering::Acquire);
204        let tail = self.tail.load(Ordering::Acquire);
205
206        tail.wrapping_sub(head) & (Self::CAPACITY - 1)
207    }
208
209    /// Check if the ring is empty
210    pub fn is_empty(&self) -> bool {
211        let head = self.head.load(Ordering::Relaxed);
212        let mut tail = self.cached_tail.load(Ordering::Relaxed);
213
214        if head == tail {
215            tail = self.tail.load(Ordering::Acquire);
216            self.cached_tail.store(tail, Ordering::Relaxed);
217        }
218
219        head == tail
220    }
221
222    /// Get the configured capacity
223    pub fn capacity(&self) -> usize {
224        self.config.capacity
225    }
226
227    /// Get overflow count for backpressure monitoring
228    pub fn overflow_count(&self) -> usize {
229        self.overflow_count.load(Ordering::Relaxed)
230    }
231
232    /// Get empty count for backpressure monitoring
233    pub fn empty_count(&self) -> usize {
234        self.config.capacity.saturating_sub(self.len())
235    }
236
237    /// Check if backpressure should be applied (ring >= threshold)
238    pub fn should_backpressure(&self) -> bool {
239        let tail = self.tail.load(Ordering::Relaxed);
240        let mut head = self.cached_head.load(Ordering::Relaxed);
241        let mut len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
242
243        if len >= self.config.backpressure_threshold {
244            head = self.head.load(Ordering::Acquire);
245            self.cached_head.store(head, Ordering::Relaxed);
246            len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
247        }
248
249        len >= self.config.backpressure_threshold
250    }
251
252    /// Number of elements that can currently be pushed without blocking.
253    pub fn write_available(&self) -> usize {
254        let tail = self.tail.load(Ordering::Relaxed);
255        let mut head = self.cached_head.load(Ordering::Relaxed);
256        let mut len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
257        if len >= self.config.capacity {
258            head = self.head.load(Ordering::Acquire);
259            self.cached_head.store(head, Ordering::Relaxed);
260            len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
261        }
262        self.config.capacity.saturating_sub(len)
263    }
264
265    /// Number of elements currently available to pop (best-effort, not a snapshot).
266    pub fn read_available(&self) -> usize {
267        let head = self.head.load(Ordering::Relaxed);
268        let mut tail = self.cached_tail.load(Ordering::Relaxed);
269        if head == tail {
270            tail = self.tail.load(Ordering::Acquire);
271            self.cached_tail.store(tail, Ordering::Relaxed);
272        }
273        tail.wrapping_sub(head) & (Self::CAPACITY - 1)
274    }
275
276    /// Wait for space to become available
277    ///
278    /// Uses exponential backoff: spins with PAUSE up to 256 iterations,
279    /// then transitions to OS yield to avoid starving the consumer.
280    pub fn wait_for_space(&self) {
281        let mut backoff = 1u32;
282        loop {
283            if !self.should_backpressure() {
284                return;
285            }
286            // Exponential backoff: 1, 2, 4, 8, … 256
287            let limit = backoff.min(256);
288            for _ in 0..limit {
289                core::hint::spin_loop();
290            }
291            backoff = backoff.saturating_mul(2);
292            if backoff > 256 {
293                std::thread::yield_now();
294                backoff = 1;
295            }
296        }
297    }
298
299    /// Wait for an element to become available
300    ///
301    /// Uses exponential backoff: spins with PAUSE up to 256 iterations,
302    /// then transitions to OS yield to avoid starving the producer.
303    pub fn wait_for_data(&self) {
304        let mut backoff = 1u32;
305        loop {
306            if !self.is_empty() {
307                return;
308            }
309            let limit = backoff.min(256);
310            for _ in 0..limit {
311                core::hint::spin_loop();
312            }
313            backoff = backoff.saturating_mul(2);
314            if backoff > 256 {
315                std::thread::yield_now();
316                backoff = 1;
317            }
318        }
319    }
320
321    /// Push a batch of elements, returning the number successfully pushed.
322    ///
323    /// Stops early if the ring becomes full. This amortizes atomic store
324    /// overhead across multiple elements: only two atomic stores (tail + cached_head)
325    /// for up to `slice.len()` pushes.
326    ///
327    /// Uses prefetching to reduce cache misses on write path.
328    pub fn push_batch(&self, slice: &[T]) -> usize
329    where
330        T: Copy,
331    {
332        let tail = self.tail.load(Ordering::Relaxed);
333        let mut head = self.cached_head.load(Ordering::Relaxed);
334        let mut current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
335
336        if current_len >= self.config.capacity {
337            head = self.head.load(Ordering::Acquire);
338            self.cached_head.store(head, Ordering::Relaxed);
339            current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
340            if current_len >= self.config.capacity {
341                // Ring is full: every requested element is rejected.
342                self.overflow_count
343                    .fetch_add(slice.len(), Ordering::Relaxed);
344                return 0;
345            }
346        }
347
348        let available = self.config.capacity - current_len;
349        let count = available.min(slice.len());
350        if count == 0 {
351            return 0;
352        }
353        if count < slice.len() {
354            // Partial batch: track the elements that didn't fit as overflow,
355            // matching push()'s per-element overflow accounting.
356            self.overflow_count
357                .fetch_add(slice.len() - count, Ordering::Relaxed);
358        }
359
360        let buf = unsafe { &mut *self.buffer.get() };
361        let mask = Self::CAPACITY - 1;
362        let start = tail & mask;
363
364        // Optimize batch write by using direct contiguous memory copying via copy_nonoverlapping
365        // instead of a per-element write loop with modulo and prefetching checks.
366        let buf_ptr = buf.as_mut_ptr() as *mut T;
367        if start + count <= Self::CAPACITY {
368            unsafe {
369                std::ptr::copy_nonoverlapping(slice.as_ptr(), buf_ptr.add(start), count);
370            }
371        } else {
372            let first_chunk = Self::CAPACITY - start;
373            let second_chunk = count - first_chunk;
374            unsafe {
375                std::ptr::copy_nonoverlapping(slice.as_ptr(), buf_ptr.add(start), first_chunk);
376                std::ptr::copy_nonoverlapping(
377                    slice.as_ptr().add(first_chunk),
378                    buf_ptr,
379                    second_chunk,
380                );
381            }
382        }
383        let new_tail = tail.wrapping_add(count) & mask;
384        self.tail.store(new_tail, Ordering::Release);
385        count
386    }
387
388    /// Pop up to `count` elements into the provided mutable slice, returning
389    /// the number of elements actually popped.
390    ///
391    /// Uses `MaybeUninit` to avoid zero-initialisation overhead.
392    /// Uses prefetching to reduce cache misses on read path.
393    pub fn pop_batch(&self, out: &mut [std::mem::MaybeUninit<T>]) -> usize {
394        let head = self.head.load(Ordering::Relaxed);
395        let mut tail = self.cached_tail.load(Ordering::Relaxed);
396
397        if head == tail {
398            tail = self.tail.load(Ordering::Acquire);
399            self.cached_tail.store(tail, Ordering::Relaxed);
400            if head == tail {
401                return 0;
402            }
403        }
404
405        let available = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
406        let count = available.min(out.len());
407        if count == 0 {
408            return 0;
409        }
410
411        let buf = unsafe { &mut *self.buffer.get() };
412        let mask = Self::CAPACITY - 1;
413        let start = head & mask;
414
415        // Optimize batch read by using direct contiguous memory copying via copy_nonoverlapping
416        // instead of a per-element read loop with modulo and prefetching checks.
417        let buf_ptr = buf.as_ptr() as *const T;
418        let out_ptr = out.as_mut_ptr() as *mut T;
419        if start + count <= Self::CAPACITY {
420            unsafe {
421                std::ptr::copy_nonoverlapping(buf_ptr.add(start), out_ptr, count);
422            }
423        } else {
424            let first_chunk = Self::CAPACITY - start;
425            let second_chunk = count - first_chunk;
426            unsafe {
427                std::ptr::copy_nonoverlapping(buf_ptr.add(start), out_ptr, first_chunk);
428                std::ptr::copy_nonoverlapping(buf_ptr, out_ptr.add(first_chunk), second_chunk);
429            }
430        }
431        let new_head = head.wrapping_add(count) & mask;
432        self.head.store(new_head, Ordering::Release);
433        count
434    }
435
436    fn increment(index: usize) -> usize {
437        (index + 1) & (Self::CAPACITY - 1)
438    }
439}
440
441// Safety: SpscRingBuffer is safe to send across threads when used as SPSC.
442// The producer and consumer operate on disjoint memory regions with proper
443// atomic ordering, making this safe for our SPSC use case.
444unsafe impl<T: Send> Send for SpscRingBuffer<T> {}
445unsafe impl<T: Send> Sync for SpscRingBuffer<T> {}
446
447impl<T> Drop for SpscRingBuffer<T> {
448    fn drop(&mut self) {
449        // Drain remaining elements on drop
450        while self.pop().is_some() {}
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457    use std::sync::Arc;
458    use std::thread;
459
460    #[test]
461    fn supports_fifo_push_and_pop() {
462        let ring = SpscRingBuffer::<i32>::new(RingConfig::default());
463        assert!(ring.push(1).is_ok());
464        assert!(ring.push(2).is_ok());
465        assert_eq!(ring.len(), 2);
466        assert_eq!(ring.pop(), Some(1));
467        assert_eq!(ring.pop(), Some(2));
468        assert!(ring.is_empty());
469    }
470
471    #[test]
472    fn returns_full_when_capacity_is_reached() {
473        let config = RingConfig {
474            capacity: 2,
475            backpressure_threshold: 1,
476        };
477        let ring = SpscRingBuffer::<i32>::new(config);
478        assert!(ring.push(1).is_ok());
479        assert!(ring.push(2).is_ok());
480        assert_eq!(ring.push(3), Err(3));
481    }
482
483    #[test]
484    fn supports_single_producer_single_consumer_threads() {
485        let ring = Arc::new(SpscRingBuffer::<i32>::new(RingConfig::default()));
486        let producer_ring = Arc::clone(&ring);
487        let consumer_ring = Arc::clone(&ring);
488
489        let producer = thread::spawn(move || {
490            for value in 0..1_000 {
491                loop {
492                    if producer_ring.push(value).is_ok() {
493                        break;
494                    }
495                    std::thread::yield_now();
496                }
497            }
498        });
499
500        let consumer = thread::spawn(move || {
501            let mut values = Vec::new();
502            while values.len() < 1_000 {
503                if let Some(value) = consumer_ring.pop() {
504                    values.push(value);
505                } else {
506                    std::thread::yield_now();
507                }
508            }
509            values
510        });
511
512        producer.join().unwrap();
513        let values = consumer.join().unwrap();
514
515        assert_eq!(values.len(), 1_000);
516        assert_eq!(values.first(), Some(&0));
517        assert_eq!(values.last(), Some(&999));
518    }
519
520    #[test]
521    fn handles_wrap_around_correctly() {
522        let ring = SpscRingBuffer::<i32>::new(RingConfig::default());
523
524        // Fill most of the ring
525        for i in 0..1020 {
526            ring.push(i).unwrap();
527        }
528
529        assert_eq!(ring.len(), 1020);
530
531        // Pop all elements in FIFO order
532        for i in 0..1020 {
533            assert_eq!(ring.pop(), Some(i));
534        }
535
536        assert!(ring.is_empty());
537    }
538
539    #[test]
540    fn backpressure_threshold_works() {
541        let config = RingConfig {
542            capacity: 100,
543            backpressure_threshold: 70,
544        };
545        let ring = SpscRingBuffer::<i32>::new(config);
546
547        // Fill to threshold - should not backpressure
548        for i in 0..69 {
549            ring.push(i).unwrap();
550        }
551        assert!(!ring.should_backpressure());
552
553        // Fill one more - should backpressure
554        let result = ring.push(70);
555        assert!(result.is_ok()); // Push succeeds until capacity (100) is reached
556        assert!(ring.should_backpressure());
557    }
558
559    #[test]
560    fn wait_for_space_releases() {
561        let ring = Arc::new(SpscRingBuffer::<i32>::new(RingConfig::default()));
562        let consumer_ring = Arc::clone(&ring);
563
564        // Fill the ring
565        for i in 0..1020 {
566            ring.push(i).unwrap();
567        }
568
569        // Consumer pops one element
570        consumer_ring.pop();
571
572        // Producer should be able to push again
573        let result = ring.push(1021);
574        assert!(result.is_ok());
575    }
576
577    #[test]
578    fn wait_for_data_releases() {
579        let ring = Arc::new(SpscRingBuffer::<i32>::new(RingConfig::default()));
580        let producer_ring = Arc::clone(&ring);
581
582        // Ring is empty
583
584        // Producer pushes one element
585        producer_ring.push(42).unwrap();
586
587        // Consumer should be able to pop now
588        assert_eq!(ring.pop(), Some(42));
589    }
590
591    #[test]
592    fn push_batch_writes_multiple_elements() {
593        let ring = SpscRingBuffer::<u64>::new(RingConfig::default());
594        let data: Vec<u64> = (0..100).collect();
595        let n = ring.push_batch(&data);
596        assert_eq!(n, 100);
597        assert_eq!(ring.len(), 100);
598    }
599
600    #[test]
601    fn push_batch_respects_capacity() {
602        let config = RingConfig {
603            capacity: 10,
604            backpressure_threshold: 8,
605        };
606        let ring = SpscRingBuffer::<u64>::new(config);
607        let data: Vec<u64> = (0..100).collect();
608        let n = ring.push_batch(&data);
609        assert_eq!(n, 10);
610        assert_eq!(ring.len(), 10);
611        // The 90 elements that didn't fit must be observable via overflow_count,
612        // otherwise backpressure through the batch path is invisible to monitoring.
613        assert_eq!(ring.overflow_count(), 90);
614    }
615
616    #[test]
617    fn pop_batch_reads_multiple_elements() {
618        let ring = SpscRingBuffer::<u64>::new(RingConfig::default());
619        for i in 0..50u64 {
620            ring.push(i).unwrap();
621        }
622        let mut out = vec![std::mem::MaybeUninit::uninit(); 20];
623        let n = ring.pop_batch(&mut out);
624        assert_eq!(n, 20);
625        let drained: Vec<u64> = out[..n]
626            .iter()
627            .map(|x| unsafe { x.assume_init_read() })
628            .collect();
629        assert_eq!(drained, (0..20).collect::<Vec<u64>>());
630        assert_eq!(ring.len(), 30);
631    }
632
633    #[test]
634    fn pop_batch_returns_available_count() {
635        let ring = SpscRingBuffer::<u64>::new(RingConfig::default());
636        for i in 0..5u64 {
637            ring.push(i).unwrap();
638        }
639        let mut out = vec![std::mem::MaybeUninit::uninit(); 100];
640        let n = ring.pop_batch(&mut out);
641        assert_eq!(n, 5);
642    }
643
644    #[test]
645    fn push_pop_batch_round_trip_spsc() {
646        let ring = Arc::new(SpscRingBuffer::<u64>::new(RingConfig::default()));
647        let prod = Arc::clone(&ring);
648        let cons = Arc::clone(&ring);
649        let iters = 5_000u64;
650        let batch_size = 64;
651
652        let h_prod = std::thread::spawn(move || {
653            let mut sent = 0u64;
654            while sent < iters {
655                let chunk_size = batch_size.min((iters - sent) as usize);
656                let chunk: Vec<u64> = (sent..sent + chunk_size as u64).collect();
657                let n = prod.push_batch(&chunk);
658                sent += n as u64;
659                if n == 0 {
660                    prod.wait_for_space();
661                }
662            }
663        });
664
665        let h_cons = std::thread::spawn(move || {
666            let mut received = 0u64;
667            let mut buf = vec![std::mem::MaybeUninit::uninit(); batch_size];
668            while received < iters {
669                let n = cons.pop_batch(&mut buf);
670                if n > 0 {
671                    received += n as u64;
672                } else {
673                    cons.wait_for_data();
674                }
675            }
676        });
677
678        h_prod.join().unwrap();
679        h_cons.join().unwrap();
680        assert!(ring.is_empty());
681    }
682
683    #[test]
684    fn write_read_available_tracking() {
685        let config = RingConfig {
686            capacity: 100,
687            backpressure_threshold: 70,
688        };
689        let ring = SpscRingBuffer::<u64>::new(config);
690        assert_eq!(ring.write_available(), 100);
691        assert_eq!(ring.read_available(), 0);
692
693        for i in 0..30u64 {
694            ring.push(i).unwrap();
695        }
696        assert_eq!(ring.write_available(), 70);
697        assert_eq!(ring.read_available(), 30);
698
699        // pop() advances head; write_available() is conservatively stale
700        // (cached_head not refreshed since no push failed), so it may report
701        // slightly less than the true capacity.
702        ring.pop();
703        assert_eq!(ring.read_available(), 29);
704        let wa = ring.write_available();
705        assert!(
706            wa == 70 || wa == 71,
707            "write_available should be 70 (conservative) or 71 (true): got {wa}"
708        );
709    }
710}