1use std::collections::HashMap;
54use std::sync::atomic::{AtomicU64, Ordering};
55use std::sync::mpsc::{sync_channel, SyncSender, TrySendError};
56use std::sync::Arc;
57use std::thread::{self, JoinHandle};
58use std::time::Duration;
59use threecrate_core::{Error, Point3f, PointCloud, Result};
60
61pub trait StreamingPipeline<T> {
75 type Output;
77
78 fn process_chunk(&mut self, chunk: &[T]) -> Result<()>;
81
82 fn finalize(self) -> Result<Self::Output>;
84
85 fn memory_bytes(&self) -> usize {
88 0
89 }
90}
91
92#[derive(Debug, Clone, Default)]
98pub struct RunStats {
99 pub items_processed: usize,
101 pub chunks_processed: usize,
103 pub errors_skipped: usize,
105}
106
107#[derive(Debug, Clone)]
109pub struct RunOptions {
110 pub skip_errors: bool,
114}
115
116impl Default for RunOptions {
117 fn default() -> Self {
118 Self { skip_errors: false }
119 }
120}
121
122pub fn run_pipeline<T, P>(
133 pipeline: &mut P,
134 source: impl Iterator<Item = Result<T>>,
135 chunk_size: usize,
136) -> Result<RunStats>
137where
138 P: StreamingPipeline<T>,
139{
140 run_pipeline_with_options(pipeline, source, chunk_size, &RunOptions::default())
141}
142
143pub fn run_pipeline_with_options<T, P>(
145 pipeline: &mut P,
146 source: impl Iterator<Item = Result<T>>,
147 chunk_size: usize,
148 opts: &RunOptions,
149) -> Result<RunStats>
150where
151 P: StreamingPipeline<T>,
152{
153 if chunk_size == 0 {
154 return Err(Error::InvalidData("chunk_size must be ≥ 1".into()));
155 }
156
157 let mut stats = RunStats::default();
158 let mut chunk: Vec<T> = Vec::with_capacity(chunk_size);
159
160 for item in source {
161 match item {
162 Ok(point) => {
163 chunk.push(point);
164 if chunk.len() == chunk_size {
165 pipeline.process_chunk(&chunk)?;
166 stats.items_processed += chunk.len();
167 stats.chunks_processed += 1;
168 chunk.clear();
169 }
170 }
171 Err(e) => {
172 if opts.skip_errors {
173 stats.errors_skipped += 1;
174 } else {
175 return Err(e);
176 }
177 }
178 }
179 }
180
181 if !chunk.is_empty() {
183 pipeline.process_chunk(&chunk)?;
184 stats.items_processed += chunk.len();
185 stats.chunks_processed += 1;
186 }
187
188 Ok(stats)
189}
190
191#[derive(Debug, Clone)]
197pub struct StreamingVoxelFilterConfig {
198 pub voxel_size: f32,
201}
202
203pub struct StreamingVoxelFilter {
217 config: StreamingVoxelFilterConfig,
218 voxels: HashMap<(i32, i32, i32), ([f64; 3], u32)>,
220}
221
222impl StreamingVoxelFilter {
223 pub fn new(config: StreamingVoxelFilterConfig) -> Self {
225 Self {
226 config,
227 voxels: HashMap::new(),
228 }
229 }
230
231 #[inline]
232 fn voxel_key(&self, p: &Point3f) -> (i32, i32, i32) {
233 let inv = 1.0 / self.config.voxel_size;
234 (
235 (p.x * inv).floor() as i32,
236 (p.y * inv).floor() as i32,
237 (p.z * inv).floor() as i32,
238 )
239 }
240
241 pub fn voxel_count(&self) -> usize {
243 self.voxels.len()
244 }
245}
246
247impl StreamingPipeline<Point3f> for StreamingVoxelFilter {
248 type Output = PointCloud<Point3f>;
249
250 fn process_chunk(&mut self, chunk: &[Point3f]) -> Result<()> {
251 if self.config.voxel_size <= 0.0 {
252 return Err(Error::InvalidData("voxel_size must be positive".into()));
253 }
254 for p in chunk {
255 let key = self.voxel_key(p);
256 let entry = self.voxels.entry(key).or_insert(([0.0; 3], 0));
257 entry.0[0] += p.x as f64;
258 entry.0[1] += p.y as f64;
259 entry.0[2] += p.z as f64;
260 entry.1 += 1;
261 }
262 Ok(())
263 }
264
265 fn finalize(self) -> Result<PointCloud<Point3f>> {
266 let points: Vec<Point3f> = self
267 .voxels
268 .values()
269 .map(|(sum, count)| {
270 let n = *count as f64;
271 Point3f::new(
272 (sum[0] / n) as f32,
273 (sum[1] / n) as f32,
274 (sum[2] / n) as f32,
275 )
276 })
277 .collect();
278 Ok(PointCloud::from_points(points))
279 }
280
281 fn memory_bytes(&self) -> usize {
282 self.voxels.len() * 90
284 }
285}
286
287#[derive(Debug, Clone)]
293pub struct PointCloudStats {
294 pub point_count: u64,
296 pub min: Point3f,
298 pub max: Point3f,
300 pub mean: Point3f,
302}
303
304pub struct StreamingStatistics {
309 count: u64,
310 min: [f32; 3],
311 max: [f32; 3],
312 sum: [f64; 3],
313}
314
315impl StreamingStatistics {
316 pub fn new() -> Self {
318 Self {
319 count: 0,
320 min: [f32::INFINITY; 3],
321 max: [f32::NEG_INFINITY; 3],
322 sum: [0.0; 3],
323 }
324 }
325}
326
327impl Default for StreamingStatistics {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333impl StreamingPipeline<Point3f> for StreamingStatistics {
334 type Output = PointCloudStats;
335
336 fn process_chunk(&mut self, chunk: &[Point3f]) -> Result<()> {
337 for p in chunk {
338 self.count += 1;
339 self.min[0] = self.min[0].min(p.x);
340 self.min[1] = self.min[1].min(p.y);
341 self.min[2] = self.min[2].min(p.z);
342 self.max[0] = self.max[0].max(p.x);
343 self.max[1] = self.max[1].max(p.y);
344 self.max[2] = self.max[2].max(p.z);
345 self.sum[0] += p.x as f64;
346 self.sum[1] += p.y as f64;
347 self.sum[2] += p.z as f64;
348 }
349 Ok(())
350 }
351
352 fn finalize(self) -> Result<PointCloudStats> {
353 if self.count == 0 {
354 return Err(Error::InvalidData("no points were processed".into()));
355 }
356 let n = self.count as f64;
357 Ok(PointCloudStats {
358 point_count: self.count,
359 min: Point3f::new(self.min[0], self.min[1], self.min[2]),
360 max: Point3f::new(self.max[0], self.max[1], self.max[2]),
361 mean: Point3f::new(
362 (self.sum[0] / n) as f32,
363 (self.sum[1] / n) as f32,
364 (self.sum[2] / n) as f32,
365 ),
366 })
367 }
368
369 fn memory_bytes(&self) -> usize {
370 std::mem::size_of::<Self>()
371 }
372}
373
374pub struct StreamingCollector {
383 points: Vec<Point3f>,
384}
385
386impl StreamingCollector {
387 pub fn new() -> Self {
389 Self { points: Vec::new() }
390 }
391
392 pub fn with_capacity(cap: usize) -> Self {
394 Self {
395 points: Vec::with_capacity(cap),
396 }
397 }
398}
399
400impl Default for StreamingCollector {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406impl StreamingPipeline<Point3f> for StreamingCollector {
407 type Output = PointCloud<Point3f>;
408
409 fn process_chunk(&mut self, chunk: &[Point3f]) -> Result<()> {
410 self.points.extend_from_slice(chunk);
411 Ok(())
412 }
413
414 fn finalize(self) -> Result<PointCloud<Point3f>> {
415 Ok(PointCloud::from_points(self.points))
416 }
417
418 fn memory_bytes(&self) -> usize {
419 self.points.len() * std::mem::size_of::<Point3f>()
420 }
421}
422
423pub fn cloud_as_stream(cloud: &PointCloud<Point3f>) -> impl Iterator<Item = Result<Point3f>> + '_ {
431 cloud.points.iter().copied().map(Ok)
432}
433
434#[derive(Debug, Clone)]
440pub struct BackpressureConfig {
441 pub max_queue_depth: usize,
445 pub chunk_size: usize,
447 pub flush_timeout: Option<Duration>,
453}
454
455impl Default for BackpressureConfig {
456 fn default() -> Self {
457 Self {
458 max_queue_depth: 1024,
459 chunk_size: 256,
460 flush_timeout: Some(Duration::from_millis(10)),
461 }
462 }
463}
464
465#[derive(Debug, Clone, Default)]
467pub struct RealtimeMetrics {
468 pub items_queued: u64,
470 pub items_processed: u64,
472 pub items_dropped: u64,
474 pub estimated_queue_depth: u64,
476}
477
478struct SharedMetrics {
479 items_queued: AtomicU64,
480 items_processed: AtomicU64,
481 items_dropped: AtomicU64,
482}
483
484impl SharedMetrics {
485 fn new() -> Arc<Self> {
486 Arc::new(Self {
487 items_queued: AtomicU64::new(0),
488 items_processed: AtomicU64::new(0),
489 items_dropped: AtomicU64::new(0),
490 })
491 }
492
493 fn snapshot(&self) -> RealtimeMetrics {
494 let queued = self.items_queued.load(Ordering::Relaxed);
495 let processed = self.items_processed.load(Ordering::Relaxed);
496 let dropped = self.items_dropped.load(Ordering::Relaxed);
497 RealtimeMetrics {
498 items_queued: queued,
499 items_processed: processed,
500 items_dropped: dropped,
501 estimated_queue_depth: queued.saturating_sub(processed),
502 }
503 }
504}
505
506pub struct RealtimePipeline<T: Send + 'static, O: Send + 'static> {
541 sender: Option<SyncSender<T>>,
542 metrics: Arc<SharedMetrics>,
543 join_handle: Option<JoinHandle<Result<O>>>,
544}
545
546impl<T: Send + 'static, O: Send + 'static> RealtimePipeline<T, O> {
547 pub fn new<P>(pipeline: P, config: BackpressureConfig) -> Self
551 where
552 P: StreamingPipeline<T, Output = O> + Send + 'static,
553 {
554 assert!(config.chunk_size >= 1, "chunk_size must be ≥ 1");
555 assert!(config.max_queue_depth >= 1, "max_queue_depth must be ≥ 1");
556
557 let (sender, receiver) = sync_channel::<T>(config.max_queue_depth);
558 let metrics = SharedMetrics::new();
559 let metrics_worker = Arc::clone(&metrics);
560 let chunk_size = config.chunk_size;
561 let flush_timeout = config.flush_timeout;
562
563 let join_handle = thread::spawn(move || {
564 realtime_worker(
565 receiver,
566 pipeline,
567 chunk_size,
568 flush_timeout,
569 metrics_worker,
570 )
571 });
572
573 Self {
574 sender: Some(sender),
575 metrics,
576 join_handle: Some(join_handle),
577 }
578 }
579
580 pub fn send(&self, item: T) -> Result<()> {
584 let sender = self
585 .sender
586 .as_ref()
587 .ok_or_else(|| Error::InvalidData("pipeline already finished".into()))?;
588 sender
589 .send(item)
590 .map_err(|_| Error::InvalidData("pipeline worker has terminated".into()))?;
591 self.metrics.items_queued.fetch_add(1, Ordering::Relaxed);
592 Ok(())
593 }
594
595 pub fn try_send(&self, item: T) -> Result<bool> {
602 let sender = self
603 .sender
604 .as_ref()
605 .ok_or_else(|| Error::InvalidData("pipeline already finished".into()))?;
606 match sender.try_send(item) {
607 Ok(()) => {
608 self.metrics.items_queued.fetch_add(1, Ordering::Relaxed);
609 Ok(true)
610 }
611 Err(TrySendError::Full(_)) => {
612 self.metrics.items_dropped.fetch_add(1, Ordering::Relaxed);
613 Ok(false)
614 }
615 Err(TrySendError::Disconnected(_)) => {
616 Err(Error::InvalidData("pipeline worker has terminated".into()))
617 }
618 }
619 }
620
621 pub fn metrics(&self) -> RealtimeMetrics {
623 self.metrics.snapshot()
624 }
625
626 pub fn finish(mut self) -> Result<O> {
629 self.sender = None;
630 self.join_handle
631 .take()
632 .expect("pipeline already finished")
633 .join()
634 .map_err(|_| Error::InvalidData("pipeline worker panicked".into()))?
635 }
636}
637
638impl<T: Send + 'static, O: Send + 'static> Drop for RealtimePipeline<T, O> {
639 fn drop(&mut self) {
640 self.sender = None;
641 if let Some(handle) = self.join_handle.take() {
642 let _ = handle.join();
643 }
644 }
645}
646
647fn realtime_worker<T, P>(
648 receiver: std::sync::mpsc::Receiver<T>,
649 mut pipeline: P,
650 chunk_size: usize,
651 flush_timeout: Option<Duration>,
652 metrics: Arc<SharedMetrics>,
653) -> Result<P::Output>
654where
655 P: StreamingPipeline<T>,
656{
657 let mut chunk: Vec<T> = Vec::with_capacity(chunk_size);
658
659 match flush_timeout {
660 None => {
661 for item in receiver {
662 metrics.items_processed.fetch_add(1, Ordering::Relaxed);
663 chunk.push(item);
664 if chunk.len() >= chunk_size {
665 pipeline.process_chunk(&chunk)?;
666 chunk.clear();
667 }
668 }
669 }
670 Some(timeout) => {
671 use std::sync::mpsc::RecvTimeoutError;
672 loop {
673 match receiver.recv_timeout(timeout) {
674 Ok(item) => {
675 metrics.items_processed.fetch_add(1, Ordering::Relaxed);
676 chunk.push(item);
677 if chunk.len() >= chunk_size {
678 pipeline.process_chunk(&chunk)?;
679 chunk.clear();
680 }
681 }
682 Err(RecvTimeoutError::Timeout) => {
683 if !chunk.is_empty() {
684 pipeline.process_chunk(&chunk)?;
685 chunk.clear();
686 }
687 }
688 Err(RecvTimeoutError::Disconnected) => break,
689 }
690 }
691 }
692 }
693
694 if !chunk.is_empty() {
695 pipeline.process_chunk(&chunk)?;
696 }
697
698 pipeline.finalize()
699}
700
701#[cfg(test)]
706mod tests {
707 use super::*;
708
709 fn grid_cloud(n: usize) -> PointCloud<Point3f> {
710 let pts: Vec<Point3f> = (0..n)
711 .map(|i| Point3f::new(i as f32 * 0.1, 0.0, 0.0))
712 .collect();
713 PointCloud::from_points(pts)
714 }
715
716 #[test]
719 fn test_collector_round_trip() {
720 let cloud = grid_cloud(25);
721 let mut collector = StreamingCollector::new();
722 let stats = run_pipeline(&mut collector, cloud_as_stream(&cloud), 8).unwrap();
723 let out = collector.finalize().unwrap();
724
725 assert_eq!(stats.items_processed, 25);
726 assert_eq!(stats.chunks_processed, 4); assert_eq!(out.len(), 25);
728 }
729
730 #[test]
733 fn test_statistics_correctness() {
734 let cloud = PointCloud::from_points(vec![
736 Point3f::new(0.0, 0.0, 0.0),
737 Point3f::new(1.0, 0.0, 0.0),
738 Point3f::new(2.0, 0.0, 0.0),
739 ]);
740 let mut stats_pipe = StreamingStatistics::new();
741 run_pipeline(&mut stats_pipe, cloud_as_stream(&cloud), 2).unwrap();
742 let s = stats_pipe.finalize().unwrap();
743
744 assert_eq!(s.point_count, 3);
745 assert!((s.min.x - 0.0).abs() < 1e-6);
746 assert!((s.max.x - 2.0).abs() < 1e-6);
747 assert!((s.mean.x - 1.0).abs() < 1e-6);
748 }
749
750 #[test]
751 fn test_statistics_empty_fails() {
752 let mut stats_pipe = StreamingStatistics::new();
753 assert!(stats_pipe.finalize().is_err());
755 }
756
757 #[test]
760 fn test_voxel_filter_reduces_density() {
761 let cloud = grid_cloud(100);
764 let config = StreamingVoxelFilterConfig { voxel_size: 1.0 };
765 let mut filter = StreamingVoxelFilter::new(config);
766 run_pipeline(&mut filter, cloud_as_stream(&cloud), 32).unwrap();
767 let out = filter.finalize().unwrap();
768
769 assert!(out.len() <= 10, "expected ≤10 voxels, got {}", out.len());
770 assert!(!out.is_empty());
771 }
772
773 #[test]
774 fn test_voxel_filter_centroid() {
775 let cloud = PointCloud::from_points(vec![
777 Point3f::new(0.1, 0.0, 0.0),
778 Point3f::new(0.3, 0.0, 0.0),
779 ]);
780 let config = StreamingVoxelFilterConfig { voxel_size: 1.0 };
781 let mut filter = StreamingVoxelFilter::new(config);
782 run_pipeline(&mut filter, cloud_as_stream(&cloud), 10).unwrap();
783 let out = filter.finalize().unwrap();
784
785 assert_eq!(out.len(), 1);
786 assert!((out.points[0].x - 0.2).abs() < 1e-5);
787 }
788
789 #[test]
790 fn test_voxel_filter_across_chunk_boundary() {
791 let cloud = PointCloud::from_points(vec![
794 Point3f::new(0.1, 0.0, 0.0),
795 Point3f::new(0.9, 0.0, 0.0),
796 ]);
797 let config = StreamingVoxelFilterConfig { voxel_size: 1.0 };
798 let mut filter = StreamingVoxelFilter::new(config);
799 run_pipeline(&mut filter, cloud_as_stream(&cloud), 1).unwrap();
801 let out = filter.finalize().unwrap();
802
803 assert_eq!(
804 out.len(),
805 1,
806 "points in the same voxel across chunks should merge"
807 );
808 assert!((out.points[0].x - 0.5).abs() < 1e-5);
809 }
810
811 #[test]
812 fn test_invalid_voxel_size() {
813 let config = StreamingVoxelFilterConfig { voxel_size: -1.0 };
814 let mut filter = StreamingVoxelFilter::new(config);
815 let cloud = PointCloud::from_points(vec![Point3f::new(0.0, 0.0, 0.0)]);
816 let result = run_pipeline(&mut filter, cloud_as_stream(&cloud), 1);
817 assert!(result.is_err());
818 }
819
820 #[test]
823 fn test_skip_errors() {
824 let source: Vec<Result<Point3f>> = vec![
825 Ok(Point3f::new(0.0, 0.0, 0.0)),
826 Err(Error::InvalidData("bad point".into())),
827 Ok(Point3f::new(1.0, 0.0, 0.0)),
828 ];
829 let mut collector = StreamingCollector::new();
830 let run_stats = run_pipeline_with_options(
831 &mut collector,
832 source.into_iter(),
833 10,
834 &RunOptions { skip_errors: true },
835 )
836 .unwrap();
837 let out = collector.finalize().unwrap();
838 assert_eq!(out.len(), 2);
839 assert_eq!(run_stats.errors_skipped, 1);
840 }
841
842 #[test]
843 fn test_error_propagation() {
844 let source: Vec<Result<Point3f>> = vec![
845 Ok(Point3f::new(0.0, 0.0, 0.0)),
846 Err(Error::InvalidData("bad point".into())),
847 ];
848 let mut collector = StreamingCollector::new();
849 assert!(run_pipeline(&mut collector, source.into_iter(), 10).is_err());
851 }
852
853 #[test]
854 fn test_chunk_size_zero_fails() {
855 let mut collector = StreamingCollector::new();
856 let result = run_pipeline(&mut collector, std::iter::empty::<Result<Point3f>>(), 0);
857 assert!(result.is_err());
858 }
859
860 #[test]
861 fn test_memory_bytes() {
862 let mut filter = StreamingVoxelFilter::new(StreamingVoxelFilterConfig { voxel_size: 0.5 });
863 let cloud = grid_cloud(20);
864 run_pipeline(&mut filter, cloud_as_stream(&cloud), 5).unwrap();
865 assert!(filter.memory_bytes() > 0);
867 }
868
869 #[test]
872 fn test_realtime_basic_round_trip() {
873 let config = BackpressureConfig {
874 max_queue_depth: 32,
875 chunk_size: 8,
876 flush_timeout: None,
877 };
878 let rt = RealtimePipeline::new(StreamingCollector::new(), config);
879 for i in 0..50_u32 {
880 rt.send(Point3f::new(i as f32, 0.0, 0.0)).unwrap();
881 }
882 let cloud = rt.finish().unwrap();
883 assert_eq!(cloud.len(), 50);
884 }
885
886 #[test]
887 fn test_realtime_with_flush_timeout() {
888 let config = BackpressureConfig {
889 max_queue_depth: 64,
890 chunk_size: 100,
891 flush_timeout: Some(Duration::from_millis(5)),
892 };
893 let rt = RealtimePipeline::new(StreamingCollector::new(), config);
894 for i in 0..20_u32 {
895 rt.send(Point3f::new(i as f32, 0.0, 0.0)).unwrap();
896 }
897 let cloud = rt.finish().unwrap();
899 assert_eq!(cloud.len(), 20);
900 }
901
902 #[test]
903 fn test_realtime_voxel_filter() {
904 let filter = StreamingVoxelFilter::new(StreamingVoxelFilterConfig { voxel_size: 1.0 });
905 let config = BackpressureConfig {
906 max_queue_depth: 64,
907 chunk_size: 16,
908 flush_timeout: None,
909 };
910 let rt = RealtimePipeline::new(filter, config);
911 for i in 0..100_u32 {
912 rt.send(Point3f::new(i as f32 * 0.1, 0.0, 0.0)).unwrap();
913 }
914 let cloud = rt.finish().unwrap();
915 assert!(
916 cloud.len() <= 10,
917 "expected ≤10 voxels, got {}",
918 cloud.len()
919 );
920 assert!(!cloud.is_empty());
921 }
922
923 #[test]
924 fn test_realtime_metrics_queued_count() {
925 let config = BackpressureConfig {
926 max_queue_depth: 64,
927 chunk_size: 32,
928 flush_timeout: None,
929 };
930 let rt = RealtimePipeline::new(StreamingCollector::new(), config);
931 for i in 0..20_u32 {
932 rt.send(Point3f::new(i as f32, 0.0, 0.0)).unwrap();
933 }
934 let m = rt.metrics();
935 assert_eq!(m.items_queued, 20);
936 assert_eq!(m.items_dropped, 0);
937 rt.finish().unwrap();
938 }
939
940 #[test]
941 fn test_realtime_try_send_accepts_when_space() {
942 let config = BackpressureConfig {
943 max_queue_depth: 16,
944 chunk_size: 8,
945 flush_timeout: None,
946 };
947 let rt = RealtimePipeline::new(StreamingCollector::new(), config);
948 let accepted = rt.try_send(Point3f::new(1.0, 0.0, 0.0)).unwrap();
949 assert!(accepted, "should accept item when queue has space");
950 let m = rt.metrics();
951 assert_eq!(m.items_queued, 1);
952 assert_eq!(m.items_dropped, 0);
953 let cloud = rt.finish().unwrap();
954 assert_eq!(cloud.len(), 1);
955 }
956
957 #[test]
958 fn test_realtime_try_send_drops_when_full() {
959 use std::sync::{Condvar, Mutex};
960
961 struct LatchedCollector {
964 latch: Arc<(Mutex<bool>, Condvar)>,
965 inner: StreamingCollector,
966 blocked: bool,
967 }
968 impl StreamingPipeline<Point3f> for LatchedCollector {
969 type Output = PointCloud<Point3f>;
970 fn process_chunk(&mut self, chunk: &[Point3f]) -> Result<()> {
971 if !self.blocked {
972 self.blocked = true;
973 let (lock, cv) = &*self.latch;
974 let mut released = lock.lock().unwrap();
975 while !*released {
976 released = cv.wait(released).unwrap();
977 }
978 }
979 self.inner.process_chunk(chunk)
980 }
981 fn finalize(self) -> Result<PointCloud<Point3f>> {
982 self.inner.finalize()
983 }
984 }
985
986 let latch = Arc::new((Mutex::new(false), Condvar::new()));
987 let latch_release = Arc::clone(&latch);
988
989 let config = BackpressureConfig {
991 max_queue_depth: 1,
992 chunk_size: 1,
993 flush_timeout: None,
994 };
995 let rt = RealtimePipeline::new(
996 LatchedCollector {
997 latch,
998 inner: StreamingCollector::new(),
999 blocked: false,
1000 },
1001 config,
1002 );
1003
1004 rt.send(Point3f::new(0.0, 0.0, 0.0)).unwrap();
1006 std::thread::sleep(Duration::from_millis(20));
1008
1009 let mut accepted = 0usize;
1011 let mut dropped = 0usize;
1012 for i in 1..=8_u32 {
1013 if rt.try_send(Point3f::new(i as f32, 0.0, 0.0)).unwrap() {
1014 accepted += 1;
1015 } else {
1016 dropped += 1;
1017 }
1018 }
1019 assert!(
1020 dropped > 0,
1021 "expected at least one drop with max_queue_depth=1"
1022 );
1023
1024 let (lock, cv) = &*latch_release;
1026 *lock.lock().unwrap() = true;
1027 cv.notify_all();
1028
1029 let total_dropped = rt.metrics().items_dropped;
1031 let cloud = rt.finish().unwrap();
1032 assert_eq!(cloud.len(), 1 + accepted);
1034 assert_eq!(total_dropped, dropped as u64);
1035 }
1036
1037 #[test]
1038 fn test_realtime_drop_without_finish() {
1039 let config = BackpressureConfig {
1040 max_queue_depth: 16,
1041 chunk_size: 4,
1042 flush_timeout: None,
1043 };
1044 let rt = RealtimePipeline::new(StreamingCollector::new(), config);
1045 for i in 0..10_u32 {
1046 rt.send(Point3f::new(i as f32, 0.0, 0.0)).unwrap();
1047 }
1048 drop(rt); }
1050
1051 #[test]
1052 fn test_realtime_large_workload() {
1053 let config = BackpressureConfig {
1054 max_queue_depth: 512,
1055 chunk_size: 128,
1056 flush_timeout: None,
1057 };
1058 let rt = RealtimePipeline::new(StreamingCollector::new(), config);
1059 const N: u32 = 10_000;
1060 for i in 0..N {
1061 rt.send(Point3f::new(i as f32, 0.0, 0.0)).unwrap();
1062 }
1063 let cloud = rt.finish().unwrap();
1064 assert_eq!(cloud.len(), N as usize);
1065 }
1066}