running_process_probe/snapshot/
stream.rs1use std::collections::VecDeque;
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::{Arc, Mutex};
26
27pub const DEFAULT_CAPACITY: usize = 256;
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct SinkStats {
37 pub accepted: u64,
39 pub dropped: u64,
43}
44
45impl SinkStats {
46 pub fn offered(&self) -> u64 {
48 self.accepted + self.dropped
49 }
50
51 pub fn is_complete(&self) -> bool {
53 self.dropped == 0
54 }
55}
56
57#[derive(Clone, Debug)]
61pub struct SampleSink<T> {
62 inner: Arc<Inner<T>>,
63}
64
65#[derive(Debug)]
66struct Inner<T> {
67 queue: Mutex<VecDeque<T>>,
68 capacity: usize,
69 accepted: AtomicU64,
70 dropped: AtomicU64,
71}
72
73impl<T> SampleSink<T> {
74 pub fn with_capacity(capacity: usize) -> Self {
80 Self {
81 inner: Arc::new(Inner {
82 queue: Mutex::new(VecDeque::with_capacity(capacity.max(1))),
83 capacity: capacity.max(1),
84 accepted: AtomicU64::new(0),
85 dropped: AtomicU64::new(0),
86 }),
87 }
88 }
89
90 pub fn offer(&self, sample: T) -> bool {
95 let mut queue = match self.inner.queue.lock() {
96 Ok(q) => q,
97 Err(poisoned) => poisoned.into_inner(),
101 };
102
103 if queue.len() >= self.inner.capacity {
104 self.inner.dropped.fetch_add(1, Ordering::Relaxed);
105 return false;
106 }
107 queue.push_back(sample);
108 self.inner.accepted.fetch_add(1, Ordering::Relaxed);
109 true
110 }
111
112 pub fn drain(&self) -> Vec<T> {
114 let mut queue = match self.inner.queue.lock() {
115 Ok(q) => q,
116 Err(poisoned) => poisoned.into_inner(),
117 };
118 queue.drain(..).collect()
119 }
120
121 pub fn len(&self) -> usize {
123 match self.inner.queue.lock() {
124 Ok(q) => q.len(),
125 Err(poisoned) => poisoned.into_inner().len(),
126 }
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.len() == 0
132 }
133
134 pub fn stats(&self) -> SinkStats {
136 SinkStats {
137 accepted: self.inner.accepted.load(Ordering::Relaxed),
138 dropped: self.inner.dropped.load(Ordering::Relaxed),
139 }
140 }
141}
142
143impl<T> Default for SampleSink<T> {
144 fn default() -> Self {
145 Self::with_capacity(DEFAULT_CAPACITY)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::sync::atomic::AtomicBool;
153 use std::time::{Duration, Instant};
154
155 #[test]
156 fn accepts_up_to_capacity_then_drops() {
157 let sink = SampleSink::with_capacity(4);
158 for i in 0..4 {
159 assert!(sink.offer(i), "sample {i} should fit");
160 }
161 assert!(!sink.offer(99), "the 5th sample must be dropped");
162
163 let stats = sink.stats();
164 assert_eq!(stats.accepted, 4);
165 assert_eq!(stats.dropped, 1);
166 assert_eq!(stats.offered(), 5);
167 assert!(!stats.is_complete());
168 }
169
170 #[test]
171 fn draining_makes_room_again() {
172 let sink = SampleSink::with_capacity(2);
173 sink.offer(1);
174 sink.offer(2);
175 assert!(!sink.offer(3));
176
177 assert_eq!(sink.drain(), vec![1, 2]);
178 assert!(
179 sink.offer(4),
180 "a drained sink must accept again — drops are transient, not terminal"
181 );
182 }
183
184 #[test]
185 fn drain_preserves_order() {
186 let sink = SampleSink::with_capacity(8);
187 for i in 0..5 {
188 sink.offer(i);
189 }
190 assert_eq!(sink.drain(), vec![0, 1, 2, 3, 4]);
191 }
192
193 #[test]
194 fn zero_capacity_is_treated_as_one() {
195 let sink = SampleSink::with_capacity(0);
196 assert!(
197 sink.offer(1),
198 "a sink that can never accept would report 100% drops and hide \
199 whether the consumer ever worked"
200 );
201 }
202
203 #[test]
205 fn producer_never_blocks_on_a_stalled_consumer() {
206 let sink: SampleSink<u64> = SampleSink::with_capacity(8);
207
208 let start = Instant::now();
210 for i in 0..10_000 {
211 sink.offer(i);
212 }
213 let elapsed = start.elapsed();
214
215 assert!(
216 elapsed < Duration::from_secs(2),
217 "producing 10k samples into a full sink took {elapsed:?}; offer() must not wait"
218 );
219
220 let stats = sink.stats();
221 assert_eq!(stats.accepted, 8, "only capacity should be retained");
222 assert_eq!(stats.dropped, 9_992);
223 assert_eq!(stats.offered(), 10_000, "every offer must be accounted for");
224 }
225
226 #[test]
228 fn slow_consumer_causes_counted_drops_not_blocking() {
229 let sink: SampleSink<u64> = SampleSink::with_capacity(16);
230 let stop = Arc::new(AtomicBool::new(false));
231
232 let consumer = {
233 let sink = sink.clone();
234 let stop = Arc::clone(&stop);
235 std::thread::spawn(move || {
236 let mut seen = 0u64;
237 while !stop.load(Ordering::Relaxed) {
238 seen += sink.drain().len() as u64;
239 std::thread::sleep(Duration::from_millis(5));
241 }
242 seen + sink.drain().len() as u64
243 })
244 };
245
246 for i in 0..5_000 {
247 sink.offer(i);
248 }
249 stop.store(true, Ordering::Relaxed);
250 let consumed = consumer.join().unwrap();
251
252 let stats = sink.stats();
253 assert_eq!(
254 stats.offered(),
255 5_000,
256 "accepted + dropped must equal what was offered"
257 );
258 assert!(
259 stats.dropped > 0,
260 "a consumer sleeping 5ms per batch cannot keep up with 5k offers"
261 );
262 assert!(
263 consumed <= stats.accepted,
264 "consumed {consumed} exceeds accepted {}",
265 stats.accepted
266 );
267 }
268
269 #[test]
271 fn clones_share_the_same_queue_and_counters() {
272 let a = SampleSink::with_capacity(4);
273 let b = a.clone();
274 a.offer(1);
275 b.offer(2);
276 assert_eq!(b.len(), 2);
277 assert_eq!(a.stats().accepted, 2);
278 assert_eq!(a.drain(), vec![1, 2]);
279 assert!(b.is_empty());
280 }
281
282 #[test]
284 fn a_poisoned_lock_does_not_panic_the_producer() {
285 let sink: SampleSink<u64> = SampleSink::with_capacity(4);
286 let poisoner = {
287 let sink = sink.clone();
288 std::thread::spawn(move || {
289 let _guard = sink.inner.queue.lock().unwrap();
290 panic!("consumer died holding the lock");
291 })
292 };
293 assert!(poisoner.join().is_err(), "the helper thread should panic");
294
295 assert!(sink.offer(1));
298 assert_eq!(sink.stats().accepted, 1);
299 }
300}