Skip to main content

cu29_logstream/
telemetry.rs

1//! Pull-based delivery after native archival, independent of UI processing.
2//!
3//! One publisher owns reception; one reader owns consumption. Unread frames are
4//! overwritten oldest first. Status is coalesced separately, so a paused reader
5//! can still inspect recording health. This host-only exchange adds no robot
6//! task-path work. Publish only after a successful `NativeArchive::accept`.
7//!
8//! Storage is allocated at construction: a ring, a status slot, and one reader
9//! frame. Frames move without cloning. Payload-owned allocations and user-owned
10//! projections are outside this bound. Queue operations use Crossbeam atomics;
11//! they do not wait for capacity or for user processing, but are not an RT
12//! wait-free primitive. Frame destructors and registered wakers must not block.
13
14use atomic_waker::AtomicWaker;
15use crossbeam_queue::ArrayQueue;
16use std::{
17    future::poll_fn,
18    num::NonZeroUsize,
19    sync::{
20        Arc,
21        atomic::{AtomicBool, AtomicU64, Ordering},
22    },
23    task::{Poll, Wake, Waker},
24    thread::{self, Thread, ThreadId},
25    time::{Duration, Instant},
26};
27
28struct Sequenced<T> {
29    sequence: u64,
30    frame: T,
31}
32
33struct Shared<T, S> {
34    frames: ArrayQueue<Sequenced<T>>,
35    status: ArrayQueue<S>,
36    waker: AtomicWaker,
37    closed: AtomicBool,
38    connected: AtomicBool,
39    overwritten: AtomicU64,
40}
41
42/// The sole producer of archived frames and current receiver status.
43pub struct TelemetryPublisher<T, S> {
44    shared: Arc<Shared<T, S>>,
45    next_sequence: u64,
46    status: S,
47}
48
49/// A single consumer cursor. User code chooses when to read and what to retain.
50pub struct TelemetryReader<T, S> {
51    shared: Arc<Shared<T, S>>,
52    next_sequence: u64,
53    current: Option<Sequenced<T>>,
54    status: S,
55    thread_waker: Option<(ThreadId, Waker)>,
56}
57
58/// A stable borrow of one frame, valid until the next mutable reader operation.
59///
60/// Holding this borrow never pins a ring slot or prevents overwrite. `missed`
61/// counts locally overwritten unread frames, not missing source CopperLists.
62pub struct TelemetryRead<'a, T> {
63    pub frame: &'a T,
64    pub missed: u64,
65}
66
67/// Creates a preallocated circular delivery buffer and independent status slot.
68///
69/// The publisher and reader are deliberately not cloneable. Use `T` for your
70/// typed frame (including its session identity) and a small `Copy` type for
71/// receiver status. An initial status notification is available immediately.
72pub fn telemetry_channel<T, S: Copy + PartialEq>(
73    capacity: NonZeroUsize,
74    status: S,
75) -> (TelemetryPublisher<T, S>, TelemetryReader<T, S>) {
76    let shared = Arc::new(Shared {
77        frames: ArrayQueue::new(capacity.get()),
78        status: ArrayQueue::new(1),
79        waker: AtomicWaker::new(),
80        closed: AtomicBool::new(false),
81        connected: AtomicBool::new(true),
82        overwritten: AtomicU64::new(0),
83    });
84    shared.status.force_push(status);
85    (
86        TelemetryPublisher {
87            shared: shared.clone(),
88            next_sequence: 0,
89            status,
90        },
91        TelemetryReader {
92            shared,
93            next_sequence: 0,
94            current: None,
95            status,
96            thread_waker: None,
97        },
98    )
99}
100
101impl<T, S: Copy + PartialEq> TelemetryPublisher<T, S> {
102    /// Moves an already archived frame into the ring. A full ring replaces its
103    /// oldest unread frame. A disconnected reader causes immediate discard.
104    pub fn publish(&mut self, frame: T) {
105        if !self.is_connected() {
106            return;
107        }
108        let entry = Sequenced {
109            sequence: self.next_sequence,
110            frame,
111        };
112        self.next_sequence = self.next_sequence.wrapping_add(1);
113        if self.shared.frames.force_push(entry).is_some() {
114            self.shared.overwritten.fetch_add(1, Ordering::Relaxed);
115        }
116        self.shared.waker.wake();
117    }
118
119    /// Replaces current status and notifies even when no frame is arriving.
120    pub fn set_status(&mut self, status: S) {
121        if self.status != status {
122            self.status = status;
123            self.shared.status.force_push(status);
124            self.shared.waker.wake();
125        }
126    }
127
128    pub fn status(&self) -> S {
129        self.status
130    }
131
132    pub fn is_connected(&self) -> bool {
133        self.shared.connected.load(Ordering::Acquire)
134    }
135
136    /// Cumulative presentation loss; independent of source/archive gaps.
137    pub fn overwritten(&self) -> u64 {
138        self.shared.overwritten.load(Ordering::Relaxed)
139    }
140}
141
142impl<T, S> Drop for TelemetryPublisher<T, S> {
143    fn drop(&mut self) {
144        self.shared.closed.store(true, Ordering::Release);
145        self.shared.waker.wake();
146    }
147}
148
149impl<T, S: Copy> TelemetryReader<T, S> {
150    /// Pulls the oldest retained frame without waiting. At most one borrowed
151    /// frame can be held through this reader. Frame sequence wraps modulo u64;
152    /// fewer than 2^64 publications may separate consecutive reads.
153    pub fn try_read(&mut self) -> Option<TelemetryRead<'_, T>> {
154        let entry = self.shared.frames.pop()?;
155        let missed = entry.sequence.wrapping_sub(self.next_sequence);
156        self.next_sequence = entry.sequence.wrapping_add(1);
157        self.current = Some(entry);
158        Some(TelemetryRead {
159            frame: &self.current.as_ref().unwrap().frame,
160            missed,
161        })
162    }
163
164    /// Observes/acknowledges coalesced current status independently of frames.
165    /// A buffered frame may belong to an older session than this status.
166    pub fn status(&mut self) -> S {
167        if let Some(status) = self.shared.status.pop() {
168            self.status = status;
169        }
170        self.status
171    }
172
173    /// True after publisher shutdown; any retained frames can still be read.
174    pub fn is_closed(&self) -> bool {
175        self.shared.closed.load(Ordering::Acquire)
176    }
177
178    pub fn overwritten(&self) -> u64 {
179        self.shared.overwritten.load(Ordering::Relaxed)
180    }
181
182    fn is_ready(&self) -> bool {
183        !self.shared.frames.is_empty() || !self.shared.status.is_empty() || self.is_closed()
184    }
185
186    /// Registers a one-shot payload-free wake. Re-arm before checking data and
187    /// status. The waker must only schedule/unpark, never process user data.
188    /// The post-registration readiness check prevents lost wakeups.
189    pub fn register_waker(&self, waker: &Waker) {
190        self.shared.waker.register(waker);
191        if self.is_ready() {
192            self.shared.waker.wake();
193        }
194    }
195
196    /// Waits asynchronously for unread data, changed status, or closure. This
197    /// is level-triggered: drain data and observe status before waiting again.
198    pub async fn ready(&mut self) {
199        poll_fn(|cx| {
200            self.register_waker(cx.waker());
201            if self.is_ready() {
202                Poll::Ready(())
203            } else {
204                Poll::Pending
205            }
206        })
207        .await;
208    }
209
210    /// Synchronous counterpart of `ready`, with a timeout for UI/input timers.
211    /// Returns false only on timeout. The first call on a thread allocates its
212    /// reusable thread waker; publication itself never allocates a notification.
213    pub fn wait_timeout(&mut self, timeout: Duration) -> bool {
214        let current = thread::current();
215        if self.thread_waker.as_ref().map(|(id, _)| *id) != Some(current.id()) {
216            self.thread_waker = Some((current.id(), Waker::from(Arc::new(ThreadWake(current)))));
217        }
218        let waker = &self.thread_waker.as_ref().unwrap().1;
219        let start = Instant::now();
220        loop {
221            self.register_waker(waker);
222            if self.is_ready() {
223                return true;
224            }
225            let remaining = timeout.saturating_sub(start.elapsed());
226            if remaining.is_zero() {
227                return false;
228            }
229            thread::park_timeout(remaining);
230        }
231    }
232}
233
234impl<T, S> Drop for TelemetryReader<T, S> {
235    fn drop(&mut self) {
236        self.shared.connected.store(false, Ordering::Release);
237        self.shared.waker.take();
238    }
239}
240
241struct ThreadWake(Thread);
242
243impl Wake for ThreadWake {
244    fn wake(self: Arc<Self>) {
245        self.0.unpark();
246    }
247
248    fn wake_by_ref(self: &Arc<Self>) {
249        self.0.unpark();
250    }
251}