1use std::collections::BTreeMap;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::thread;
20
21use asyn_rs::error::AsynResult;
22use asyn_rs::port::{PortDriver, PortDriverBase, PortFlags};
23use asyn_rs::runtime::config::RuntimeConfig;
24use asyn_rs::runtime::port::{PortRuntimeHandle, create_port_runtime};
25use asyn_rs::user::AsynUser;
26
27use asyn_rs::port_handle::PortHandle;
28
29use crate::ndarray::NDArray;
30use crate::ndarray_pool::NDArrayPool;
31use crate::params::ndarray_driver::NDArrayDriverParams;
32
33use super::channel::{
34 NDArrayOutput, NDArrayReceiver, NDArraySender, PublishOutcome, ndarray_channel,
35};
36use super::params::PluginBaseParams;
37use super::wiring::{WiringRegistry, upstream_key};
38
39#[derive(Debug, Clone)]
41pub enum ParamChangeValue {
42 Int32(i32),
43 Float64(f64),
44 Octet(String),
45}
46
47impl ParamChangeValue {
48 pub fn as_i32(&self) -> i32 {
49 match self {
50 ParamChangeValue::Int32(v) => *v,
51 ParamChangeValue::Float64(v) => *v as i32,
52 ParamChangeValue::Octet(_) => 0,
53 }
54 }
55
56 pub fn as_f64(&self) -> f64 {
57 match self {
58 ParamChangeValue::Int32(v) => *v as f64,
59 ParamChangeValue::Float64(v) => *v,
60 ParamChangeValue::Octet(_) => 0.0,
61 }
62 }
63
64 pub fn as_string(&self) -> Option<&str> {
65 match self {
66 ParamChangeValue::Octet(s) => Some(s),
67 _ => None,
68 }
69 }
70}
71
72pub enum ParamUpdate {
74 Int32 {
75 reason: usize,
76 addr: i32,
77 value: i32,
78 },
79 Float64 {
80 reason: usize,
81 addr: i32,
82 value: f64,
83 },
84 Octet {
85 reason: usize,
86 addr: i32,
87 value: String,
88 },
89 Float64Array {
90 reason: usize,
91 addr: i32,
92 value: Vec<f64>,
93 },
94}
95
96impl ParamUpdate {
97 pub fn int32(reason: usize, value: i32) -> Self {
99 Self::Int32 {
100 reason,
101 addr: 0,
102 value,
103 }
104 }
105 pub fn float64(reason: usize, value: f64) -> Self {
107 Self::Float64 {
108 reason,
109 addr: 0,
110 value,
111 }
112 }
113 pub fn int32_addr(reason: usize, addr: i32, value: i32) -> Self {
115 Self::Int32 {
116 reason,
117 addr,
118 value,
119 }
120 }
121 pub fn float64_addr(reason: usize, addr: i32, value: f64) -> Self {
123 Self::Float64 {
124 reason,
125 addr,
126 value,
127 }
128 }
129 pub fn float64_array(reason: usize, value: Vec<f64>) -> Self {
131 Self::Float64Array {
132 reason,
133 addr: 0,
134 value,
135 }
136 }
137 pub fn float64_array_addr(reason: usize, addr: i32, value: Vec<f64>) -> Self {
139 Self::Float64Array {
140 reason,
141 addr,
142 value,
143 }
144 }
145 pub fn octet(reason: usize, value: String) -> Self {
147 Self::Octet {
148 reason,
149 addr: 0,
150 value,
151 }
152 }
153 pub fn octet_addr(reason: usize, addr: i32, value: String) -> Self {
155 Self::Octet {
156 reason,
157 addr,
158 value,
159 }
160 }
161}
162
163pub struct ProcessResult {
165 pub output_arrays: Vec<Arc<NDArray>>,
166 pub param_updates: Vec<ParamUpdate>,
167 pub scatter: bool,
174}
175
176impl ProcessResult {
177 pub fn sink(param_updates: Vec<ParamUpdate>) -> Self {
179 Self {
180 output_arrays: vec![],
181 param_updates,
182 scatter: false,
183 }
184 }
185
186 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
188 Self {
189 output_arrays,
190 param_updates: vec![],
191 scatter: false,
192 }
193 }
194
195 pub fn empty() -> Self {
197 Self {
198 output_arrays: vec![],
199 param_updates: vec![],
200 scatter: false,
201 }
202 }
203
204 pub fn scatter(output_arrays: Vec<Arc<NDArray>>) -> Self {
207 Self {
208 output_arrays,
209 param_updates: vec![],
210 scatter: true,
211 }
212 }
213}
214
215pub struct ParamChangeResult {
217 pub output_arrays: Vec<Arc<NDArray>>,
218 pub param_updates: Vec<ParamUpdate>,
219}
220
221impl ParamChangeResult {
222 pub fn updates(param_updates: Vec<ParamUpdate>) -> Self {
223 Self {
224 output_arrays: vec![],
225 param_updates,
226 }
227 }
228
229 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
230 Self {
231 output_arrays,
232 param_updates: vec![],
233 }
234 }
235
236 pub fn combined(output_arrays: Vec<Arc<NDArray>>, param_updates: Vec<ParamUpdate>) -> Self {
237 Self {
238 output_arrays,
239 param_updates,
240 }
241 }
242
243 pub fn empty() -> Self {
244 Self {
245 output_arrays: vec![],
246 param_updates: vec![],
247 }
248 }
249}
250
251pub trait NDPluginProcess: Send + 'static {
253 fn process_array(&mut self, array: &NDArray, pool: &NDArrayPool) -> ProcessResult;
255
256 fn plugin_type(&self) -> &str;
258
259 fn compression_aware(&self) -> bool {
265 false
266 }
267
268 fn does_array_callbacks(&self) -> bool {
275 true
276 }
277
278 fn register_params(
280 &mut self,
281 _base: &mut PortDriverBase,
282 ) -> Result<(), asyn_rs::error::AsynError> {
283 Ok(())
284 }
285
286 fn on_param_change(
289 &mut self,
290 _reason: usize,
291 _params: &PluginParamSnapshot,
292 ) -> ParamChangeResult {
293 ParamChangeResult::empty()
294 }
295
296 fn array_data_handle(&self) -> Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>> {
300 None
301 }
302}
303
304pub struct PluginParamSnapshot {
306 pub enable_callbacks: bool,
307 pub reason: usize,
309 pub addr: i32,
311 pub value: ParamChangeValue,
313}
314
315struct SortEntry {
319 arrays: Vec<Arc<NDArray>>,
320 inserted: std::time::Instant,
321}
322
323struct SortBuffer {
331 entries: BTreeMap<i32, SortEntry>,
333 prev_unique_id: i32,
335 first_output: bool,
337 disordered_arrays: i32,
339 dropped_output_arrays: i32,
342}
343
344impl SortBuffer {
345 fn new() -> Self {
346 Self {
347 entries: BTreeMap::new(),
348 prev_unique_id: 0,
349 first_output: true,
350 disordered_arrays: 0,
351 dropped_output_arrays: 0,
352 }
353 }
354
355 fn order_ok(&self, unique_id: i32) -> bool {
357 unique_id == self.prev_unique_id || unique_id == self.prev_unique_id + 1
358 }
359
360 fn note_emitted(&mut self, unique_id: i32) {
363 if !self.first_output && !self.order_ok(unique_id) {
364 self.disordered_arrays += 1;
365 }
366 self.first_output = false;
367 self.prev_unique_id = unique_id;
368 }
369
370 fn insert(&mut self, unique_id: i32, arrays: Vec<Arc<NDArray>>, sort_size: i32) -> bool {
375 if sort_size > 0 && self.entries.len() as i32 >= sort_size {
376 self.dropped_output_arrays += 1;
377 return false;
378 }
379 self.entries
380 .entry(unique_id)
381 .or_insert_with(|| SortEntry {
382 arrays: Vec::new(),
383 inserted: std::time::Instant::now(),
384 })
385 .arrays
386 .extend(arrays);
387 true
388 }
389
390 fn drain_ready(&mut self, sort_time: f64) -> Vec<(i32, Vec<Arc<NDArray>>)> {
394 let now = std::time::Instant::now();
395 let mut out = Vec::new();
396 while let Some((&head_id, entry)) = self.entries.iter().next() {
397 let delta = now.duration_since(entry.inserted).as_secs_f64();
398 let order_ok = self.order_ok(head_id);
399 if (!self.first_output && order_ok) || delta > sort_time {
400 let entry = self.entries.remove(&head_id).unwrap();
401 self.note_emitted(head_id);
402 out.push((head_id, entry.arrays));
403 } else {
404 break;
405 }
406 }
407 out
408 }
409
410 fn drain_all(&mut self) -> Vec<(i32, Vec<Arc<NDArray>>)> {
413 let entries = std::mem::take(&mut self.entries);
414 let mut out = Vec::with_capacity(entries.len());
415 for (id, entry) in entries {
416 self.note_emitted(id);
417 out.push((id, entry.arrays));
418 }
419 out
420 }
421
422 fn len(&self) -> i32 {
424 self.entries.len() as i32
425 }
426}
427
428struct SharedProcessorInner<P: NDPluginProcess> {
431 processor: P,
432 output: Arc<parking_lot::Mutex<NDArrayOutput>>,
433 pool: Arc<NDArrayPool>,
434 ndarray_params: NDArrayDriverParams,
435 plugin_params: PluginBaseParams,
436 port_handle: PortHandle,
437 array_counter: i32,
441 std_array_data_param: Option<usize>,
443 array_callbacks: bool,
450 min_callback_time: f64,
452 last_process_time: Option<std::time::Instant>,
454 sort_mode: i32,
456 sort_time: f64,
458 sort_size: i32,
460 sort_buffer: SortBuffer,
462 dropped_arrays: Arc<std::sync::atomic::AtomicI32>,
466 compression_aware: bool,
469 max_byte_rate: f64,
471 throttler: super::throttler::Throttler,
473 prev_input_array: Option<Arc<NDArray>>,
476 dims_prev: Vec<i32>,
479 nd_array_addr: i32,
481 max_threads: i32,
483 num_threads: i32,
485}
486
487impl<P: NDPluginProcess> SharedProcessorInner<P> {
488 fn should_throttle(&self) -> bool {
489 if self.min_callback_time <= 0.0 {
490 return false;
491 }
492 if let Some(last) = self.last_process_time {
493 last.elapsed().as_secs_f64() < self.min_callback_time
494 } else {
495 false
496 }
497 }
498
499 fn array_byte_cost(array: &NDArray) -> f64 {
502 match &array.codec {
503 Some(c) => c.compressed_size as f64,
504 None => array.info().total_bytes as f64,
505 }
506 }
507
508 fn throttle_ok(&mut self, array: &NDArray) -> bool {
511 if self.max_byte_rate == 0.0 {
512 return true;
513 }
514 let cost = Self::array_byte_cost(array);
515 if self.throttler.try_take(cost) {
516 true
517 } else {
518 self.sort_buffer.dropped_output_arrays += 1;
519 false
520 }
521 }
522
523 fn route_output_arrays(&mut self, arrays: Vec<Arc<NDArray>>) -> Vec<Arc<NDArray>> {
531 let mut ready = Vec::new();
532 for arr in arrays {
533 if !self.throttle_ok(&arr) {
534 continue; }
536 let uid = arr.unique_id;
537 if self.sort_mode != 0
538 && !self.sort_buffer.first_output
539 && !self.sort_buffer.order_ok(uid)
540 {
541 self.sort_buffer.insert(uid, vec![arr], self.sort_size);
543 } else {
544 self.sort_buffer.note_emitted(uid);
546 ready.push(arr);
547 }
548 }
549 if self.sort_mode != 0 {
552 for (_id, mut bucket) in self.sort_buffer.drain_ready(self.sort_time) {
553 ready.append(&mut bucket);
554 }
555 }
556 ready
557 }
558
559 fn process_and_publish(&mut self, array: &Arc<NDArray>) -> Option<ProcessOutput> {
563 if self.should_throttle() {
570 return None;
571 }
572 self.prev_input_array = Some(Arc::clone(array));
577 let t0 = std::time::Instant::now();
578 let result = self.processor.process_array(array, &self.pool);
579 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
580 self.last_process_time = Some(t0);
581
582 let produced = result.output_arrays.len();
596 let ready = if self.array_callbacks || self.std_array_data_param.is_some() {
597 self.route_output_arrays(result.output_arrays)
598 } else {
599 Vec::new()
600 };
601 let count_frame =
606 !(self.std_array_data_param.is_some() && produced > 0 && ready.is_empty());
607 let mut output = self.build_publish_batch(
608 ready,
609 result.param_updates,
610 result.scatter,
611 Some(array.as_ref()),
612 elapsed_ms,
613 self.array_callbacks,
614 count_frame,
615 );
616 output.batch.merge(self.build_status_params_batch());
617 Some(output)
618 }
619
620 fn dropped_arrays_only_batch(&self) -> ProcessOutput {
623 ProcessOutput {
624 arrays: vec![],
625 scatter: false,
626 batch: self.build_status_params_batch(),
627 }
628 }
629
630 fn process_plugin(&mut self) -> Option<ProcessOutput> {
633 let prev = self.prev_input_array.clone()?;
634 self.process_and_publish(&prev)
635 }
636
637 fn tick_sort_buffer(&mut self) -> ProcessOutput {
640 let entries = self.sort_buffer.drain_ready(self.sort_time);
641 self.emit_drained(entries)
642 }
643
644 fn flush_sort_buffer(&mut self) -> ProcessOutput {
646 let entries = self.sort_buffer.drain_all();
647 self.emit_drained(entries)
648 }
649
650 fn emit_drained(&mut self, entries: Vec<(i32, Vec<Arc<NDArray>>)>) -> ProcessOutput {
651 let mut all_arrays = Vec::new();
652 let mut combined = ParamBatch::empty();
653 for (_unique_id, arrays) in entries {
654 let output = self.build_publish_batch(arrays, vec![], false, None, 0.0, true, true);
659 all_arrays.extend(output.arrays);
660 combined.merge(output.batch);
661 }
662 combined.merge(self.build_sort_params_batch());
663 ProcessOutput {
664 arrays: all_arrays,
665 scatter: false,
666 batch: combined,
667 }
668 }
669
670 fn build_sort_params_batch(&self) -> ParamBatch {
671 use asyn_rs::request::ParamSetValue;
672 let sort_free = self.sort_size - self.sort_buffer.len();
673 ParamBatch {
674 addr0: vec![
675 ParamSetValue::Int32 {
676 reason: self.plugin_params.sort_free,
677 addr: 0,
678 value: sort_free,
679 },
680 ParamSetValue::Int32 {
681 reason: self.plugin_params.disordered_arrays,
682 addr: 0,
683 value: self.sort_buffer.disordered_arrays,
684 },
685 ParamSetValue::Int32 {
686 reason: self.plugin_params.dropped_output_arrays,
687 addr: 0,
688 value: self.sort_buffer.dropped_output_arrays,
689 },
690 ],
691 extra: std::collections::HashMap::new(),
692 }
693 }
694
695 fn build_status_params_batch(&self) -> ParamBatch {
698 use asyn_rs::request::ParamSetValue;
699 let mut batch = self.build_sort_params_batch();
700 batch.addr0.push(ParamSetValue::Int32 {
701 reason: self.plugin_params.dropped_arrays,
702 addr: 0,
703 value: self
704 .dropped_arrays
705 .load(std::sync::atomic::Ordering::Acquire),
706 });
707 batch
708 }
709
710 fn build_publish_batch(
720 &mut self,
721 output_arrays: Vec<Arc<NDArray>>,
722 param_updates: Vec<ParamUpdate>,
723 scatter: bool,
724 fallback_array: Option<&NDArray>,
725 elapsed_ms: f64,
726 deliver: bool,
727 count_frame: bool,
728 ) -> ProcessOutput {
729 use asyn_rs::request::ParamSetValue;
730
731 let mut addr0: Vec<ParamSetValue> = Vec::new();
732 let mut extra: std::collections::HashMap<i32, Vec<ParamSetValue>> =
733 std::collections::HashMap::new();
734
735 if let Some(report_arr) = output_arrays.first().map(|a| a.as_ref()).or(fallback_array) {
736 if count_frame {
741 self.array_counter += 1;
742 }
743
744 if let (Some(param), Some(served)) = (
755 self.std_array_data_param,
756 output_arrays.first().map(|a| a.as_ref()),
757 ) {
758 use crate::ndarray::NDDataBuffer;
759 use asyn_rs::param::ParamValue;
760 let value = match &served.data {
761 NDDataBuffer::I8(v) => {
762 Some(ParamValue::Int8Array(std::sync::Arc::from(v.as_slice())))
763 }
764 NDDataBuffer::U8(v) => Some(ParamValue::Int8Array(std::sync::Arc::from(
765 v.iter().map(|&x| x as i8).collect::<Vec<_>>().as_slice(),
766 ))),
767 NDDataBuffer::I16(v) => {
768 Some(ParamValue::Int16Array(std::sync::Arc::from(v.as_slice())))
769 }
770 NDDataBuffer::U16(v) => Some(ParamValue::Int16Array(std::sync::Arc::from(
771 v.iter().map(|&x| x as i16).collect::<Vec<_>>().as_slice(),
772 ))),
773 NDDataBuffer::I32(v) => {
774 Some(ParamValue::Int32Array(std::sync::Arc::from(v.as_slice())))
775 }
776 NDDataBuffer::U32(v) => Some(ParamValue::Int32Array(std::sync::Arc::from(
777 v.iter().map(|&x| x as i32).collect::<Vec<_>>().as_slice(),
778 ))),
779 NDDataBuffer::I64(v) => {
780 Some(ParamValue::Int64Array(std::sync::Arc::from(v.as_slice())))
781 }
782 NDDataBuffer::U64(v) => Some(ParamValue::Int64Array(std::sync::Arc::from(
783 v.iter().map(|&x| x as i64).collect::<Vec<_>>().as_slice(),
784 ))),
785 NDDataBuffer::F32(v) => {
786 Some(ParamValue::Float32Array(std::sync::Arc::from(v.as_slice())))
787 }
788 NDDataBuffer::F64(v) => {
789 Some(ParamValue::Float64Array(std::sync::Arc::from(v.as_slice())))
790 }
791 };
792 if let Some(value) = value {
793 let ts = served.timestamp.to_system_time();
794 self.port_handle
795 .interrupts()
796 .notify(asyn_rs::interrupt::InterruptValue {
797 reason: param,
798 addr: 0,
799 value,
800 timestamp: ts,
801 uint32_changed_mask: 0,
802 ..Default::default()
803 });
804 }
805 }
806
807 let info = report_arr.info();
808 let color_mode = report_arr
812 .attributes
813 .get("ColorMode")
814 .and_then(|a| a.value.as_i64())
815 .map(|v| v as i32)
816 .unwrap_or(info.color_mode as i32);
817 let bayer_pattern = report_arr
818 .attributes
819 .get("BayerPattern")
820 .and_then(|a| a.value.as_i64())
821 .map(|v| v as i32)
822 .unwrap_or(0);
823
824 let mut cur_dims = vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS];
831 for (slot, d) in cur_dims.iter_mut().zip(
832 report_arr
833 .dims
834 .iter()
835 .take(crate::ndarray::ND_ARRAY_MAX_DIMS),
836 ) {
837 *slot = d.size as i32;
838 }
839 if cur_dims != self.dims_prev {
840 self.dims_prev = cur_dims.clone();
841 self.port_handle
842 .interrupts()
843 .notify(asyn_rs::interrupt::InterruptValue {
844 reason: self.ndarray_params.array_dimensions,
845 addr: 0,
846 value: asyn_rs::param::ParamValue::Int32Array(std::sync::Arc::from(
847 cur_dims.as_slice(),
848 )),
849 timestamp: report_arr.timestamp.to_system_time(),
850 uint32_changed_mask: 0,
851 ..Default::default()
852 });
853 }
854
855 addr0.extend([
856 ParamSetValue::Int32 {
857 reason: self.ndarray_params.array_counter,
858 addr: 0,
859 value: self.array_counter,
860 },
861 ParamSetValue::Int32 {
862 reason: self.ndarray_params.unique_id,
863 addr: 0,
864 value: report_arr.unique_id,
865 },
866 ParamSetValue::Int32 {
867 reason: self.ndarray_params.n_dimensions,
868 addr: 0,
869 value: report_arr.dims.len() as i32,
870 },
871 ParamSetValue::Int32 {
872 reason: self.ndarray_params.array_size_x,
873 addr: 0,
874 value: info.x_size as i32,
875 },
876 ParamSetValue::Int32 {
877 reason: self.ndarray_params.array_size_y,
878 addr: 0,
879 value: info.y_size as i32,
880 },
881 ParamSetValue::Int32 {
882 reason: self.ndarray_params.array_size_z,
883 addr: 0,
884 value: info.color_size as i32,
885 },
886 ParamSetValue::Int32 {
887 reason: self.ndarray_params.array_size,
888 addr: 0,
889 value: info.total_bytes as i32,
890 },
891 ParamSetValue::Int32 {
892 reason: self.ndarray_params.data_type,
893 addr: 0,
894 value: report_arr.data.data_type() as i32,
895 },
896 ParamSetValue::Int32 {
897 reason: self.ndarray_params.color_mode,
898 addr: 0,
899 value: color_mode,
900 },
901 ParamSetValue::Int32 {
902 reason: self.ndarray_params.bayer_pattern,
903 addr: 0,
904 value: bayer_pattern,
905 },
906 ParamSetValue::Float64 {
907 reason: self.ndarray_params.timestamp_rbv,
908 addr: 0,
909 value: report_arr.timestamp.as_f64(),
910 },
911 ParamSetValue::Int32 {
912 reason: self.ndarray_params.epics_ts_sec,
913 addr: 0,
914 value: report_arr.timestamp.sec as i32,
915 },
916 ParamSetValue::Int32 {
917 reason: self.ndarray_params.epics_ts_nsec,
918 addr: 0,
919 value: report_arr.timestamp.nsec as i32,
920 },
921 ]);
922
923 match &report_arr.codec {
929 Some(codec) => {
930 addr0.push(ParamSetValue::Octet {
931 reason: self.ndarray_params.codec,
932 addr: 0,
933 value: codec.name.as_str().to_string(),
934 });
935 addr0.push(ParamSetValue::Int32 {
936 reason: self.ndarray_params.compressed_size,
937 addr: 0,
938 value: codec.compressed_size as i32,
939 });
940 }
941 None => {
942 addr0.push(ParamSetValue::Octet {
943 reason: self.ndarray_params.codec,
944 addr: 0,
945 value: String::new(),
946 });
947 addr0.push(ParamSetValue::Int32 {
948 reason: self.ndarray_params.compressed_size,
949 addr: 0,
950 value: info.total_bytes as i32,
951 });
952 }
953 }
954 }
955
956 addr0.push(ParamSetValue::Float64 {
957 reason: self.plugin_params.execution_time,
958 addr: 0,
959 value: elapsed_ms,
960 });
961
962 for update in ¶m_updates {
967 match update {
968 ParamUpdate::Int32 {
969 reason,
970 addr,
971 value,
972 } => {
973 let pv = ParamSetValue::Int32 {
974 reason: *reason,
975 addr: *addr,
976 value: *value,
977 };
978 if *addr == 0 {
979 addr0.push(pv);
980 } else {
981 extra.entry(*addr).or_default().push(pv);
982 }
983 }
984 ParamUpdate::Float64 {
985 reason,
986 addr,
987 value,
988 } => {
989 let pv = ParamSetValue::Float64 {
990 reason: *reason,
991 addr: *addr,
992 value: *value,
993 };
994 if *addr == 0 {
995 addr0.push(pv);
996 } else {
997 extra.entry(*addr).or_default().push(pv);
998 }
999 }
1000 ParamUpdate::Octet {
1001 reason,
1002 addr,
1003 value,
1004 } => {
1005 let pv = ParamSetValue::Octet {
1006 reason: *reason,
1007 addr: *addr,
1008 value: value.clone(),
1009 };
1010 if *addr == 0 {
1011 addr0.push(pv);
1012 } else {
1013 extra.entry(*addr).or_default().push(pv);
1014 }
1015 }
1016 ParamUpdate::Float64Array {
1017 reason,
1018 addr,
1019 value,
1020 } => {
1021 let pv = ParamSetValue::Float64Array {
1022 reason: *reason,
1023 addr: *addr,
1024 value: value.clone(),
1025 };
1026 if *addr == 0 {
1027 addr0.push(pv);
1028 } else {
1029 extra.entry(*addr).or_default().push(pv);
1030 }
1031 }
1032 }
1033 }
1034
1035 ProcessOutput {
1036 arrays: if deliver { output_arrays } else { Vec::new() },
1040 scatter,
1041 batch: ParamBatch { addr0, extra },
1042 }
1043 }
1044}
1045
1046struct ProcessOutput {
1048 arrays: Vec<Arc<NDArray>>,
1049 scatter: bool,
1050 batch: ParamBatch,
1051}
1052
1053impl ProcessOutput {
1054 async fn publish_arrays(&self, senders: &[NDArraySender], scatter_cursor: &mut usize) {
1062 for arr in &self.arrays {
1063 if self.scatter {
1064 Self::scatter_publish(arr, senders, scatter_cursor).await;
1065 } else {
1066 let futs = senders.iter().map(|s| s.publish(arr.clone()));
1067 futures_util::future::join_all(futs).await;
1068 }
1069 }
1070 }
1071
1072 async fn scatter_publish(arr: &Arc<NDArray>, senders: &[NDArraySender], cursor: &mut usize) {
1092 let active: Vec<&NDArraySender> = senders.iter().filter(|s| s.is_enabled()).collect();
1093 let n = active.len();
1094 if n == 0 {
1095 return;
1096 }
1097 for attempt in 0..n {
1098 let target = *cursor % n;
1099 *cursor = cursor.wrapping_add(1);
1100 let is_last = attempt == n - 1;
1101 match active[target].publish_scatter(arr.clone(), is_last).await {
1102 PublishOutcome::Delivered => break,
1106 PublishOutcome::DroppedQueueFull
1110 | PublishOutcome::Disabled
1111 | PublishOutcome::ChannelClosed => {
1112 if is_last {
1113 break;
1114 }
1115 }
1116 }
1117 }
1118 }
1119}
1120
1121struct ParamBatch {
1124 addr0: Vec<asyn_rs::request::ParamSetValue>,
1125 extra: std::collections::HashMap<i32, Vec<asyn_rs::request::ParamSetValue>>,
1126}
1127
1128impl ParamBatch {
1129 fn empty() -> Self {
1130 Self {
1131 addr0: Vec::new(),
1132 extra: std::collections::HashMap::new(),
1133 }
1134 }
1135
1136 fn merge(&mut self, other: ParamBatch) {
1137 self.addr0.extend(other.addr0);
1138 for (addr, updates) in other.extra {
1139 self.extra.entry(addr).or_default().extend(updates);
1140 }
1141 }
1142
1143 async fn flush(self, port: &asyn_rs::port_handle::PortHandle) {
1145 if !self.addr0.is_empty() {
1146 if let Err(e) = port.set_params_and_notify(0, self.addr0).await {
1147 eprintln!("plugin param flush error (addr 0): {e}");
1148 }
1149 }
1150 for (addr, updates) in self.extra {
1151 if let Err(e) = port.set_params_and_notify(addr, updates).await {
1152 eprintln!("plugin param flush error (addr {addr}): {e}");
1153 }
1154 }
1155 }
1156}
1157
1158#[allow(dead_code)]
1160pub struct PluginPortDriver {
1161 base: PortDriverBase,
1162 ndarray_params: NDArrayDriverParams,
1163 plugin_params: PluginBaseParams,
1164 param_change_tx: tokio::sync::mpsc::UnboundedSender<(usize, i32, ParamChangeValue)>,
1165 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1167 std_array_data_param: Option<usize>,
1169}
1170
1171impl PluginPortDriver {
1172 fn new<P: NDPluginProcess>(
1173 port_name: &str,
1174 plugin_type_name: &str,
1175 queue_size: usize,
1176 ndarray_port: &str,
1177 max_addr: usize,
1178 param_change_tx: tokio::sync::mpsc::UnboundedSender<(usize, i32, ParamChangeValue)>,
1179 processor: &mut P,
1180 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1181 ) -> AsynResult<Self> {
1182 let mut base = PortDriverBase::new(
1183 port_name,
1184 max_addr,
1185 PortFlags {
1186 can_block: true,
1187 ..Default::default()
1188 },
1189 );
1190
1191 let ndarray_params = NDArrayDriverParams::create(&mut base)?;
1192 let plugin_params = PluginBaseParams::create(&mut base)?;
1193
1194 base.set_int32_param(plugin_params.enable_callbacks, 0, 0)?;
1196 base.set_int32_param(plugin_params.blocking_callbacks, 0, 0)?;
1197 base.set_int32_param(plugin_params.queue_size, 0, queue_size as i32)?;
1198 base.set_int32_param(plugin_params.dropped_arrays, 0, 0)?;
1199 base.set_int32_param(plugin_params.queue_use, 0, 0)?;
1200 base.set_string_param(plugin_params.plugin_type, 0, plugin_type_name.into())?;
1201 base.set_int32_param(
1202 ndarray_params.array_callbacks,
1203 0,
1204 processor.does_array_callbacks() as i32,
1205 )?;
1206 base.set_int32_param(ndarray_params.write_file, 0, 0)?;
1207 base.set_int32_param(ndarray_params.read_file, 0, 0)?;
1208 base.set_int32_param(ndarray_params.capture, 0, 0)?;
1209 base.set_int32_param(ndarray_params.file_write_status, 0, 0)?;
1210 base.set_string_param(ndarray_params.file_write_message, 0, "".into())?;
1211 base.set_string_param(ndarray_params.file_path, 0, "".into())?;
1212 base.set_string_param(ndarray_params.file_name, 0, "".into())?;
1213 base.set_int32_param(ndarray_params.file_number, 0, 0)?;
1214 base.set_int32_param(ndarray_params.auto_increment, 0, 0)?;
1215 base.set_string_param(ndarray_params.file_template, 0, "%s%s_%3.3d.dat".into())?;
1216 base.set_string_param(ndarray_params.full_file_name, 0, "".into())?;
1217 base.set_int32_param(ndarray_params.create_dir, 0, 0)?;
1218 base.set_string_param(ndarray_params.temp_suffix, 0, "".into())?;
1219
1220 base.set_string_param(ndarray_params.port_name_self, 0, port_name.into())?;
1222 base.set_string_param(
1223 ndarray_params.ad_core_version,
1224 0,
1225 env!("CARGO_PKG_VERSION").into(),
1226 )?;
1227 base.set_string_param(
1228 ndarray_params.driver_version,
1229 0,
1230 env!("CARGO_PKG_VERSION").into(),
1231 )?;
1232 if !ndarray_port.is_empty() {
1233 base.set_string_param(plugin_params.nd_array_port, 0, ndarray_port.into())?;
1234 }
1235
1236 let std_array_data_param = if array_data.is_some() {
1238 Some(base.create_param("STD_ARRAY_DATA", asyn_rs::param::ParamType::GenericPointer)?)
1239 } else {
1240 None
1241 };
1242
1243 processor.register_params(&mut base)?;
1245
1246 Ok(Self {
1247 base,
1248 ndarray_params,
1249 plugin_params,
1250 param_change_tx,
1251 array_data,
1252 std_array_data_param,
1253 })
1254 }
1255}
1256
1257fn copy_direct<T: Copy>(src: &[T], dst: &mut [T]) -> usize {
1259 let n = src.len().min(dst.len());
1260 dst[..n].copy_from_slice(&src[..n]);
1261 n
1262}
1263
1264fn copy_convert<S, D>(src: &[S], dst: &mut [D]) -> usize
1266where
1267 S: CastToF64 + Copy,
1268 D: CastFromF64 + Copy,
1269{
1270 let n = src.len().min(dst.len());
1271 for i in 0..n {
1272 dst[i] = D::cast_from_f64(src[i].cast_to_f64());
1273 }
1274 n
1275}
1276
1277trait CCastTo<D> {
1293 fn ccast(self) -> D;
1294}
1295macro_rules! impl_ccast {
1296 ( $src:ty => $( $dst:ty ),+ ) => {
1297 $(
1298 impl CCastTo<$dst> for $src {
1299 #[inline]
1300 fn ccast(self) -> $dst {
1301 self as $dst
1302 }
1303 }
1304 )+
1305 };
1306}
1307impl_ccast!(i8 => i16, i32, i64);
1308impl_ccast!(u8 => i8, i16, i32, i64);
1309impl_ccast!(i16 => i8, i32, i64);
1310impl_ccast!(u16 => i8, i16, i32, i64);
1311impl_ccast!(i32 => i8, i16, i64);
1312impl_ccast!(u32 => i8, i16, i32, i64);
1313impl_ccast!(i64 => i8, i16, i32);
1314impl_ccast!(u64 => i8, i16, i32, i64);
1315
1316fn copy_ccast<S, D>(src: &[S], dst: &mut [D]) -> usize
1320where
1321 S: CCastTo<D> + Copy,
1322 D: Copy,
1323{
1324 let n = src.len().min(dst.len());
1325 for i in 0..n {
1326 dst[i] = src[i].ccast();
1327 }
1328 n
1329}
1330
1331trait CastToF64 {
1333 fn cast_to_f64(self) -> f64;
1334}
1335
1336impl CastToF64 for i8 {
1337 fn cast_to_f64(self) -> f64 {
1338 self as f64
1339 }
1340}
1341impl CastToF64 for u8 {
1342 fn cast_to_f64(self) -> f64 {
1343 self as f64
1344 }
1345}
1346impl CastToF64 for i16 {
1347 fn cast_to_f64(self) -> f64 {
1348 self as f64
1349 }
1350}
1351impl CastToF64 for u16 {
1352 fn cast_to_f64(self) -> f64 {
1353 self as f64
1354 }
1355}
1356impl CastToF64 for i32 {
1357 fn cast_to_f64(self) -> f64 {
1358 self as f64
1359 }
1360}
1361impl CastToF64 for u32 {
1362 fn cast_to_f64(self) -> f64 {
1363 self as f64
1364 }
1365}
1366impl CastToF64 for i64 {
1367 fn cast_to_f64(self) -> f64 {
1368 self as f64
1369 }
1370}
1371impl CastToF64 for u64 {
1372 fn cast_to_f64(self) -> f64 {
1373 self as f64
1374 }
1375}
1376impl CastToF64 for f32 {
1377 fn cast_to_f64(self) -> f64 {
1378 self as f64
1379 }
1380}
1381impl CastToF64 for f64 {
1382 fn cast_to_f64(self) -> f64 {
1383 self
1384 }
1385}
1386
1387trait CastFromF64 {
1389 fn cast_from_f64(v: f64) -> Self;
1390}
1391
1392impl CastFromF64 for i8 {
1393 fn cast_from_f64(v: f64) -> Self {
1394 v as i8
1395 }
1396}
1397impl CastFromF64 for i16 {
1398 fn cast_from_f64(v: f64) -> Self {
1399 v as i16
1400 }
1401}
1402impl CastFromF64 for i32 {
1403 fn cast_from_f64(v: f64) -> Self {
1404 v as i32
1405 }
1406}
1407impl CastFromF64 for i64 {
1408 fn cast_from_f64(v: f64) -> Self {
1409 v as i64
1410 }
1411}
1412impl CastFromF64 for f32 {
1413 fn cast_from_f64(v: f64) -> Self {
1414 v as f32
1415 }
1416}
1417impl CastFromF64 for f64 {
1418 fn cast_from_f64(v: f64) -> Self {
1419 v
1420 }
1421}
1422
1423macro_rules! impl_read_array {
1426 (
1427 $self:expr, $buf:expr, $direct_variant:ident,
1428 ccast: [ $( $ccast_variant:ident ),* ],
1429 convert: [ $( $variant:ident ),* ]
1430 ) => {{
1431 use crate::ndarray::NDDataBuffer;
1432 let handle = match &$self.array_data {
1433 Some(h) => h,
1434 None => return Ok(0),
1435 };
1436 let guard = handle.lock();
1437 let array = match &*guard {
1438 Some(a) => a,
1439 None => return Ok(0),
1440 };
1441 let n = match &array.data {
1442 NDDataBuffer::$direct_variant(v) => copy_direct(v, $buf),
1443 $( NDDataBuffer::$ccast_variant(v) => copy_ccast(v, $buf), )*
1444 $( NDDataBuffer::$variant(v) => copy_convert(v, $buf), )*
1445 };
1446 Ok(n)
1447 }};
1448}
1449
1450impl PortDriver for PluginPortDriver {
1451 fn base(&self) -> &PortDriverBase {
1452 &self.base
1453 }
1454
1455 fn base_mut(&mut self) -> &mut PortDriverBase {
1456 &mut self.base
1457 }
1458
1459 fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1460 let reason = user.reason;
1461 let addr = user.addr;
1462 self.base.set_int32_param(reason, addr, value)?;
1463 self.base.call_param_callbacks(addr)?;
1464 let _ = self
1466 .param_change_tx
1467 .send((reason, addr, ParamChangeValue::Int32(value)));
1468 Ok(())
1469 }
1470
1471 fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1472 let reason = user.reason;
1473 let addr = user.addr;
1474 self.base.set_float64_param(reason, addr, value)?;
1475 self.base.call_param_callbacks(addr)?;
1476 let _ = self
1477 .param_change_tx
1478 .send((reason, addr, ParamChangeValue::Float64(value)));
1479 Ok(())
1480 }
1481
1482 fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1483 let reason = user.reason;
1484 let addr = user.addr;
1485 let s = String::from_utf8_lossy(data).into_owned();
1486 self.base.set_string_param(reason, addr, s.clone())?;
1487 self.base.call_param_callbacks(addr)?;
1488 let _ = self
1489 .param_change_tx
1490 .send((reason, addr, ParamChangeValue::Octet(s)));
1491 Ok(data.len())
1492 }
1493
1494 fn read_int8_array(&mut self, _user: &AsynUser, buf: &mut [i8]) -> AsynResult<usize> {
1495 impl_read_array!(
1498 self, buf, I8,
1499 ccast: [U8, I16, U16, I32, U32, I64, U64],
1500 convert: [F32, F64]
1501 )
1502 }
1503
1504 fn read_int16_array(&mut self, _user: &AsynUser, buf: &mut [i16]) -> AsynResult<usize> {
1505 impl_read_array!(
1506 self, buf, I16,
1507 ccast: [I8, U8, U16, I32, U32, I64, U64],
1508 convert: [F32, F64]
1509 )
1510 }
1511
1512 fn read_int32_array(&mut self, _user: &AsynUser, buf: &mut [i32]) -> AsynResult<usize> {
1513 impl_read_array!(
1514 self, buf, I32,
1515 ccast: [I8, U8, I16, U16, U32, I64, U64],
1516 convert: [F32, F64]
1517 )
1518 }
1519
1520 fn read_int64_array(&mut self, _user: &AsynUser, buf: &mut [i64]) -> AsynResult<usize> {
1521 impl_read_array!(
1522 self, buf, I64,
1523 ccast: [I8, U8, I16, U16, I32, U32, U64],
1524 convert: [F32, F64]
1525 )
1526 }
1527
1528 fn read_float32_array(&mut self, _user: &AsynUser, buf: &mut [f32]) -> AsynResult<usize> {
1529 impl_read_array!(
1530 self, buf, F32,
1531 ccast: [],
1532 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F64]
1533 )
1534 }
1535
1536 fn read_float64_array(&mut self, _user: &AsynUser, buf: &mut [f64]) -> AsynResult<usize> {
1537 impl_read_array!(
1538 self, buf, F64,
1539 ccast: [],
1540 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F32]
1541 )
1542 }
1543}
1544
1545#[derive(Clone)]
1547pub struct PluginRuntimeHandle {
1548 port_runtime: PortRuntimeHandle,
1549 array_sender: NDArraySender,
1550 array_output: Arc<parking_lot::Mutex<NDArrayOutput>>,
1551 port_name: String,
1552 pub ndarray_params: NDArrayDriverParams,
1553 pub plugin_params: PluginBaseParams,
1554}
1555
1556impl PluginRuntimeHandle {
1557 pub fn port_runtime(&self) -> &PortRuntimeHandle {
1558 &self.port_runtime
1559 }
1560
1561 pub fn array_sender(&self) -> &NDArraySender {
1562 &self.array_sender
1563 }
1564
1565 pub fn array_output(&self) -> &Arc<parking_lot::Mutex<NDArrayOutput>> {
1566 &self.array_output
1567 }
1568
1569 pub fn port_name(&self) -> &str {
1570 &self.port_name
1571 }
1572}
1573
1574pub fn create_plugin_runtime<P: NDPluginProcess>(
1581 port_name: &str,
1582 processor: P,
1583 pool: Arc<NDArrayPool>,
1584 queue_size: usize,
1585 ndarray_port: &str,
1586 wiring: Arc<WiringRegistry>,
1587) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1588 create_plugin_runtime_multi_addr(
1589 port_name,
1590 processor,
1591 pool,
1592 queue_size,
1593 ndarray_port,
1594 wiring,
1595 1,
1596 )
1597}
1598
1599pub fn create_plugin_runtime_multi_addr<P: NDPluginProcess>(
1603 port_name: &str,
1604 mut processor: P,
1605 pool: Arc<NDArrayPool>,
1606 queue_size: usize,
1607 ndarray_port: &str,
1608 wiring: Arc<WiringRegistry>,
1609 max_addr: usize,
1610) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1611 let (param_tx, param_rx) =
1616 tokio::sync::mpsc::unbounded_channel::<(usize, i32, ParamChangeValue)>();
1617
1618 let plugin_type_name = processor.plugin_type().to_string();
1620 let compression_aware = processor.compression_aware();
1621 let does_array_callbacks = processor.does_array_callbacks();
1622 let array_data = processor.array_data_handle();
1623
1624 let driver = PluginPortDriver::new(
1626 port_name,
1627 &plugin_type_name,
1628 queue_size,
1629 ndarray_port,
1630 max_addr,
1631 param_tx,
1632 &mut processor,
1633 array_data,
1634 )
1635 .expect("failed to create plugin port driver");
1636
1637 let ndarray_params = driver.ndarray_params;
1638 let plugin_params = driver.plugin_params;
1639 let std_array_data_param = driver.std_array_data_param;
1640
1641 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default());
1643
1644 let port_handle = port_runtime.port_handle().clone();
1646
1647 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
1649
1650 let enabled = Arc::new(AtomicBool::new(false));
1652 let blocking_mode = Arc::new(AtomicBool::new(false));
1653
1654 let array_output = Arc::new(parking_lot::Mutex::new(NDArrayOutput::new()));
1656 let array_output_for_handle = array_output.clone();
1657 wiring.register_output_addrs(port_name, max_addr, array_output.clone());
1663 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
1666 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
1667 processor,
1668 output: array_output,
1669 pool,
1670 ndarray_params,
1671 plugin_params,
1672 port_handle,
1673 array_counter: 0,
1674 std_array_data_param,
1675 array_callbacks: does_array_callbacks,
1678 min_callback_time: 0.0,
1679 last_process_time: None,
1680 sort_mode: 0,
1681 sort_time: 0.0,
1682 sort_size: 10,
1683 sort_buffer: SortBuffer::new(),
1684 dropped_arrays: dropped_arrays_counter,
1685 compression_aware,
1686 max_byte_rate: 0.0,
1687 throttler: super::throttler::Throttler::new(0.0),
1688 prev_input_array: None,
1689 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
1690 nd_array_addr: 0,
1691 max_threads: 1,
1692 num_threads: 1,
1693 }));
1694
1695 let data_enabled = enabled.clone();
1696 let data_blocking = blocking_mode.clone();
1697
1698 let mut array_sender = array_sender;
1699 array_sender.set_mode_flags(enabled, blocking_mode);
1700
1701 let sender_port_name = port_name.to_string();
1703 let initial_upstream = ndarray_port.to_string();
1704
1705 let data_jh = thread::Builder::new()
1707 .name(format!("plugin-data-{port_name}"))
1708 .spawn(move || {
1709 plugin_data_loop(
1710 shared,
1711 array_rx,
1712 param_rx,
1713 plugin_params,
1714 ndarray_params.array_counter,
1715 data_enabled,
1716 data_blocking,
1717 sender_port_name,
1718 initial_upstream,
1719 wiring,
1720 );
1721 })
1722 .expect("failed to spawn plugin data thread");
1723
1724 let handle = PluginRuntimeHandle {
1725 port_runtime,
1726 array_sender,
1727 array_output: array_output_for_handle,
1728 port_name: port_name.to_string(),
1729 ndarray_params,
1730 plugin_params,
1731 };
1732
1733 (handle, data_jh)
1734}
1735
1736fn queue_status_batch(
1742 plugin_params: &PluginBaseParams,
1743 max_capacity: usize,
1744 free: i32,
1745) -> ParamBatch {
1746 use asyn_rs::request::ParamSetValue;
1747 ParamBatch {
1748 addr0: vec![
1749 ParamSetValue::Int32 {
1750 reason: plugin_params.queue_size,
1751 addr: 0,
1752 value: max_capacity as i32,
1753 },
1754 ParamSetValue::Int32 {
1755 reason: plugin_params.queue_use,
1756 addr: 0,
1757 value: free,
1758 },
1759 ],
1760 extra: std::collections::HashMap::new(),
1761 }
1762}
1763
1764async fn clamp_writeback(port: &PortHandle, reason: usize, value: i32) {
1767 use asyn_rs::request::ParamSetValue;
1768 let _ = port
1769 .set_params_and_notify(
1770 0,
1771 vec![ParamSetValue::Int32 {
1772 reason,
1773 addr: 0,
1774 value,
1775 }],
1776 )
1777 .await;
1778}
1779
1780fn plugin_data_loop<P: NDPluginProcess>(
1781 shared: Arc<parking_lot::Mutex<SharedProcessorInner<P>>>,
1782 mut array_rx: NDArrayReceiver,
1783 mut param_rx: tokio::sync::mpsc::UnboundedReceiver<(usize, i32, ParamChangeValue)>,
1784 plugin_params: PluginBaseParams,
1785 array_counter_reason: usize,
1786 enabled: Arc<AtomicBool>,
1787 blocking_mode: Arc<AtomicBool>,
1788 sender_port_name: String,
1789 initial_upstream: String,
1790 wiring: Arc<WiringRegistry>,
1791) {
1792 let enable_callbacks_reason = plugin_params.enable_callbacks;
1793 let blocking_callbacks_reason = plugin_params.blocking_callbacks;
1794 let min_callback_time_reason = plugin_params.min_callback_time;
1795 let sort_mode_reason = plugin_params.sort_mode;
1796 let sort_time_reason = plugin_params.sort_time;
1797 let sort_size_reason = plugin_params.sort_size;
1798 let nd_array_port_reason = plugin_params.nd_array_port;
1799 let nd_array_addr_reason = plugin_params.nd_array_addr;
1800 let process_plugin_reason = plugin_params.process_plugin;
1801 let max_byte_rate_reason = plugin_params.max_byte_rate;
1802 let num_threads_reason = plugin_params.num_threads;
1803 let max_threads_reason = plugin_params.max_threads;
1804 let array_callbacks_reason = shared.lock().ndarray_params.array_callbacks;
1805 let mut current_upstream = initial_upstream;
1809 let mut current_addr: i32 = 0;
1810 let rt = tokio::runtime::Builder::new_current_thread()
1811 .enable_all()
1812 .build()
1813 .unwrap();
1814 rt.block_on(async {
1815 let mut sort_flush_interval = tokio::time::interval(std::time::Duration::from_secs(3600));
1818 let mut sort_flush_active = false;
1819 let mut last_queue_free: Option<i32> = None;
1822 let mut scatter_cursor: usize = 0;
1827
1828 loop {
1829 tokio::select! {
1830 msg = array_rx.recv_msg() => {
1831 match msg {
1832 Some(msg) => {
1833 if !enabled.load(Ordering::Acquire) {
1838 continue;
1839 }
1840 let (process_output, senders, port) = {
1842 let mut guard = shared.lock();
1843 let compressed = msg.array.codec.is_some();
1847 let output = if compressed && !guard.compression_aware {
1848 guard
1849 .dropped_arrays
1850 .fetch_add(1, Ordering::AcqRel);
1851 Some(guard.dropped_arrays_only_batch())
1852 } else {
1853 guard.process_and_publish(&msg.array)
1858 };
1859 let senders = guard.output.lock().senders_clone();
1860 let port = guard.port_handle.clone();
1861 (output, senders, port)
1862 };
1863 let max_cap = array_rx.max_capacity();
1868 let free = max_cap.saturating_sub(array_rx.pending()) as i32;
1869 let queue_batch = if last_queue_free != Some(free) {
1870 last_queue_free = Some(free);
1871 Some(queue_status_batch(&plugin_params, max_cap, free))
1872 } else {
1873 None
1874 };
1875 if let Some(po) = process_output {
1878 po.publish_arrays(&senders, &mut scatter_cursor).await;
1879 po.batch.flush(&port).await;
1880 }
1881 if let Some(qb) = queue_batch {
1882 qb.flush(&port).await;
1883 }
1884 }
1885 None => break,
1886 }
1887 }
1888 param = param_rx.recv() => {
1889 match param {
1890 Some((reason, addr, value)) => {
1891 if reason == enable_callbacks_reason {
1892 let on = value.as_i32() != 0;
1893 enabled.store(on, Ordering::Release);
1894 if !on {
1897 shared.lock().prev_input_array = None;
1898 }
1899 }
1900 if reason == blocking_callbacks_reason {
1901 blocking_mode.store(value.as_i32() != 0, Ordering::Release);
1902 }
1903 if reason == array_callbacks_reason {
1908 shared.lock().array_callbacks = value.as_i32() != 0;
1909 }
1910 if reason == min_callback_time_reason {
1912 shared.lock().min_callback_time = value.as_f64();
1913 }
1914 if reason == max_byte_rate_reason {
1917 let rate = value.as_f64();
1918 let mut guard = shared.lock();
1919 guard.max_byte_rate = rate;
1920 guard.throttler.reset(rate);
1921 }
1922 if reason == max_threads_reason {
1929 let (port, clamped, mt) = {
1931 let mut guard = shared.lock();
1932 guard.max_threads = value.as_i32().max(1);
1933 let clamped =
1934 guard.num_threads.clamp(1, guard.max_threads);
1935 guard.num_threads = clamped;
1936 (guard.port_handle.clone(), clamped, guard.max_threads)
1937 };
1938 clamp_writeback(&port, num_threads_reason, clamped).await;
1939 clamp_writeback(&port, max_threads_reason, mt).await;
1940 }
1941 if reason == num_threads_reason {
1942 let (port, clamped) = {
1943 let mut guard = shared.lock();
1944 let clamped =
1945 value.as_i32().clamp(1, guard.max_threads.max(1));
1946 guard.num_threads = clamped;
1947 (guard.port_handle.clone(), clamped)
1948 };
1949 clamp_writeback(&port, num_threads_reason, clamped).await;
1950 }
1951 if reason == nd_array_addr_reason {
1955 let new_addr = value.as_i32();
1956 if new_addr != current_addr {
1957 let old_key = upstream_key(¤t_upstream, current_addr);
1958 let new_key = upstream_key(¤t_upstream, new_addr);
1959 shared.lock().nd_array_addr = new_addr;
1960 match wiring.rewire_by_name(
1961 &sender_port_name,
1962 &old_key,
1963 &new_key,
1964 ) {
1965 Ok(()) => current_addr = new_addr,
1966 Err(e) => {
1967 eprintln!("NDArrayAddr reconnect failed: {e}");
1968 shared.lock().nd_array_addr = current_addr;
1969 }
1970 }
1971 }
1972 }
1973 if reason == process_plugin_reason && value.as_i32() != 0 {
1976 let (process_output, senders, port) = {
1977 let mut guard = shared.lock();
1978 let output = guard.process_plugin();
1979 let senders = guard.output.lock().senders_clone();
1980 let port = guard.port_handle.clone();
1981 (output, senders, port)
1982 };
1983 if let Some(po) = process_output {
1984 po.publish_arrays(&senders, &mut scatter_cursor).await;
1985 po.batch.flush(&port).await;
1986 } else {
1987 #[cfg(feature = "ioc")]
2001 if let Some(entry) =
2002 asyn_rs::asyn_record::get_port(&sender_port_name)
2003 {
2004 asyn_rs::asyn_trace!(
2005 entry.trace,
2006 sender_port_name.as_str(),
2007 asyn_rs::trace::TraceMask::WARNING,
2008 "plugin {sender_port_name}: ProcessPlugin \
2009 requested but no input array cached"
2010 );
2011 }
2012 }
2013 }
2014 if reason == array_counter_reason {
2018 shared.lock().array_counter = value.as_i32();
2019 }
2020 if reason == sort_mode_reason {
2022 let mode = value.as_i32();
2023 let flush_work = {
2026 let mut guard = shared.lock();
2027 guard.sort_mode = mode;
2028 if mode == 0 {
2029 let output = guard.flush_sort_buffer();
2030 let senders = guard.output.lock().senders_clone();
2031 let port = guard.port_handle.clone();
2032 sort_flush_active = false;
2033 Some((output, senders, port))
2034 } else {
2035 sort_flush_active = guard.sort_time > 0.0;
2036 if sort_flush_active {
2037 let dur = std::time::Duration::from_secs_f64(guard.sort_time);
2038 sort_flush_interval = tokio::time::interval(dur);
2039 }
2040 None
2041 }
2042 };
2043 if let Some((output, senders, port)) = flush_work {
2044 output.publish_arrays(&senders, &mut scatter_cursor).await;
2045 output.batch.flush(&port).await;
2046 }
2047 }
2048 if reason == sort_time_reason {
2049 let t = value.as_f64();
2050 let mut guard = shared.lock();
2051 guard.sort_time = t;
2052 if guard.sort_mode != 0 && t > 0.0 {
2053 sort_flush_active = true;
2054 let dur = std::time::Duration::from_secs_f64(t);
2055 sort_flush_interval = tokio::time::interval(dur);
2056 } else {
2057 sort_flush_active = false;
2058 }
2059 drop(guard);
2060 }
2061 if reason == sort_size_reason {
2062 shared.lock().sort_size = value.as_i32();
2063 }
2064 if reason == nd_array_port_reason {
2066 if let Some(new_port) = value.as_string() {
2067 if new_port != current_upstream {
2068 let old_key =
2069 upstream_key(¤t_upstream, current_addr);
2070 let new_key = upstream_key(new_port, current_addr);
2071 match wiring.rewire_by_name(
2072 &sender_port_name,
2073 &old_key,
2074 &new_key,
2075 ) {
2076 Ok(()) => current_upstream = new_port.to_string(),
2077 Err(e) => {
2078 eprintln!("NDArrayPort rewire failed: {e}")
2079 }
2080 }
2081 }
2082 }
2083 }
2084 let snapshot = PluginParamSnapshot {
2085 enable_callbacks: enabled.load(Ordering::Acquire),
2086 reason,
2087 addr,
2088 value,
2089 };
2090 let (process_output, senders, port) = {
2091 let mut guard = shared.lock();
2092 let t0 = std::time::Instant::now();
2093 let result = guard.processor.on_param_change(reason, &snapshot);
2094 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
2095 let output = if !result.output_arrays.is_empty() || !result.param_updates.is_empty() {
2096 let deliver = guard.array_callbacks;
2097 Some(guard.build_publish_batch(result.output_arrays, result.param_updates, false, None, elapsed_ms, deliver, true))
2098 } else {
2099 None
2100 };
2101 let senders = guard.output.lock().senders_clone();
2102 (output, senders, guard.port_handle.clone())
2103 };
2104 if let Some(po) = process_output {
2105 po.publish_arrays(&senders, &mut scatter_cursor).await;
2106 po.batch.flush(&port).await;
2107 }
2108 }
2109 None => break,
2110 }
2111 }
2112 _ = sort_flush_interval.tick(), if sort_flush_active => {
2113 let (output, senders, port) = {
2116 let mut guard = shared.lock();
2117 let output = guard.tick_sort_buffer();
2118 let senders = guard.output.lock().senders_clone();
2119 let port = guard.port_handle.clone();
2120 (output, senders, port)
2121 };
2122 output.publish_arrays(&senders, &mut scatter_cursor).await;
2123 output.batch.flush(&port).await;
2124 }
2125 }
2126 }
2127 });
2128}
2129
2130pub fn wire_downstream(upstream: &PluginRuntimeHandle, downstream_sender: NDArraySender) {
2137 upstream.array_output().lock().add(downstream_sender);
2138}
2139
2140pub fn create_plugin_runtime_with_output<P: NDPluginProcess>(
2142 port_name: &str,
2143 mut processor: P,
2144 pool: Arc<NDArrayPool>,
2145 queue_size: usize,
2146 output: NDArrayOutput,
2147 ndarray_port: &str,
2148 wiring: Arc<WiringRegistry>,
2149) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
2150 let (param_tx, param_rx) =
2154 tokio::sync::mpsc::unbounded_channel::<(usize, i32, ParamChangeValue)>();
2155
2156 let plugin_type_name = processor.plugin_type().to_string();
2157 let compression_aware = processor.compression_aware();
2158 let does_array_callbacks = processor.does_array_callbacks();
2159 let array_data = processor.array_data_handle();
2160 let driver = PluginPortDriver::new(
2161 port_name,
2162 &plugin_type_name,
2163 queue_size,
2164 ndarray_port,
2165 1,
2166 param_tx,
2167 &mut processor,
2168 array_data,
2169 )
2170 .expect("failed to create plugin port driver");
2171
2172 let ndarray_params = driver.ndarray_params;
2173 let plugin_params = driver.plugin_params;
2174 let std_array_data_param = driver.std_array_data_param;
2175
2176 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default());
2177
2178 let port_handle = port_runtime.port_handle().clone();
2179
2180 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
2181
2182 let enabled = Arc::new(AtomicBool::new(false));
2183 let blocking_mode = Arc::new(AtomicBool::new(false));
2184
2185 let array_output = Arc::new(parking_lot::Mutex::new(output));
2186 let array_output_for_handle = array_output.clone();
2187 wiring.register_output(port_name, array_output.clone());
2191 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
2193 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
2194 processor,
2195 output: array_output,
2196 pool,
2197 ndarray_params,
2198 plugin_params,
2199 port_handle,
2200 array_counter: 0,
2201 std_array_data_param,
2202 array_callbacks: does_array_callbacks,
2205 min_callback_time: 0.0,
2206 last_process_time: None,
2207 sort_mode: 0,
2208 sort_time: 0.0,
2209 sort_size: 10,
2210 sort_buffer: SortBuffer::new(),
2211 dropped_arrays: dropped_arrays_counter,
2212 compression_aware,
2213 max_byte_rate: 0.0,
2214 throttler: super::throttler::Throttler::new(0.0),
2215 prev_input_array: None,
2216 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
2217 nd_array_addr: 0,
2218 max_threads: 1,
2219 num_threads: 1,
2220 }));
2221
2222 let data_enabled = enabled.clone();
2223 let data_blocking = blocking_mode.clone();
2224
2225 let mut array_sender = array_sender;
2226 array_sender.set_mode_flags(enabled, blocking_mode);
2227
2228 let sender_port_name = port_name.to_string();
2230 let initial_upstream = ndarray_port.to_string();
2231
2232 let data_jh = thread::Builder::new()
2233 .name(format!("plugin-data-{port_name}"))
2234 .spawn(move || {
2235 plugin_data_loop(
2236 shared,
2237 array_rx,
2238 param_rx,
2239 plugin_params,
2240 ndarray_params.array_counter,
2241 data_enabled,
2242 data_blocking,
2243 sender_port_name,
2244 initial_upstream,
2245 wiring,
2246 );
2247 })
2248 .expect("failed to spawn plugin data thread");
2249
2250 let handle = PluginRuntimeHandle {
2251 port_runtime,
2252 array_sender,
2253 array_output: array_output_for_handle,
2254 port_name: port_name.to_string(),
2255 ndarray_params,
2256 plugin_params,
2257 };
2258
2259 (handle, data_jh)
2260}
2261
2262#[cfg(test)]
2263mod tests {
2264 use super::*;
2265 use crate::ndarray::{NDDataType, NDDimension};
2266 use crate::plugin::channel::ndarray_channel;
2267
2268 struct PassthroughProcessor;
2270
2271 impl NDPluginProcess for PassthroughProcessor {
2272 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2273 ProcessResult::arrays(vec![Arc::new(array.clone())])
2274 }
2275 fn plugin_type(&self) -> &str {
2276 "Passthrough"
2277 }
2278 }
2279
2280 struct SinkProcessor {
2282 count: usize,
2283 }
2284
2285 impl NDPluginProcess for SinkProcessor {
2286 fn process_array(&mut self, _array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2287 self.count += 1;
2288 ProcessResult::empty()
2289 }
2290 fn plugin_type(&self) -> &str {
2291 "Sink"
2292 }
2293 }
2294
2295 fn make_test_array(id: i32) -> Arc<NDArray> {
2296 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2297 arr.unique_id = id;
2298 Arc::new(arr)
2299 }
2300
2301 fn test_wiring() -> Arc<WiringRegistry> {
2302 Arc::new(WiringRegistry::new())
2303 }
2304
2305 fn enable_callbacks(handle: &PluginRuntimeHandle) {
2307 handle
2308 .port_runtime()
2309 .port_handle()
2310 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2311 .unwrap();
2312 std::thread::sleep(std::time::Duration::from_millis(10));
2313 }
2314
2315 fn send_array(sender: &NDArraySender, array: Arc<NDArray>) {
2319 let sender = sender.clone();
2320 let jh = std::thread::spawn(move || {
2321 let rt = tokio::runtime::Builder::new_current_thread()
2322 .enable_all()
2323 .build()
2324 .unwrap();
2325 rt.block_on(sender.publish(array));
2326 });
2327 jh.join().unwrap();
2328 }
2329
2330 #[test]
2331 fn test_passthrough_runtime() {
2332 let pool = Arc::new(NDArrayPool::new(1_000_000));
2333
2334 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2336 let mut output = NDArrayOutput::new();
2337 output.add(downstream_sender);
2338
2339 let (handle, _data_jh) = create_plugin_runtime_with_output(
2340 "PASS1",
2341 PassthroughProcessor,
2342 pool,
2343 10,
2344 output,
2345 "",
2346 test_wiring(),
2347 );
2348 enable_callbacks(&handle);
2349
2350 send_array(handle.array_sender(), make_test_array(42));
2352
2353 let received = downstream_rx.blocking_recv().unwrap();
2355 assert_eq!(received.unique_id, 42);
2356 }
2357
2358 #[test]
2359 fn test_sink_runtime() {
2360 let pool = Arc::new(NDArrayPool::new(1_000_000));
2361
2362 let (handle, _data_jh) = create_plugin_runtime(
2363 "SINK1",
2364 SinkProcessor { count: 0 },
2365 pool,
2366 10,
2367 "",
2368 test_wiring(),
2369 );
2370 enable_callbacks(&handle);
2371
2372 send_array(handle.array_sender(), make_test_array(1));
2374 send_array(handle.array_sender(), make_test_array(2));
2375
2376 std::thread::sleep(std::time::Duration::from_millis(50));
2378
2379 assert_eq!(handle.port_name(), "SINK1");
2381 }
2382
2383 #[test]
2384 fn test_plugin_type_param() {
2385 let pool = Arc::new(NDArrayPool::new(1_000_000));
2386
2387 let (handle, _data_jh) = create_plugin_runtime(
2388 "TYPE_TEST",
2389 PassthroughProcessor,
2390 pool,
2391 10,
2392 "",
2393 test_wiring(),
2394 );
2395
2396 assert_eq!(handle.port_name(), "TYPE_TEST");
2398 assert_eq!(handle.port_runtime().port_name(), "TYPE_TEST");
2399 }
2400
2401 #[test]
2402 fn test_shutdown_on_handle_drop() {
2403 let pool = Arc::new(NDArrayPool::new(1_000_000));
2404
2405 let (handle, data_jh) = create_plugin_runtime(
2406 "SHUTDOWN_TEST",
2407 PassthroughProcessor,
2408 pool,
2409 10,
2410 "",
2411 test_wiring(),
2412 );
2413
2414 let sender = handle.array_sender().clone();
2416 drop(handle);
2417 drop(sender);
2418
2419 let result = data_jh.join();
2421 assert!(result.is_ok());
2422 }
2423
2424 #[test]
2425 fn test_wire_to_nonzero_ndarray_addr() {
2426 use crate::plugin::wiring::upstream_key;
2432 let pool = Arc::new(NDArrayPool::new(1_000_000));
2433 let wiring = test_wiring();
2434
2435 let (up_handle, _up_jh) = create_plugin_runtime_multi_addr(
2437 "UP_MULTI",
2438 PassthroughProcessor,
2439 pool,
2440 10,
2441 "",
2442 wiring.clone(),
2443 2,
2444 );
2445 enable_callbacks(&up_handle);
2446
2447 let addr0 = wiring.lookup_output("UP_MULTI");
2449 let addr1 = wiring.lookup_output(&upstream_key("UP_MULTI", 1));
2450 assert!(addr0.is_some(), "addr 0 output must be registered");
2451 assert!(
2452 addr1.is_some(),
2453 "addr 1 output must be registered for a max_addr=2 port"
2454 );
2455
2456 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWN_ADDR1", 10);
2458 wiring
2459 .rewire(&downstream_sender, "", &upstream_key("UP_MULTI", 1))
2460 .expect("wiring a consumer to NDArrayAddr=1 must succeed");
2461
2462 send_array(up_handle.array_sender(), make_test_array(99));
2464 let received = downstream_rx.blocking_recv().unwrap();
2465 assert_eq!(
2466 received.unique_id, 99,
2467 "consumer wired to NDArrayAddr=1 must receive upstream arrays"
2468 );
2469 }
2470
2471 #[test]
2472 fn test_nonblocking_passthrough() {
2473 let pool = Arc::new(NDArrayPool::new(1_000_000));
2474 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2475 let mut output = NDArrayOutput::new();
2476 output.add(downstream_sender);
2477
2478 let (handle, _data_jh) = create_plugin_runtime_with_output(
2479 "NB_TEST",
2480 PassthroughProcessor,
2481 pool,
2482 10,
2483 output,
2484 "",
2485 test_wiring(),
2486 );
2487 enable_callbacks(&handle);
2488
2489 send_array(handle.array_sender(), make_test_array(42));
2490
2491 let received = downstream_rx.blocking_recv().unwrap();
2492 assert_eq!(received.unique_id, 42);
2493 }
2494
2495 #[test]
2496 fn test_blocking_to_nonblocking_switch() {
2497 let pool = Arc::new(NDArrayPool::new(1_000_000));
2498 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2499 let mut output = NDArrayOutput::new();
2500 output.add(downstream_sender);
2501
2502 let (handle, _data_jh) = create_plugin_runtime_with_output(
2503 "SWITCH_TEST",
2504 PassthroughProcessor,
2505 pool,
2506 10,
2507 output,
2508 "",
2509 test_wiring(),
2510 );
2511 enable_callbacks(&handle);
2512
2513 handle
2515 .port_runtime()
2516 .port_handle()
2517 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 1)
2518 .unwrap();
2519 std::thread::sleep(std::time::Duration::from_millis(50));
2520
2521 send_array(handle.array_sender(), make_test_array(1));
2522 let received = downstream_rx.blocking_recv().unwrap();
2523 assert_eq!(received.unique_id, 1);
2524
2525 handle
2527 .port_runtime()
2528 .port_handle()
2529 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 0)
2530 .unwrap();
2531 std::thread::sleep(std::time::Duration::from_millis(50));
2532
2533 send_array(handle.array_sender(), make_test_array(2));
2535 let received = downstream_rx.blocking_recv().unwrap();
2536 assert_eq!(received.unique_id, 2);
2537 }
2538
2539 #[test]
2540 fn test_enable_callbacks_disables_processing() {
2541 let pool = Arc::new(NDArrayPool::new(1_000_000));
2542 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2543 let mut output = NDArrayOutput::new();
2544 output.add(downstream_sender);
2545
2546 let (handle, _data_jh) = create_plugin_runtime_with_output(
2547 "ENABLE_TEST",
2548 PassthroughProcessor,
2549 pool,
2550 10,
2551 output,
2552 "",
2553 test_wiring(),
2554 );
2555
2556 handle
2558 .port_runtime()
2559 .port_handle()
2560 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 0)
2561 .unwrap();
2562 std::thread::sleep(std::time::Duration::from_millis(50));
2563
2564 send_array(handle.array_sender(), make_test_array(99));
2566
2567 let rt = tokio::runtime::Builder::new_current_thread()
2569 .enable_all()
2570 .build()
2571 .unwrap();
2572 let result = rt.block_on(async {
2573 tokio::time::timeout(std::time::Duration::from_millis(100), downstream_rx.recv()).await
2574 });
2575 assert!(
2576 result.is_err(),
2577 "should not receive array when callbacks disabled"
2578 );
2579 }
2580
2581 #[test]
2582 fn test_downstream_receives_multiple() {
2583 let pool = Arc::new(NDArrayPool::new(1_000_000));
2584
2585 let (ds1, mut rx1) = ndarray_channel("DS1", 10);
2586 let (ds2, mut rx2) = ndarray_channel("DS2", 10);
2587 let mut output = NDArrayOutput::new();
2588 output.add(ds1);
2589 output.add(ds2);
2590
2591 let (handle, _data_jh) = create_plugin_runtime_with_output(
2592 "DS_TEST",
2593 PassthroughProcessor,
2594 pool,
2595 10,
2596 output,
2597 "",
2598 test_wiring(),
2599 );
2600 enable_callbacks(&handle);
2601
2602 send_array(handle.array_sender(), make_test_array(77));
2603
2604 let r1 = rx1.blocking_recv().unwrap();
2606 let r2 = rx2.blocking_recv().unwrap();
2607 assert_eq!(r1.unique_id, 77);
2608 assert_eq!(r2.unique_id, 77);
2609 }
2610
2611 #[test]
2612 fn test_param_updates_after_send() {
2613 let pool = Arc::new(NDArrayPool::new(1_000_000));
2614
2615 struct ParamTracker;
2616 impl NDPluginProcess for ParamTracker {
2617 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2618 ProcessResult::arrays(vec![Arc::new(array.clone())])
2619 }
2620 fn plugin_type(&self) -> &str {
2621 "ParamTracker"
2622 }
2623 }
2624
2625 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2626 let mut output = NDArrayOutput::new();
2627 output.add(downstream_sender);
2628
2629 let (handle, _data_jh) = create_plugin_runtime_with_output(
2630 "PARAM_TEST",
2631 ParamTracker,
2632 pool,
2633 10,
2634 output,
2635 "",
2636 test_wiring(),
2637 );
2638 enable_callbacks(&handle);
2639
2640 send_array(handle.array_sender(), make_test_array(1));
2642 let received = downstream_rx.blocking_recv().unwrap();
2643 assert_eq!(received.unique_id, 1);
2644
2645 handle
2647 .port_runtime()
2648 .port_handle()
2649 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2650 .unwrap();
2651 std::thread::sleep(std::time::Duration::from_millis(50));
2652
2653 send_array(handle.array_sender(), make_test_array(2));
2655 let received = downstream_rx.blocking_recv().unwrap();
2656 assert_eq!(received.unique_id, 2);
2657 }
2658
2659 #[test]
2660 fn test_sort_buffer_reorders_by_unique_id() {
2661 let mut buf = SortBuffer::new();
2662
2663 buf.insert(3, vec![make_test_array(3)], 10);
2665 buf.insert(1, vec![make_test_array(1)], 10);
2666 buf.insert(2, vec![make_test_array(2)], 10);
2667
2668 assert_eq!(buf.len(), 3);
2669
2670 let drained = buf.drain_all();
2671 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2672 assert_eq!(ids, vec![1, 2, 3], "should drain in sorted uniqueId order");
2673 assert_eq!(buf.len(), 0);
2674 assert_eq!(buf.prev_unique_id, 3);
2675 }
2676
2677 #[test]
2678 fn test_sort_buffer_drain_ready_contiguous() {
2679 let mut buf = SortBuffer::new();
2682 buf.note_emitted(0);
2685 buf.insert(1, vec![make_test_array(1)], 10);
2686 buf.insert(2, vec![make_test_array(2)], 10);
2687 buf.insert(5, vec![make_test_array(5)], 10); let drained = buf.drain_ready(100.0);
2691 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2692 assert_eq!(ids, vec![1, 2], "contiguous run released; id=5 held by gap");
2693 assert_eq!(buf.len(), 1);
2694 }
2695
2696 #[test]
2697 fn test_sort_buffer_drain_ready_deadline() {
2698 let mut buf = SortBuffer::new();
2700 buf.note_emitted(1); buf.insert(5, vec![make_test_array(5)], 10); std::thread::sleep(std::time::Duration::from_millis(30));
2703 let drained = buf.drain_ready(0.01);
2705 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2706 assert_eq!(ids, vec![5], "stale head released via deadline");
2707 }
2708
2709 #[test]
2710 fn test_sort_buffer_detects_disordered_on_emit() {
2711 let mut buf = SortBuffer::new();
2713 buf.note_emitted(5); buf.note_emitted(3); assert_eq!(buf.disordered_arrays, 1);
2716 buf.note_emitted(4); assert_eq!(buf.disordered_arrays, 1);
2718 }
2719
2720 #[test]
2721 fn test_sort_buffer_drops_when_full() {
2722 let mut buf = SortBuffer::new();
2723
2724 assert!(buf.insert(1, vec![make_test_array(1)], 2));
2726 assert!(buf.insert(2, vec![make_test_array(2)], 2));
2727 assert!(!buf.insert(3, vec![make_test_array(3)], 2));
2728
2729 assert_eq!(buf.len(), 2);
2730 assert_eq!(buf.dropped_output_arrays, 1);
2731 }
2732
2733 #[test]
2734 fn test_sort_mode_runtime_integration() {
2735 let pool = Arc::new(NDArrayPool::new(1_000_000));
2736 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2737 let mut output = NDArrayOutput::new();
2738 output.add(downstream_sender);
2739
2740 let (handle, _data_jh) = create_plugin_runtime_with_output(
2741 "SORT_TEST",
2742 PassthroughProcessor,
2743 pool,
2744 10,
2745 output,
2746 "",
2747 test_wiring(),
2748 );
2749 enable_callbacks(&handle);
2750
2751 handle
2753 .port_runtime()
2754 .port_handle()
2755 .write_int32_blocking(handle.plugin_params.sort_size, 0, 10)
2756 .unwrap();
2757 handle
2758 .port_runtime()
2759 .port_handle()
2760 .write_float64_blocking(handle.plugin_params.sort_time, 0, 0.1)
2761 .unwrap();
2762 handle
2763 .port_runtime()
2764 .port_handle()
2765 .write_int32_blocking(handle.plugin_params.sort_mode, 0, 1)
2766 .unwrap();
2767 std::thread::sleep(std::time::Duration::from_millis(50));
2768
2769 send_array(handle.array_sender(), make_test_array(1));
2772 send_array(handle.array_sender(), make_test_array(2));
2773 send_array(handle.array_sender(), make_test_array(3));
2774
2775 let rt = tokio::runtime::Builder::new_current_thread()
2776 .enable_all()
2777 .build()
2778 .unwrap();
2779 let fast = rt.block_on(async {
2780 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
2781 });
2782 assert!(
2783 fast.is_ok(),
2784 "in-order arrays must be emitted immediately, not buffered"
2785 );
2786 assert_eq!(fast.unwrap().unwrap().unique_id, 1);
2787 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
2788 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 3);
2789
2790 send_array(handle.array_sender(), make_test_array(5));
2794 send_array(handle.array_sender(), make_test_array(4));
2795 std::thread::sleep(std::time::Duration::from_millis(50));
2796 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 4);
2798 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 5);
2799 }
2800
2801 #[test]
2802 fn test_throttle_drops_output_arrays() {
2803 let pool = Arc::new(NDArrayPool::new(1_000_000));
2806 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2807 let mut output = NDArrayOutput::new();
2808 output.add(downstream_sender);
2809
2810 let (handle, _data_jh) = create_plugin_runtime_with_output(
2811 "THROTTLE_TEST",
2812 PassthroughProcessor,
2813 pool,
2814 10,
2815 output,
2816 "",
2817 test_wiring(),
2818 );
2819 enable_callbacks(&handle);
2820
2821 handle
2824 .port_runtime()
2825 .port_handle()
2826 .write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 8.0)
2827 .unwrap();
2828 std::thread::sleep(std::time::Duration::from_millis(20));
2829
2830 for id in 1..=5 {
2831 send_array(handle.array_sender(), make_test_array(id));
2832 }
2833 std::thread::sleep(std::time::Duration::from_millis(50));
2834
2835 let rt = tokio::runtime::Builder::new_current_thread()
2837 .enable_all()
2838 .build()
2839 .unwrap();
2840 let mut received = 0;
2841 while rt
2842 .block_on(async {
2843 tokio::time::timeout(std::time::Duration::from_millis(20), downstream_rx.recv())
2844 .await
2845 })
2846 .map(|o| o.is_some())
2847 .unwrap_or(false)
2848 {
2849 received += 1;
2850 }
2851 assert!(
2852 received < 5,
2853 "throttle must drop some arrays (got {received})"
2854 );
2855 assert!(received >= 1, "first array within budget must pass");
2856 }
2857
2858 #[test]
2859 fn test_process_plugin_reprocesses_last_input() {
2860 let pool = Arc::new(NDArrayPool::new(1_000_000));
2862 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2863 let mut output = NDArrayOutput::new();
2864 output.add(downstream_sender);
2865
2866 let (handle, _data_jh) = create_plugin_runtime_with_output(
2867 "PROCESS_PLUGIN_TEST",
2868 PassthroughProcessor,
2869 pool,
2870 10,
2871 output,
2872 "",
2873 test_wiring(),
2874 );
2875 enable_callbacks(&handle);
2876
2877 send_array(handle.array_sender(), make_test_array(7));
2878 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 7);
2879
2880 handle
2882 .port_runtime()
2883 .port_handle()
2884 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
2885 .unwrap();
2886 let reprocessed = downstream_rx.blocking_recv().unwrap();
2887 assert_eq!(
2888 reprocessed.unique_id, 7,
2889 "ProcessPlugin re-emits last input"
2890 );
2891 }
2892
2893 #[test]
2894 fn test_min_callback_time_throttle_not_counted() {
2895 let pool = Arc::new(NDArrayPool::new(1_000_000));
2903 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2904 let mut output = NDArrayOutput::new();
2905 output.add(downstream_sender);
2906
2907 let (handle, _data_jh) = create_plugin_runtime_with_output(
2908 "MIN_CB_TEST",
2909 PassthroughProcessor,
2910 pool,
2911 10,
2912 output,
2913 "",
2914 test_wiring(),
2915 );
2916 enable_callbacks(&handle);
2917 let dropped = handle.array_sender().dropped_arrays_counter().clone();
2918
2919 handle
2921 .port_runtime()
2922 .port_handle()
2923 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
2924 .unwrap();
2925 std::thread::sleep(std::time::Duration::from_millis(20));
2926
2927 send_array(handle.array_sender(), make_test_array(1));
2928 send_array(handle.array_sender(), make_test_array(2));
2929 std::thread::sleep(std::time::Duration::from_millis(50));
2930
2931 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
2932 let rt = tokio::runtime::Builder::new_current_thread()
2933 .enable_all()
2934 .build()
2935 .unwrap();
2936 let second = rt.block_on(async {
2937 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
2938 });
2939 assert!(
2940 second.is_err(),
2941 "second array throttled out by MinCallbackTime"
2942 );
2943 assert_eq!(
2944 dropped.load(Ordering::Acquire),
2945 0,
2946 "a MinCallbackTime-throttled frame must NOT increment DroppedArrays"
2947 );
2948 }
2949
2950 #[test]
2951 fn test_array_callbacks_zero_withholds_downstream_delivery() {
2952 let pool = Arc::new(NDArrayPool::new(1_000_000));
2958 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2959 let mut output = NDArrayOutput::new();
2960 output.add(downstream_sender);
2961
2962 let (handle, _data_jh) = create_plugin_runtime_with_output(
2963 "ARRAY_CB_TEST",
2964 PassthroughProcessor,
2965 pool,
2966 10,
2967 output,
2968 "",
2969 test_wiring(),
2970 );
2971 enable_callbacks(&handle);
2972 let port = handle.port_runtime().port_handle().clone();
2973
2974 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
2976 .unwrap();
2977 std::thread::sleep(std::time::Duration::from_millis(20));
2978
2979 send_array(handle.array_sender(), make_test_array(1));
2980 std::thread::sleep(std::time::Duration::from_millis(50));
2981
2982 let rt = tokio::runtime::Builder::new_current_thread()
2984 .enable_all()
2985 .build()
2986 .unwrap();
2987 let got = rt.block_on(async {
2988 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
2989 });
2990 assert!(
2991 got.is_err(),
2992 "NDArrayCallbacks=0 must withhold downstream delivery"
2993 );
2994 assert_eq!(
2996 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
2997 .unwrap(),
2998 1,
2999 "processing (and metadata params) must continue while delivery is off"
3000 );
3001
3002 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 1)
3004 .unwrap();
3005 std::thread::sleep(std::time::Duration::from_millis(20));
3006 send_array(handle.array_sender(), make_test_array(2));
3007 std::thread::sleep(std::time::Duration::from_millis(50));
3008 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3009 }
3010
3011 #[test]
3012 fn test_plugin_output_publishes_compressed_size() {
3013 struct CompressProcessor;
3019 impl NDPluginProcess for CompressProcessor {
3020 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3021 let mut out = array.clone();
3022 out.codec = Some(crate::codec::Codec {
3023 name: crate::codec::CodecName::JPEG,
3024 compressed_size: 7,
3025 level: 0,
3026 shuffle: 0,
3027 compressor: 0,
3028 original_data_type: NDDataType::UInt8,
3029 });
3030 ProcessResult::arrays(vec![Arc::new(out)])
3031 }
3032 fn plugin_type(&self) -> &str {
3033 "Compress"
3034 }
3035 }
3036
3037 {
3039 let pool = Arc::new(NDArrayPool::new(1_000_000));
3040 let (ds, _rx) = ndarray_channel("DS_RAW", 10);
3041 let mut output = NDArrayOutput::new();
3042 output.add(ds);
3043 let (handle, _jh) = create_plugin_runtime_with_output(
3044 "CODEC_RAW",
3045 PassthroughProcessor,
3046 pool,
3047 10,
3048 output,
3049 "",
3050 test_wiring(),
3051 );
3052 enable_callbacks(&handle);
3053 let port = handle.port_runtime().port_handle().clone();
3054 send_array(handle.array_sender(), make_test_array(1));
3055 std::thread::sleep(std::time::Duration::from_millis(50));
3056 assert_eq!(
3057 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3058 .unwrap(),
3059 4,
3060 "uncompressed output must publish CompressedSize == raw byte count"
3061 );
3062 }
3063
3064 {
3066 let pool = Arc::new(NDArrayPool::new(1_000_000));
3067 let (ds, _rx) = ndarray_channel("DS_CMP", 10);
3068 let mut output = NDArrayOutput::new();
3069 output.add(ds);
3070 let (handle, _jh) = create_plugin_runtime_with_output(
3071 "CODEC_CMP",
3072 CompressProcessor,
3073 pool,
3074 10,
3075 output,
3076 "",
3077 test_wiring(),
3078 );
3079 enable_callbacks(&handle);
3080 let port = handle.port_runtime().port_handle().clone();
3081 send_array(handle.array_sender(), make_test_array(1));
3082 std::thread::sleep(std::time::Duration::from_millis(50));
3083 assert_eq!(
3084 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3085 .unwrap(),
3086 7,
3087 "compressed output must publish CompressedSize == codec.compressed_size"
3088 );
3089 }
3090 }
3091
3092 #[test]
3093 fn test_process_plugin_skips_throttled_input() {
3094 let pool = Arc::new(NDArrayPool::new(1_000_000));
3099 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3100 let mut output = NDArrayOutput::new();
3101 output.add(downstream_sender);
3102
3103 let (handle, _data_jh) = create_plugin_runtime_with_output(
3104 "PROCESS_THROTTLE_TEST",
3105 PassthroughProcessor,
3106 pool,
3107 10,
3108 output,
3109 "",
3110 test_wiring(),
3111 );
3112 enable_callbacks(&handle);
3113
3114 handle
3116 .port_runtime()
3117 .port_handle()
3118 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3119 .unwrap();
3120 std::thread::sleep(std::time::Duration::from_millis(20));
3121
3122 send_array(handle.array_sender(), make_test_array(1));
3123 send_array(handle.array_sender(), make_test_array(2));
3124 std::thread::sleep(std::time::Duration::from_millis(50));
3125
3126 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3128
3129 handle
3134 .port_runtime()
3135 .port_handle()
3136 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 0.0)
3137 .unwrap();
3138 std::thread::sleep(std::time::Duration::from_millis(20));
3139 handle
3140 .port_runtime()
3141 .port_handle()
3142 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3143 .unwrap();
3144 let reprocessed = downstream_rx.blocking_recv().unwrap();
3145 assert_eq!(
3146 reprocessed.unique_id, 1,
3147 "ProcessPlugin must re-inject the last processed array (1), not the throttled array (2)"
3148 );
3149 }
3150
3151 #[test]
3152 fn test_g3_compressed_array_dropped_on_non_aware_plugin() {
3153 let pool = Arc::new(NDArrayPool::new(1_000_000));
3155 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3156 let mut output = NDArrayOutput::new();
3157 output.add(downstream_sender);
3158
3159 let (handle, _data_jh) = create_plugin_runtime_with_output(
3160 "G3_TEST",
3161 PassthroughProcessor, pool,
3163 10,
3164 output,
3165 "",
3166 test_wiring(),
3167 );
3168 enable_callbacks(&handle);
3169
3170 let mut compressed = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
3172 compressed.unique_id = 1;
3173 compressed.codec = Some(crate::codec::Codec {
3174 name: crate::codec::CodecName::JPEG,
3175 compressed_size: 16,
3176 level: 0,
3177 shuffle: 0,
3178 compressor: 0,
3179 original_data_type: NDDataType::UInt8,
3180 });
3181 send_array(handle.array_sender(), Arc::new(compressed));
3182
3183 send_array(handle.array_sender(), make_test_array(2));
3185
3186 let r = downstream_rx.blocking_recv().unwrap();
3187 assert_eq!(
3188 r.unique_id, 2,
3189 "compressed array dropped; only the raw array reaches downstream"
3190 );
3191 }
3192
3193 #[test]
3194 fn test_drop_on_full_increments_dropped_counter() {
3195 struct SlowProcessor;
3199 impl NDPluginProcess for SlowProcessor {
3200 fn process_array(&mut self, _a: &NDArray, _p: &NDArrayPool) -> ProcessResult {
3201 std::thread::sleep(std::time::Duration::from_millis(200));
3202 ProcessResult::empty()
3203 }
3204 fn plugin_type(&self) -> &str {
3205 "Slow"
3206 }
3207 }
3208 let pool = Arc::new(NDArrayPool::new(1_000_000));
3209
3210 let (downstream_handle, _ds_jh) =
3212 create_plugin_runtime("B1_DOWNSTREAM", SlowProcessor, pool, 1, "", test_wiring());
3213 enable_callbacks(&downstream_handle);
3214 let ds_sender = downstream_handle.array_sender().clone();
3215 let dropped = ds_sender.dropped_arrays_counter().clone();
3216
3217 send_array(&ds_sender, make_test_array(1));
3220 send_array(&ds_sender, make_test_array(2));
3221 send_array(&ds_sender, make_test_array(3));
3222 send_array(&ds_sender, make_test_array(4));
3223
3224 assert!(
3225 dropped.load(Ordering::Acquire) >= 1,
3226 "arrays dropped on a full queue must be counted (got {})",
3227 dropped.load(Ordering::Acquire)
3228 );
3229 }
3230
3231 #[test]
3232 fn test_cross_width_narrowing_array_read_truncates() {
3233 let mut out = [0i8; 1];
3242 let n = copy_ccast(&[300u16], &mut out);
3243 assert_eq!(n, 1);
3244 assert_eq!(out[0], 44, "(epicsInt8)(epicsUInt16)300 == 44 (low 8 bits)");
3245 let mut sat = [0i8; 1];
3247 copy_convert(&[300u16], &mut sat);
3248 assert_eq!(sat[0], 127, "f64 round-trip saturates — the wrong behavior");
3249
3250 let mut out2 = [0i8; 1];
3252 copy_ccast(&[0x1234_5678i32], &mut out2);
3253 assert_eq!(out2[0], 0x78);
3254
3255 let mut out3 = [0i8; 1];
3257 copy_ccast(&[-1i32], &mut out3);
3258 assert_eq!(out3[0], -1);
3259
3260 let mut out4 = [0i8; 1];
3262 copy_ccast(&[255u16], &mut out4);
3263 assert_eq!(out4[0], -1);
3264
3265 let mut out5 = [0i32; 1];
3267 copy_ccast(&[0x0000_0001_0000_002Ai64], &mut out5);
3268 assert_eq!(out5[0], 42);
3269
3270 let mut out6 = [0i16; 1];
3272 copy_ccast(&[70000u32], &mut out6);
3273 assert_eq!(out6[0], 4464);
3274
3275 let mut out7 = [0i8; 1];
3278 copy_ccast(&[255u8], &mut out7);
3279 assert_eq!(out7[0], -1);
3280
3281 let mut fout = [0i32; 1];
3287 copy_convert(&[42.9f64], &mut fout);
3288 assert_eq!(fout[0], 42, "f64 -> i32 truncates toward zero");
3289 }
3290
3291 fn block<F: std::future::Future>(f: F) -> F::Output {
3295 tokio::runtime::Builder::new_current_thread()
3296 .enable_all()
3297 .build()
3298 .unwrap()
3299 .block_on(f)
3300 }
3301
3302 #[test]
3303 fn test_scatter_reroutes_past_full_consumer() {
3304 let (sa, mut ra) = ndarray_channel("A", 1);
3309 let (sb, mut rb) = ndarray_channel("B", 1);
3310 let (sc, _rc) = ndarray_channel("C", 1);
3311 block(async {
3312 assert_eq!(
3313 sa.publish(make_test_array(99)).await,
3314 PublishOutcome::Delivered
3315 );
3316 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3317 let mut cursor = 0usize;
3318 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3319 assert_eq!(cursor, 2);
3321 assert_eq!(rb.recv().await.unwrap().unique_id, 1);
3322 assert_eq!(ra.recv().await.unwrap().unique_id, 99);
3324 assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3325 });
3326 }
3327
3328 #[test]
3329 fn test_scatter_drops_on_last_when_all_full_counts_once() {
3330 let (sa, mut ra) = ndarray_channel("A", 1);
3334 let (sb, mut rb) = ndarray_channel("B", 1);
3335 block(async {
3336 sa.publish(make_test_array(91)).await;
3337 sb.publish(make_test_array(92)).await;
3338 let senders = vec![sa.clone(), sb.clone()];
3339 let mut cursor = 0usize;
3340 ProcessOutput::scatter_publish(&make_test_array(7), &senders, &mut cursor).await;
3341 assert_eq!(cursor, 2); assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3344 assert_eq!(sb.dropped_arrays_counter().load(Ordering::Acquire), 1);
3345 assert_eq!(ra.recv().await.unwrap().unique_id, 91);
3347 assert_eq!(rb.recv().await.unwrap().unique_id, 92);
3348 });
3349 }
3350
3351 #[test]
3352 fn test_scatter_cursor_advances_per_attempt_across_frames() {
3353 let (sa, _ra) = ndarray_channel("A", 1);
3358 let (sb, mut rb) = ndarray_channel("B", 10);
3359 let (sc, mut rc) = ndarray_channel("C", 10);
3360 block(async {
3361 sa.publish(make_test_array(90)).await; let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3363 let mut cursor = 0usize;
3364 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3365 assert_eq!(cursor, 2); assert_eq!(rb.recv().await.unwrap().unique_id, 0);
3367 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3368 assert_eq!(cursor, 3); assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3370 });
3371 }
3372
3373 #[test]
3374 fn test_scatter_skips_disabled_consumer() {
3375 let (sa, mut ra) = ndarray_channel("A", 10);
3378 let (mut sb, _rb) = ndarray_channel("B", 10);
3379 let (sc, mut rc) = ndarray_channel("C", 10);
3380 sb.set_mode_flags(
3381 Arc::new(AtomicBool::new(false)),
3382 Arc::new(AtomicBool::new(false)),
3383 );
3384 block(async {
3385 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3386 let mut cursor = 0usize;
3387 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3389 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3390 assert_eq!(ra.recv().await.unwrap().unique_id, 0);
3391 assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3392 });
3393 }
3394}