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)]
46enum PluginParamMsg {
47 Change(usize, i32, ParamChangeValue),
49 Barrier(std::sync::mpsc::SyncSender<()>),
56}
57
58#[derive(Debug, Clone)]
60pub enum ParamChangeValue {
61 Int32(i32),
62 Float64(f64),
63 Octet(String),
64}
65
66impl ParamChangeValue {
67 pub fn as_i32(&self) -> i32 {
68 match self {
69 ParamChangeValue::Int32(v) => *v,
70 ParamChangeValue::Float64(v) => *v as i32,
71 ParamChangeValue::Octet(_) => 0,
72 }
73 }
74
75 pub fn as_f64(&self) -> f64 {
76 match self {
77 ParamChangeValue::Int32(v) => *v as f64,
78 ParamChangeValue::Float64(v) => *v,
79 ParamChangeValue::Octet(_) => 0.0,
80 }
81 }
82
83 pub fn as_string(&self) -> Option<&str> {
84 match self {
85 ParamChangeValue::Octet(s) => Some(s),
86 _ => None,
87 }
88 }
89}
90
91pub enum ParamUpdate {
93 Int32 {
94 reason: usize,
95 addr: i32,
96 value: i32,
97 },
98 Float64 {
99 reason: usize,
100 addr: i32,
101 value: f64,
102 },
103 Octet {
104 reason: usize,
105 addr: i32,
106 value: String,
107 },
108 Float64Array {
109 reason: usize,
110 addr: i32,
111 value: Vec<f64>,
112 },
113}
114
115impl ParamUpdate {
116 pub fn int32(reason: usize, value: i32) -> Self {
118 Self::Int32 {
119 reason,
120 addr: 0,
121 value,
122 }
123 }
124 pub fn float64(reason: usize, value: f64) -> Self {
126 Self::Float64 {
127 reason,
128 addr: 0,
129 value,
130 }
131 }
132 pub fn int32_addr(reason: usize, addr: i32, value: i32) -> Self {
134 Self::Int32 {
135 reason,
136 addr,
137 value,
138 }
139 }
140 pub fn float64_addr(reason: usize, addr: i32, value: f64) -> Self {
142 Self::Float64 {
143 reason,
144 addr,
145 value,
146 }
147 }
148 pub fn float64_array(reason: usize, value: Vec<f64>) -> Self {
150 Self::Float64Array {
151 reason,
152 addr: 0,
153 value,
154 }
155 }
156 pub fn float64_array_addr(reason: usize, addr: i32, value: Vec<f64>) -> Self {
158 Self::Float64Array {
159 reason,
160 addr,
161 value,
162 }
163 }
164 pub fn octet(reason: usize, value: String) -> Self {
166 Self::Octet {
167 reason,
168 addr: 0,
169 value,
170 }
171 }
172 pub fn octet_addr(reason: usize, addr: i32, value: String) -> Self {
174 Self::Octet {
175 reason,
176 addr,
177 value,
178 }
179 }
180}
181
182pub struct ProcessResult {
184 pub output_arrays: Vec<Arc<NDArray>>,
185 pub param_updates: Vec<ParamUpdate>,
186 pub scatter: bool,
193}
194
195impl ProcessResult {
196 pub fn sink(param_updates: Vec<ParamUpdate>) -> Self {
198 Self {
199 output_arrays: vec![],
200 param_updates,
201 scatter: false,
202 }
203 }
204
205 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
207 Self {
208 output_arrays,
209 param_updates: vec![],
210 scatter: false,
211 }
212 }
213
214 pub fn empty() -> Self {
216 Self {
217 output_arrays: vec![],
218 param_updates: vec![],
219 scatter: false,
220 }
221 }
222
223 pub fn scatter(output_arrays: Vec<Arc<NDArray>>) -> Self {
226 Self {
227 output_arrays,
228 param_updates: vec![],
229 scatter: true,
230 }
231 }
232}
233
234pub struct ParamChangeResult {
236 pub output_arrays: Vec<Arc<NDArray>>,
237 pub param_updates: Vec<ParamUpdate>,
238}
239
240impl ParamChangeResult {
241 pub fn updates(param_updates: Vec<ParamUpdate>) -> Self {
242 Self {
243 output_arrays: vec![],
244 param_updates,
245 }
246 }
247
248 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
249 Self {
250 output_arrays,
251 param_updates: vec![],
252 }
253 }
254
255 pub fn combined(output_arrays: Vec<Arc<NDArray>>, param_updates: Vec<ParamUpdate>) -> Self {
256 Self {
257 output_arrays,
258 param_updates,
259 }
260 }
261
262 pub fn empty() -> Self {
263 Self {
264 output_arrays: vec![],
265 param_updates: vec![],
266 }
267 }
268}
269
270pub trait NDPluginProcess: Send + 'static {
272 fn process_array(&mut self, array: &NDArray, pool: &NDArrayPool) -> ProcessResult;
274
275 fn plugin_type(&self) -> &str;
277
278 fn compression_aware(&self) -> bool {
284 false
285 }
286
287 fn does_array_callbacks(&self) -> bool {
294 true
295 }
296
297 fn register_params(
299 &mut self,
300 _base: &mut PortDriverBase,
301 ) -> Result<(), asyn_rs::error::AsynError> {
302 Ok(())
303 }
304
305 fn on_param_change(
308 &mut self,
309 _reason: usize,
310 _params: &PluginParamSnapshot,
311 ) -> ParamChangeResult {
312 ParamChangeResult::empty()
313 }
314
315 fn array_data_handle(&self) -> Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>> {
319 None
320 }
321}
322
323pub struct PluginParamSnapshot {
325 pub enable_callbacks: bool,
326 pub reason: usize,
328 pub addr: i32,
330 pub value: ParamChangeValue,
332}
333
334struct SortEntry {
338 arrays: Vec<Arc<NDArray>>,
339 inserted: std::time::Instant,
340}
341
342struct SortBuffer {
350 entries: BTreeMap<i32, SortEntry>,
352 prev_unique_id: i32,
354 first_output: bool,
356 disordered_arrays: i32,
358 dropped_output_arrays: i32,
361}
362
363impl SortBuffer {
364 fn new() -> Self {
365 Self {
366 entries: BTreeMap::new(),
367 prev_unique_id: 0,
368 first_output: true,
369 disordered_arrays: 0,
370 dropped_output_arrays: 0,
371 }
372 }
373
374 fn order_ok(&self, unique_id: i32) -> bool {
376 unique_id == self.prev_unique_id || unique_id == self.prev_unique_id + 1
377 }
378
379 fn note_emitted(&mut self, unique_id: i32) {
382 if !self.first_output && !self.order_ok(unique_id) {
383 self.disordered_arrays += 1;
384 }
385 self.first_output = false;
386 self.prev_unique_id = unique_id;
387 }
388
389 fn insert(&mut self, unique_id: i32, arrays: Vec<Arc<NDArray>>, sort_size: i32) -> bool {
394 if sort_size > 0 && self.entries.len() as i32 >= sort_size {
395 self.dropped_output_arrays += 1;
396 return false;
397 }
398 self.entries
399 .entry(unique_id)
400 .or_insert_with(|| SortEntry {
401 arrays: Vec::new(),
402 inserted: std::time::Instant::now(),
403 })
404 .arrays
405 .extend(arrays);
406 true
407 }
408
409 fn drain_ready(&mut self, sort_time: f64) -> Vec<(i32, Vec<Arc<NDArray>>)> {
413 let now = std::time::Instant::now();
414 let mut out = Vec::new();
415 while let Some((&head_id, entry)) = self.entries.iter().next() {
416 let delta = now.duration_since(entry.inserted).as_secs_f64();
417 let order_ok = self.order_ok(head_id);
418 if (!self.first_output && order_ok) || delta > sort_time {
419 let entry = self.entries.remove(&head_id).unwrap();
420 self.note_emitted(head_id);
421 out.push((head_id, entry.arrays));
422 } else {
423 break;
424 }
425 }
426 out
427 }
428
429 fn drain_all(&mut self) -> Vec<(i32, Vec<Arc<NDArray>>)> {
432 let entries = std::mem::take(&mut self.entries);
433 let mut out = Vec::with_capacity(entries.len());
434 for (id, entry) in entries {
435 self.note_emitted(id);
436 out.push((id, entry.arrays));
437 }
438 out
439 }
440
441 fn len(&self) -> i32 {
443 self.entries.len() as i32
444 }
445}
446
447struct SharedProcessorInner<P: NDPluginProcess> {
450 processor: P,
451 output: Arc<parking_lot::Mutex<NDArrayOutput>>,
452 pool: Arc<NDArrayPool>,
453 ndarray_params: NDArrayDriverParams,
454 plugin_params: PluginBaseParams,
455 port_handle: PortHandle,
456 array_counter: i32,
460 std_array_data_param: Option<usize>,
462 array_callbacks: bool,
469 min_callback_time: f64,
471 last_process_time: Option<std::time::Instant>,
473 sort_mode: i32,
475 sort_time: f64,
477 sort_size: i32,
479 sort_buffer: SortBuffer,
481 dropped_arrays: Arc<std::sync::atomic::AtomicI32>,
485 compression_aware: bool,
488 max_byte_rate: f64,
490 throttler: super::throttler::Throttler,
492 prev_input_array: Option<Arc<NDArray>>,
495 dims_prev: Vec<i32>,
498 nd_array_addr: i32,
500 max_threads: i32,
502 num_threads: i32,
504}
505
506impl<P: NDPluginProcess> SharedProcessorInner<P> {
507 fn should_throttle(&self) -> bool {
508 if self.min_callback_time <= 0.0 {
509 return false;
510 }
511 if let Some(last) = self.last_process_time {
512 last.elapsed().as_secs_f64() < self.min_callback_time
513 } else {
514 false
515 }
516 }
517
518 fn array_byte_cost(array: &NDArray) -> f64 {
521 match &array.codec {
522 Some(c) => c.compressed_size as f64,
523 None => array.info().total_bytes as f64,
524 }
525 }
526
527 fn throttle_ok(&mut self, array: &NDArray) -> bool {
530 if self.max_byte_rate == 0.0 {
531 return true;
532 }
533 let cost = Self::array_byte_cost(array);
534 if self.throttler.try_take(cost) {
535 true
536 } else {
537 self.sort_buffer.dropped_output_arrays += 1;
538 false
539 }
540 }
541
542 fn route_output_arrays(&mut self, arrays: Vec<Arc<NDArray>>) -> Vec<Arc<NDArray>> {
550 let mut ready = Vec::new();
551 for arr in arrays {
552 if !self.throttle_ok(&arr) {
553 continue; }
555 let uid = arr.unique_id;
556 if self.sort_mode != 0
557 && !self.sort_buffer.first_output
558 && !self.sort_buffer.order_ok(uid)
559 {
560 self.sort_buffer.insert(uid, vec![arr], self.sort_size);
562 } else {
563 self.sort_buffer.note_emitted(uid);
565 ready.push(arr);
566 }
567 }
568 if self.sort_mode != 0 {
571 for (_id, mut bucket) in self.sort_buffer.drain_ready(self.sort_time) {
572 ready.append(&mut bucket);
573 }
574 }
575 ready
576 }
577
578 fn process_and_publish(&mut self, array: &Arc<NDArray>) -> Option<ProcessOutput> {
582 if self.should_throttle() {
589 return None;
590 }
591 self.prev_input_array = Some(Arc::clone(array));
596 let t0 = std::time::Instant::now();
597 let result = self.processor.process_array(array, &self.pool);
598 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
599 self.last_process_time = Some(t0);
600
601 let produced = result.output_arrays.len();
615 let ready = if self.array_callbacks || self.std_array_data_param.is_some() {
616 self.route_output_arrays(result.output_arrays)
617 } else {
618 Vec::new()
619 };
620 let count_frame =
625 !(self.std_array_data_param.is_some() && produced > 0 && ready.is_empty());
626 let mut output = self.build_publish_batch(
627 ready,
628 result.param_updates,
629 result.scatter,
630 Some(array.as_ref()),
631 elapsed_ms,
632 self.array_callbacks,
633 count_frame,
634 );
635 output.batch.merge(self.build_status_params_batch());
636 Some(output)
637 }
638
639 fn dropped_arrays_only_batch(&self) -> ProcessOutput {
642 ProcessOutput {
643 arrays: vec![],
644 scatter: false,
645 batch: self.build_status_params_batch(),
646 }
647 }
648
649 fn process_plugin(&mut self) -> Option<ProcessOutput> {
652 let prev = self.prev_input_array.clone()?;
653 self.process_and_publish(&prev)
654 }
655
656 fn tick_sort_buffer(&mut self) -> ProcessOutput {
659 let entries = self.sort_buffer.drain_ready(self.sort_time);
660 self.emit_drained(entries)
661 }
662
663 fn flush_sort_buffer(&mut self) -> ProcessOutput {
665 let entries = self.sort_buffer.drain_all();
666 self.emit_drained(entries)
667 }
668
669 fn emit_drained(&mut self, entries: Vec<(i32, Vec<Arc<NDArray>>)>) -> ProcessOutput {
670 let mut all_arrays = Vec::new();
671 let mut combined = ParamBatch::empty();
672 for (_unique_id, arrays) in entries {
673 let output = self.build_publish_batch(arrays, vec![], false, None, 0.0, true, true);
678 all_arrays.extend(output.arrays);
679 combined.merge(output.batch);
680 }
681 combined.merge(self.build_sort_params_batch());
682 ProcessOutput {
683 arrays: all_arrays,
684 scatter: false,
685 batch: combined,
686 }
687 }
688
689 fn build_sort_params_batch(&self) -> ParamBatch {
690 use asyn_rs::request::ParamSetValue;
691 let sort_free = self.sort_size - self.sort_buffer.len();
692 ParamBatch {
693 addr0: vec![
694 ParamSetValue::Int32 {
695 reason: self.plugin_params.sort_free,
696 addr: 0,
697 value: sort_free,
698 },
699 ParamSetValue::Int32 {
700 reason: self.plugin_params.disordered_arrays,
701 addr: 0,
702 value: self.sort_buffer.disordered_arrays,
703 },
704 ParamSetValue::Int32 {
705 reason: self.plugin_params.dropped_output_arrays,
706 addr: 0,
707 value: self.sort_buffer.dropped_output_arrays,
708 },
709 ],
710 extra: std::collections::HashMap::new(),
711 }
712 }
713
714 fn build_status_params_batch(&self) -> ParamBatch {
717 use asyn_rs::request::ParamSetValue;
718 let mut batch = self.build_sort_params_batch();
719 batch.addr0.push(ParamSetValue::Int32 {
720 reason: self.plugin_params.dropped_arrays,
721 addr: 0,
722 value: self
723 .dropped_arrays
724 .load(std::sync::atomic::Ordering::Acquire),
725 });
726 batch
727 }
728
729 fn build_publish_batch(
739 &mut self,
740 output_arrays: Vec<Arc<NDArray>>,
741 param_updates: Vec<ParamUpdate>,
742 scatter: bool,
743 fallback_array: Option<&NDArray>,
744 elapsed_ms: f64,
745 deliver: bool,
746 count_frame: bool,
747 ) -> ProcessOutput {
748 use asyn_rs::request::ParamSetValue;
749
750 let mut addr0: Vec<ParamSetValue> = Vec::new();
751 let mut extra: std::collections::HashMap<i32, Vec<ParamSetValue>> =
752 std::collections::HashMap::new();
753
754 if let Some(report_arr) = output_arrays.first().map(|a| a.as_ref()).or(fallback_array) {
755 if count_frame {
760 self.array_counter += 1;
761 }
762
763 if let (Some(param), Some(served)) = (
774 self.std_array_data_param,
775 output_arrays.first().map(|a| a.as_ref()),
776 ) {
777 use crate::ndarray::NDDataBuffer;
778 use asyn_rs::param::ParamValue;
779 let value = match &served.data {
780 NDDataBuffer::I8(v) => {
781 Some(ParamValue::Int8Array(std::sync::Arc::from(v.as_slice())))
782 }
783 NDDataBuffer::U8(v) => Some(ParamValue::Int8Array(std::sync::Arc::from(
784 v.iter().map(|&x| x as i8).collect::<Vec<_>>().as_slice(),
785 ))),
786 NDDataBuffer::I16(v) => {
787 Some(ParamValue::Int16Array(std::sync::Arc::from(v.as_slice())))
788 }
789 NDDataBuffer::U16(v) => Some(ParamValue::Int16Array(std::sync::Arc::from(
790 v.iter().map(|&x| x as i16).collect::<Vec<_>>().as_slice(),
791 ))),
792 NDDataBuffer::I32(v) => {
793 Some(ParamValue::Int32Array(std::sync::Arc::from(v.as_slice())))
794 }
795 NDDataBuffer::U32(v) => Some(ParamValue::Int32Array(std::sync::Arc::from(
796 v.iter().map(|&x| x as i32).collect::<Vec<_>>().as_slice(),
797 ))),
798 NDDataBuffer::I64(v) => {
799 Some(ParamValue::Int64Array(std::sync::Arc::from(v.as_slice())))
800 }
801 NDDataBuffer::U64(v) => Some(ParamValue::Int64Array(std::sync::Arc::from(
802 v.iter().map(|&x| x as i64).collect::<Vec<_>>().as_slice(),
803 ))),
804 NDDataBuffer::F32(v) => {
805 Some(ParamValue::Float32Array(std::sync::Arc::from(v.as_slice())))
806 }
807 NDDataBuffer::F64(v) => {
808 Some(ParamValue::Float64Array(std::sync::Arc::from(v.as_slice())))
809 }
810 };
811 if let Some(value) = value {
812 let ts = served.timestamp.to_system_time();
813 self.port_handle
814 .interrupts()
815 .notify(asyn_rs::interrupt::InterruptValue {
816 reason: param,
817 addr: 0,
818 value,
819 timestamp: ts,
820 uint32_changed_mask: 0,
821 ..Default::default()
822 });
823 }
824 }
825
826 let info = report_arr.info();
827 let color_mode = report_arr
831 .attributes
832 .get("ColorMode")
833 .and_then(|a| a.value.as_i64())
834 .map(|v| v as i32)
835 .unwrap_or(info.color_mode as i32);
836 let bayer_pattern = report_arr
837 .attributes
838 .get("BayerPattern")
839 .and_then(|a| a.value.as_i64())
840 .map(|v| v as i32)
841 .unwrap_or(0);
842
843 let mut cur_dims = vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS];
850 for (slot, d) in cur_dims.iter_mut().zip(
851 report_arr
852 .dims
853 .iter()
854 .take(crate::ndarray::ND_ARRAY_MAX_DIMS),
855 ) {
856 *slot = d.size as i32;
857 }
858 if cur_dims != self.dims_prev {
859 self.dims_prev = cur_dims.clone();
860 self.port_handle
861 .interrupts()
862 .notify(asyn_rs::interrupt::InterruptValue {
863 reason: self.ndarray_params.array_dimensions,
864 addr: 0,
865 value: asyn_rs::param::ParamValue::Int32Array(std::sync::Arc::from(
866 cur_dims.as_slice(),
867 )),
868 timestamp: report_arr.timestamp.to_system_time(),
869 uint32_changed_mask: 0,
870 ..Default::default()
871 });
872 }
873
874 addr0.extend([
875 ParamSetValue::Int32 {
876 reason: self.ndarray_params.array_counter,
877 addr: 0,
878 value: self.array_counter,
879 },
880 ParamSetValue::Int32 {
881 reason: self.ndarray_params.unique_id,
882 addr: 0,
883 value: report_arr.unique_id,
884 },
885 ParamSetValue::Int32 {
886 reason: self.ndarray_params.n_dimensions,
887 addr: 0,
888 value: report_arr.dims.len() as i32,
889 },
890 ParamSetValue::Int32 {
891 reason: self.ndarray_params.array_size_x,
892 addr: 0,
893 value: info.x_size as i32,
894 },
895 ParamSetValue::Int32 {
896 reason: self.ndarray_params.array_size_y,
897 addr: 0,
898 value: info.y_size as i32,
899 },
900 ParamSetValue::Int32 {
901 reason: self.ndarray_params.array_size_z,
902 addr: 0,
903 value: info.color_size as i32,
904 },
905 ParamSetValue::Int32 {
906 reason: self.ndarray_params.array_size,
907 addr: 0,
908 value: info.total_bytes as i32,
909 },
910 ParamSetValue::Int32 {
911 reason: self.ndarray_params.data_type,
912 addr: 0,
913 value: report_arr.data.data_type() as i32,
914 },
915 ParamSetValue::Int32 {
916 reason: self.ndarray_params.color_mode,
917 addr: 0,
918 value: color_mode,
919 },
920 ParamSetValue::Int32 {
921 reason: self.ndarray_params.bayer_pattern,
922 addr: 0,
923 value: bayer_pattern,
924 },
925 ParamSetValue::Float64 {
926 reason: self.ndarray_params.timestamp_rbv,
927 addr: 0,
928 value: report_arr.timestamp.as_f64(),
929 },
930 ParamSetValue::Int32 {
931 reason: self.ndarray_params.epics_ts_sec,
932 addr: 0,
933 value: report_arr.timestamp.sec as i32,
934 },
935 ParamSetValue::Int32 {
936 reason: self.ndarray_params.epics_ts_nsec,
937 addr: 0,
938 value: report_arr.timestamp.nsec as i32,
939 },
940 ]);
941
942 match &report_arr.codec {
948 Some(codec) => {
949 addr0.push(ParamSetValue::Octet {
950 reason: self.ndarray_params.codec,
951 addr: 0,
952 value: codec.name.as_str().to_string(),
953 });
954 addr0.push(ParamSetValue::Int32 {
955 reason: self.ndarray_params.compressed_size,
956 addr: 0,
957 value: codec.compressed_size as i32,
958 });
959 }
960 None => {
961 addr0.push(ParamSetValue::Octet {
962 reason: self.ndarray_params.codec,
963 addr: 0,
964 value: String::new(),
965 });
966 addr0.push(ParamSetValue::Int32 {
967 reason: self.ndarray_params.compressed_size,
968 addr: 0,
969 value: info.total_bytes as i32,
970 });
971 }
972 }
973 }
974
975 addr0.push(ParamSetValue::Float64 {
976 reason: self.plugin_params.execution_time,
977 addr: 0,
978 value: elapsed_ms,
979 });
980
981 for update in ¶m_updates {
986 match update {
987 ParamUpdate::Int32 {
988 reason,
989 addr,
990 value,
991 } => {
992 let pv = ParamSetValue::Int32 {
993 reason: *reason,
994 addr: *addr,
995 value: *value,
996 };
997 if *addr == 0 {
998 addr0.push(pv);
999 } else {
1000 extra.entry(*addr).or_default().push(pv);
1001 }
1002 }
1003 ParamUpdate::Float64 {
1004 reason,
1005 addr,
1006 value,
1007 } => {
1008 let pv = ParamSetValue::Float64 {
1009 reason: *reason,
1010 addr: *addr,
1011 value: *value,
1012 };
1013 if *addr == 0 {
1014 addr0.push(pv);
1015 } else {
1016 extra.entry(*addr).or_default().push(pv);
1017 }
1018 }
1019 ParamUpdate::Octet {
1020 reason,
1021 addr,
1022 value,
1023 } => {
1024 let pv = ParamSetValue::Octet {
1025 reason: *reason,
1026 addr: *addr,
1027 value: value.clone(),
1028 };
1029 if *addr == 0 {
1030 addr0.push(pv);
1031 } else {
1032 extra.entry(*addr).or_default().push(pv);
1033 }
1034 }
1035 ParamUpdate::Float64Array {
1036 reason,
1037 addr,
1038 value,
1039 } => {
1040 let pv = ParamSetValue::Float64Array {
1041 reason: *reason,
1042 addr: *addr,
1043 value: value.clone(),
1044 };
1045 if *addr == 0 {
1046 addr0.push(pv);
1047 } else {
1048 extra.entry(*addr).or_default().push(pv);
1049 }
1050 }
1051 }
1052 }
1053
1054 ProcessOutput {
1055 arrays: if deliver { output_arrays } else { Vec::new() },
1059 scatter,
1060 batch: ParamBatch { addr0, extra },
1061 }
1062 }
1063}
1064
1065struct ProcessOutput {
1067 arrays: Vec<Arc<NDArray>>,
1068 scatter: bool,
1069 batch: ParamBatch,
1070}
1071
1072impl ProcessOutput {
1073 async fn publish_arrays(&self, senders: &[NDArraySender], scatter_cursor: &mut usize) {
1081 for arr in &self.arrays {
1082 if self.scatter {
1083 Self::scatter_publish(arr, senders, scatter_cursor).await;
1084 } else {
1085 let futs = senders.iter().map(|s| s.publish(arr.clone()));
1086 futures_util::future::join_all(futs).await;
1087 }
1088 }
1089 }
1090
1091 async fn scatter_publish(arr: &Arc<NDArray>, senders: &[NDArraySender], cursor: &mut usize) {
1111 let active: Vec<&NDArraySender> = senders.iter().filter(|s| s.is_enabled()).collect();
1112 let n = active.len();
1113 if n == 0 {
1114 return;
1115 }
1116 for attempt in 0..n {
1117 let target = *cursor % n;
1118 *cursor = cursor.wrapping_add(1);
1119 let is_last = attempt == n - 1;
1120 match active[target].publish_scatter(arr.clone(), is_last).await {
1121 PublishOutcome::Delivered => break,
1125 PublishOutcome::DroppedQueueFull
1129 | PublishOutcome::Disabled
1130 | PublishOutcome::ChannelClosed => {
1131 if is_last {
1132 break;
1133 }
1134 }
1135 }
1136 }
1137 }
1138}
1139
1140struct ParamBatch {
1143 addr0: Vec<asyn_rs::request::ParamSetValue>,
1144 extra: std::collections::HashMap<i32, Vec<asyn_rs::request::ParamSetValue>>,
1145}
1146
1147impl ParamBatch {
1148 fn empty() -> Self {
1149 Self {
1150 addr0: Vec::new(),
1151 extra: std::collections::HashMap::new(),
1152 }
1153 }
1154
1155 fn merge(&mut self, other: ParamBatch) {
1156 self.addr0.extend(other.addr0);
1157 for (addr, updates) in other.extra {
1158 self.extra.entry(addr).or_default().extend(updates);
1159 }
1160 }
1161
1162 async fn flush(self, port: &asyn_rs::port_handle::PortHandle) {
1164 if !self.addr0.is_empty() {
1165 if let Err(e) = port.set_params_and_notify(0, self.addr0).await {
1166 eprintln!("plugin param flush error (addr 0): {e}");
1167 }
1168 }
1169 for (addr, updates) in self.extra {
1170 if let Err(e) = port.set_params_and_notify(addr, updates).await {
1171 eprintln!("plugin param flush error (addr {addr}): {e}");
1172 }
1173 }
1174 }
1175}
1176
1177#[allow(dead_code)]
1179pub struct PluginPortDriver {
1180 base: PortDriverBase,
1181 ndarray_params: NDArrayDriverParams,
1182 plugin_params: PluginBaseParams,
1183 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1184 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1186 std_array_data_param: Option<usize>,
1188}
1189
1190impl PluginPortDriver {
1191 fn new<P: NDPluginProcess>(
1192 port_name: &str,
1193 plugin_type_name: &str,
1194 queue_size: usize,
1195 ndarray_port: &str,
1196 max_addr: usize,
1197 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1198 processor: &mut P,
1199 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1200 ) -> AsynResult<Self> {
1201 let mut base = PortDriverBase::new(
1202 port_name,
1203 max_addr,
1204 PortFlags {
1205 can_block: true,
1206 ..Default::default()
1207 },
1208 );
1209
1210 let ndarray_params = NDArrayDriverParams::create(&mut base)?;
1211 let plugin_params = PluginBaseParams::create(&mut base)?;
1212
1213 base.set_int32_param(plugin_params.enable_callbacks, 0, 0)?;
1215 base.set_int32_param(plugin_params.blocking_callbacks, 0, 0)?;
1216 base.set_int32_param(plugin_params.queue_size, 0, queue_size as i32)?;
1217 base.set_int32_param(plugin_params.dropped_arrays, 0, 0)?;
1218 base.set_int32_param(plugin_params.queue_use, 0, 0)?;
1219 base.set_string_param(plugin_params.plugin_type, 0, plugin_type_name.into())?;
1220 base.set_int32_param(
1221 ndarray_params.array_callbacks,
1222 0,
1223 processor.does_array_callbacks() as i32,
1224 )?;
1225 base.set_int32_param(ndarray_params.write_file, 0, 0)?;
1226 base.set_int32_param(ndarray_params.read_file, 0, 0)?;
1227 base.set_int32_param(ndarray_params.capture, 0, 0)?;
1228 base.set_int32_param(ndarray_params.file_write_status, 0, 0)?;
1229 base.set_string_param(ndarray_params.file_write_message, 0, "".into())?;
1230 base.set_string_param(ndarray_params.file_path, 0, "".into())?;
1231 base.set_string_param(ndarray_params.file_name, 0, "".into())?;
1232 base.set_int32_param(ndarray_params.file_number, 0, 0)?;
1233 base.set_int32_param(ndarray_params.auto_increment, 0, 0)?;
1234 base.set_string_param(ndarray_params.file_template, 0, "%s%s_%3.3d.dat".into())?;
1235 base.set_string_param(ndarray_params.full_file_name, 0, "".into())?;
1236 base.set_int32_param(ndarray_params.create_dir, 0, 0)?;
1237 base.set_string_param(ndarray_params.temp_suffix, 0, "".into())?;
1238
1239 base.set_string_param(ndarray_params.port_name_self, 0, port_name.into())?;
1241 base.set_string_param(
1242 ndarray_params.ad_core_version,
1243 0,
1244 env!("CARGO_PKG_VERSION").into(),
1245 )?;
1246 base.set_string_param(
1247 ndarray_params.driver_version,
1248 0,
1249 env!("CARGO_PKG_VERSION").into(),
1250 )?;
1251 if !ndarray_port.is_empty() {
1252 base.set_string_param(plugin_params.nd_array_port, 0, ndarray_port.into())?;
1253 }
1254
1255 let std_array_data_param = if array_data.is_some() {
1257 Some(base.create_param("STD_ARRAY_DATA", asyn_rs::param::ParamType::GenericPointer)?)
1258 } else {
1259 None
1260 };
1261
1262 processor.register_params(&mut base)?;
1264
1265 Ok(Self {
1266 base,
1267 ndarray_params,
1268 plugin_params,
1269 param_change_tx,
1270 array_data,
1271 std_array_data_param,
1272 })
1273 }
1274}
1275
1276fn copy_direct<T: Copy>(src: &[T], dst: &mut [T]) -> usize {
1278 let n = src.len().min(dst.len());
1279 dst[..n].copy_from_slice(&src[..n]);
1280 n
1281}
1282
1283fn copy_convert<S, D>(src: &[S], dst: &mut [D]) -> usize
1285where
1286 S: CastToF64 + Copy,
1287 D: CastFromF64 + Copy,
1288{
1289 let n = src.len().min(dst.len());
1290 for i in 0..n {
1291 dst[i] = D::cast_from_f64(src[i].cast_to_f64());
1292 }
1293 n
1294}
1295
1296trait CCastTo<D> {
1312 fn ccast(self) -> D;
1313}
1314macro_rules! impl_ccast {
1315 ( $src:ty => $( $dst:ty ),+ ) => {
1316 $(
1317 impl CCastTo<$dst> for $src {
1318 #[inline]
1319 fn ccast(self) -> $dst {
1320 self as $dst
1321 }
1322 }
1323 )+
1324 };
1325}
1326impl_ccast!(i8 => i16, i32, i64);
1327impl_ccast!(u8 => i8, i16, i32, i64);
1328impl_ccast!(i16 => i8, i32, i64);
1329impl_ccast!(u16 => i8, i16, i32, i64);
1330impl_ccast!(i32 => i8, i16, i64);
1331impl_ccast!(u32 => i8, i16, i32, i64);
1332impl_ccast!(i64 => i8, i16, i32);
1333impl_ccast!(u64 => i8, i16, i32, i64);
1334
1335fn copy_ccast<S, D>(src: &[S], dst: &mut [D]) -> usize
1339where
1340 S: CCastTo<D> + Copy,
1341 D: Copy,
1342{
1343 let n = src.len().min(dst.len());
1344 for i in 0..n {
1345 dst[i] = src[i].ccast();
1346 }
1347 n
1348}
1349
1350trait CastToF64 {
1352 fn cast_to_f64(self) -> f64;
1353}
1354
1355impl CastToF64 for i8 {
1356 fn cast_to_f64(self) -> f64 {
1357 self as f64
1358 }
1359}
1360impl CastToF64 for u8 {
1361 fn cast_to_f64(self) -> f64 {
1362 self as f64
1363 }
1364}
1365impl CastToF64 for i16 {
1366 fn cast_to_f64(self) -> f64 {
1367 self as f64
1368 }
1369}
1370impl CastToF64 for u16 {
1371 fn cast_to_f64(self) -> f64 {
1372 self as f64
1373 }
1374}
1375impl CastToF64 for i32 {
1376 fn cast_to_f64(self) -> f64 {
1377 self as f64
1378 }
1379}
1380impl CastToF64 for u32 {
1381 fn cast_to_f64(self) -> f64 {
1382 self as f64
1383 }
1384}
1385impl CastToF64 for i64 {
1386 fn cast_to_f64(self) -> f64 {
1387 self as f64
1388 }
1389}
1390impl CastToF64 for u64 {
1391 fn cast_to_f64(self) -> f64 {
1392 self as f64
1393 }
1394}
1395impl CastToF64 for f32 {
1396 fn cast_to_f64(self) -> f64 {
1397 self as f64
1398 }
1399}
1400impl CastToF64 for f64 {
1401 fn cast_to_f64(self) -> f64 {
1402 self
1403 }
1404}
1405
1406trait CastFromF64 {
1408 fn cast_from_f64(v: f64) -> Self;
1409}
1410
1411impl CastFromF64 for i8 {
1412 fn cast_from_f64(v: f64) -> Self {
1413 v as i8
1414 }
1415}
1416impl CastFromF64 for i16 {
1417 fn cast_from_f64(v: f64) -> Self {
1418 v as i16
1419 }
1420}
1421impl CastFromF64 for i32 {
1422 fn cast_from_f64(v: f64) -> Self {
1423 v as i32
1424 }
1425}
1426impl CastFromF64 for i64 {
1427 fn cast_from_f64(v: f64) -> Self {
1428 v as i64
1429 }
1430}
1431impl CastFromF64 for f32 {
1432 fn cast_from_f64(v: f64) -> Self {
1433 v as f32
1434 }
1435}
1436impl CastFromF64 for f64 {
1437 fn cast_from_f64(v: f64) -> Self {
1438 v
1439 }
1440}
1441
1442macro_rules! impl_read_array {
1445 (
1446 $self:expr, $buf:expr, $direct_variant:ident,
1447 ccast: [ $( $ccast_variant:ident ),* ],
1448 convert: [ $( $variant:ident ),* ]
1449 ) => {{
1450 use crate::ndarray::NDDataBuffer;
1451 let handle = match &$self.array_data {
1452 Some(h) => h,
1453 None => return Ok(0),
1454 };
1455 let guard = handle.lock();
1456 let array = match &*guard {
1457 Some(a) => a,
1458 None => return Ok(0),
1459 };
1460 let n = match &array.data {
1461 NDDataBuffer::$direct_variant(v) => copy_direct(v, $buf),
1462 $( NDDataBuffer::$ccast_variant(v) => copy_ccast(v, $buf), )*
1463 $( NDDataBuffer::$variant(v) => copy_convert(v, $buf), )*
1464 };
1465 Ok(n)
1466 }};
1467}
1468
1469impl PortDriver for PluginPortDriver {
1470 fn base(&self) -> &PortDriverBase {
1471 &self.base
1472 }
1473
1474 fn base_mut(&mut self) -> &mut PortDriverBase {
1475 &mut self.base
1476 }
1477
1478 fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1479 let reason = user.reason;
1480 let addr = user.addr;
1481 self.base.set_int32_param(reason, addr, value)?;
1482 self.base.call_param_callbacks(addr)?;
1483 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1485 reason,
1486 addr,
1487 ParamChangeValue::Int32(value),
1488 ));
1489 Ok(())
1490 }
1491
1492 fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1493 let reason = user.reason;
1494 let addr = user.addr;
1495 self.base.set_float64_param(reason, addr, value)?;
1496 self.base.call_param_callbacks(addr)?;
1497 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1498 reason,
1499 addr,
1500 ParamChangeValue::Float64(value),
1501 ));
1502 Ok(())
1503 }
1504
1505 fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1506 let reason = user.reason;
1507 let addr = user.addr;
1508 let s = String::from_utf8_lossy(data).into_owned();
1509 self.base.set_string_param(reason, addr, s.clone())?;
1510 self.base.call_param_callbacks(addr)?;
1511 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1512 reason,
1513 addr,
1514 ParamChangeValue::Octet(s),
1515 ));
1516 Ok(data.len())
1517 }
1518
1519 fn read_int8_array(&mut self, _user: &AsynUser, buf: &mut [i8]) -> AsynResult<usize> {
1520 impl_read_array!(
1523 self, buf, I8,
1524 ccast: [U8, I16, U16, I32, U32, I64, U64],
1525 convert: [F32, F64]
1526 )
1527 }
1528
1529 fn read_int16_array(&mut self, _user: &AsynUser, buf: &mut [i16]) -> AsynResult<usize> {
1530 impl_read_array!(
1531 self, buf, I16,
1532 ccast: [I8, U8, U16, I32, U32, I64, U64],
1533 convert: [F32, F64]
1534 )
1535 }
1536
1537 fn read_int32_array(&mut self, _user: &AsynUser, buf: &mut [i32]) -> AsynResult<usize> {
1538 impl_read_array!(
1539 self, buf, I32,
1540 ccast: [I8, U8, I16, U16, U32, I64, U64],
1541 convert: [F32, F64]
1542 )
1543 }
1544
1545 fn read_int64_array(&mut self, _user: &AsynUser, buf: &mut [i64]) -> AsynResult<usize> {
1546 impl_read_array!(
1547 self, buf, I64,
1548 ccast: [I8, U8, I16, U16, I32, U32, U64],
1549 convert: [F32, F64]
1550 )
1551 }
1552
1553 fn read_float32_array(&mut self, _user: &AsynUser, buf: &mut [f32]) -> AsynResult<usize> {
1554 impl_read_array!(
1555 self, buf, F32,
1556 ccast: [],
1557 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F64]
1558 )
1559 }
1560
1561 fn read_float64_array(&mut self, _user: &AsynUser, buf: &mut [f64]) -> AsynResult<usize> {
1562 impl_read_array!(
1563 self, buf, F64,
1564 ccast: [],
1565 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F32]
1566 )
1567 }
1568}
1569
1570#[derive(Clone)]
1572pub struct PluginRuntimeHandle {
1573 port_runtime: PortRuntimeHandle,
1574 array_sender: NDArraySender,
1575 array_output: Arc<parking_lot::Mutex<NDArrayOutput>>,
1576 port_name: String,
1577 param_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1578 pub ndarray_params: NDArrayDriverParams,
1579 pub plugin_params: PluginBaseParams,
1580}
1581
1582impl PluginRuntimeHandle {
1583 pub fn port_runtime(&self) -> &PortRuntimeHandle {
1584 &self.port_runtime
1585 }
1586
1587 pub fn array_sender(&self) -> &NDArraySender {
1588 &self.array_sender
1589 }
1590
1591 pub fn array_output(&self) -> &Arc<parking_lot::Mutex<NDArrayOutput>> {
1592 &self.array_output
1593 }
1594
1595 pub fn wait_params_applied(&self, timeout: std::time::Duration) -> bool {
1613 let (ack_tx, ack_rx) = std::sync::mpsc::sync_channel(1);
1614 if self.param_tx.send(PluginParamMsg::Barrier(ack_tx)).is_err() {
1615 return false;
1616 }
1617 ack_rx.recv_timeout(timeout).is_ok()
1618 }
1619
1620 pub fn port_name(&self) -> &str {
1621 &self.port_name
1622 }
1623}
1624
1625pub fn create_plugin_runtime<P: NDPluginProcess>(
1632 port_name: &str,
1633 processor: P,
1634 pool: Arc<NDArrayPool>,
1635 queue_size: usize,
1636 ndarray_port: &str,
1637 wiring: Arc<WiringRegistry>,
1638) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1639 create_plugin_runtime_multi_addr(
1640 port_name,
1641 processor,
1642 pool,
1643 queue_size,
1644 ndarray_port,
1645 wiring,
1646 1,
1647 )
1648}
1649
1650pub fn create_plugin_runtime_multi_addr<P: NDPluginProcess>(
1654 port_name: &str,
1655 mut processor: P,
1656 pool: Arc<NDArrayPool>,
1657 queue_size: usize,
1658 ndarray_port: &str,
1659 wiring: Arc<WiringRegistry>,
1660 max_addr: usize,
1661) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1662 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
1667 let handle_param_tx = param_tx.clone();
1668
1669 let plugin_type_name = processor.plugin_type().to_string();
1671 let compression_aware = processor.compression_aware();
1672 let does_array_callbacks = processor.does_array_callbacks();
1673 let array_data = processor.array_data_handle();
1674
1675 let driver = PluginPortDriver::new(
1677 port_name,
1678 &plugin_type_name,
1679 queue_size,
1680 ndarray_port,
1681 max_addr,
1682 param_tx,
1683 &mut processor,
1684 array_data,
1685 )
1686 .expect("failed to create plugin port driver");
1687
1688 let ndarray_params = driver.ndarray_params;
1689 let plugin_params = driver.plugin_params;
1690 let std_array_data_param = driver.std_array_data_param;
1691
1692 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default());
1694
1695 let port_handle = port_runtime.port_handle().clone();
1697
1698 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
1700
1701 let enabled = Arc::new(AtomicBool::new(false));
1703 let blocking_mode = Arc::new(AtomicBool::new(false));
1704
1705 let array_output = Arc::new(parking_lot::Mutex::new(NDArrayOutput::new()));
1707 let array_output_for_handle = array_output.clone();
1708 wiring.register_output_addrs(port_name, max_addr, array_output.clone());
1714 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
1717 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
1718 processor,
1719 output: array_output,
1720 pool,
1721 ndarray_params,
1722 plugin_params,
1723 port_handle,
1724 array_counter: 0,
1725 std_array_data_param,
1726 array_callbacks: does_array_callbacks,
1729 min_callback_time: 0.0,
1730 last_process_time: None,
1731 sort_mode: 0,
1732 sort_time: 0.0,
1733 sort_size: 10,
1734 sort_buffer: SortBuffer::new(),
1735 dropped_arrays: dropped_arrays_counter,
1736 compression_aware,
1737 max_byte_rate: 0.0,
1738 throttler: super::throttler::Throttler::new(0.0),
1739 prev_input_array: None,
1740 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
1741 nd_array_addr: 0,
1742 max_threads: 1,
1743 num_threads: 1,
1744 }));
1745
1746 let data_enabled = enabled.clone();
1747 let data_blocking = blocking_mode.clone();
1748
1749 let mut array_sender = array_sender;
1750 array_sender.set_mode_flags(enabled, blocking_mode);
1751
1752 let sender_port_name = port_name.to_string();
1754 let initial_upstream = ndarray_port.to_string();
1755
1756 let data_jh = thread::Builder::new()
1758 .name(format!("plugin-data-{port_name}"))
1759 .spawn(move || {
1760 plugin_data_loop(
1761 shared,
1762 array_rx,
1763 param_rx,
1764 plugin_params,
1765 ndarray_params.array_counter,
1766 data_enabled,
1767 data_blocking,
1768 sender_port_name,
1769 initial_upstream,
1770 wiring,
1771 );
1772 })
1773 .expect("failed to spawn plugin data thread");
1774
1775 let handle = PluginRuntimeHandle {
1776 port_runtime,
1777 array_sender,
1778 array_output: array_output_for_handle,
1779 port_name: port_name.to_string(),
1780 param_tx: handle_param_tx,
1781 ndarray_params,
1782 plugin_params,
1783 };
1784
1785 (handle, data_jh)
1786}
1787
1788fn queue_status_batch(
1794 plugin_params: &PluginBaseParams,
1795 max_capacity: usize,
1796 free: i32,
1797) -> ParamBatch {
1798 use asyn_rs::request::ParamSetValue;
1799 ParamBatch {
1800 addr0: vec![
1801 ParamSetValue::Int32 {
1802 reason: plugin_params.queue_size,
1803 addr: 0,
1804 value: max_capacity as i32,
1805 },
1806 ParamSetValue::Int32 {
1807 reason: plugin_params.queue_use,
1808 addr: 0,
1809 value: free,
1810 },
1811 ],
1812 extra: std::collections::HashMap::new(),
1813 }
1814}
1815
1816async fn clamp_writeback(port: &PortHandle, reason: usize, value: i32) {
1819 use asyn_rs::request::ParamSetValue;
1820 let _ = port
1821 .set_params_and_notify(
1822 0,
1823 vec![ParamSetValue::Int32 {
1824 reason,
1825 addr: 0,
1826 value,
1827 }],
1828 )
1829 .await;
1830}
1831
1832fn plugin_data_loop<P: NDPluginProcess>(
1833 shared: Arc<parking_lot::Mutex<SharedProcessorInner<P>>>,
1834 mut array_rx: NDArrayReceiver,
1835 mut param_rx: tokio::sync::mpsc::UnboundedReceiver<PluginParamMsg>,
1836 plugin_params: PluginBaseParams,
1837 array_counter_reason: usize,
1838 enabled: Arc<AtomicBool>,
1839 blocking_mode: Arc<AtomicBool>,
1840 sender_port_name: String,
1841 initial_upstream: String,
1842 wiring: Arc<WiringRegistry>,
1843) {
1844 let enable_callbacks_reason = plugin_params.enable_callbacks;
1845 let blocking_callbacks_reason = plugin_params.blocking_callbacks;
1846 let min_callback_time_reason = plugin_params.min_callback_time;
1847 let sort_mode_reason = plugin_params.sort_mode;
1848 let sort_time_reason = plugin_params.sort_time;
1849 let sort_size_reason = plugin_params.sort_size;
1850 let nd_array_port_reason = plugin_params.nd_array_port;
1851 let nd_array_addr_reason = plugin_params.nd_array_addr;
1852 let process_plugin_reason = plugin_params.process_plugin;
1853 let max_byte_rate_reason = plugin_params.max_byte_rate;
1854 let num_threads_reason = plugin_params.num_threads;
1855 let max_threads_reason = plugin_params.max_threads;
1856 let array_callbacks_reason = shared.lock().ndarray_params.array_callbacks;
1857 let mut current_upstream = initial_upstream;
1861 let mut current_addr: i32 = 0;
1862 let rt = tokio::runtime::Builder::new_current_thread()
1863 .enable_all()
1864 .build()
1865 .unwrap();
1866 rt.block_on(async {
1867 let mut sort_flush_interval = tokio::time::interval(std::time::Duration::from_secs(3600));
1870 let mut sort_flush_active = false;
1871 let mut last_queue_free: Option<i32> = None;
1874 let mut scatter_cursor: usize = 0;
1879 let mut held_barriers: Vec<std::sync::mpsc::SyncSender<()>> = Vec::new();
1885
1886 loop {
1887 if !held_barriers.is_empty() && array_rx.pending() == 0 {
1891 for ack in held_barriers.drain(..) {
1892 let _ = ack.try_send(());
1893 }
1894 }
1895 tokio::select! {
1896 msg = array_rx.recv_msg() => {
1897 match msg {
1898 Some(msg) => {
1899 if !enabled.load(Ordering::Acquire) {
1904 continue;
1905 }
1906 let (process_output, senders, port) = {
1908 let mut guard = shared.lock();
1909 let compressed = msg.array.codec.is_some();
1913 let output = if compressed && !guard.compression_aware {
1914 guard
1915 .dropped_arrays
1916 .fetch_add(1, Ordering::AcqRel);
1917 Some(guard.dropped_arrays_only_batch())
1918 } else {
1919 guard.process_and_publish(&msg.array)
1924 };
1925 let senders = guard.output.lock().senders_clone();
1926 let port = guard.port_handle.clone();
1927 (output, senders, port)
1928 };
1929 let max_cap = array_rx.max_capacity();
1934 let free = max_cap.saturating_sub(array_rx.pending()) as i32;
1935 let queue_batch = if last_queue_free != Some(free) {
1936 last_queue_free = Some(free);
1937 Some(queue_status_batch(&plugin_params, max_cap, free))
1938 } else {
1939 None
1940 };
1941 if let Some(po) = process_output {
1944 po.publish_arrays(&senders, &mut scatter_cursor).await;
1945 po.batch.flush(&port).await;
1946 }
1947 if let Some(qb) = queue_batch {
1948 qb.flush(&port).await;
1949 }
1950 }
1951 None => break,
1952 }
1953 }
1954 param = param_rx.recv() => {
1955 match param {
1956 Some(PluginParamMsg::Barrier(ack)) => {
1962 held_barriers.push(ack);
1963 }
1964 Some(PluginParamMsg::Change(reason, addr, value)) => {
1965 if reason == enable_callbacks_reason {
1966 let on = value.as_i32() != 0;
1967 enabled.store(on, Ordering::Release);
1968 if !on {
1971 shared.lock().prev_input_array = None;
1972 }
1973 }
1974 if reason == blocking_callbacks_reason {
1975 blocking_mode.store(value.as_i32() != 0, Ordering::Release);
1976 }
1977 if reason == array_callbacks_reason {
1982 shared.lock().array_callbacks = value.as_i32() != 0;
1983 }
1984 if reason == min_callback_time_reason {
1986 shared.lock().min_callback_time = value.as_f64();
1987 }
1988 if reason == max_byte_rate_reason {
1991 let rate = value.as_f64();
1992 let mut guard = shared.lock();
1993 guard.max_byte_rate = rate;
1994 guard.throttler.reset(rate);
1995 }
1996 if reason == max_threads_reason {
2003 let (port, clamped, mt) = {
2005 let mut guard = shared.lock();
2006 guard.max_threads = value.as_i32().max(1);
2007 let clamped =
2008 guard.num_threads.clamp(1, guard.max_threads);
2009 guard.num_threads = clamped;
2010 (guard.port_handle.clone(), clamped, guard.max_threads)
2011 };
2012 clamp_writeback(&port, num_threads_reason, clamped).await;
2013 clamp_writeback(&port, max_threads_reason, mt).await;
2014 }
2015 if reason == num_threads_reason {
2016 let (port, clamped) = {
2017 let mut guard = shared.lock();
2018 let clamped =
2019 value.as_i32().clamp(1, guard.max_threads.max(1));
2020 guard.num_threads = clamped;
2021 (guard.port_handle.clone(), clamped)
2022 };
2023 clamp_writeback(&port, num_threads_reason, clamped).await;
2024 }
2025 if reason == nd_array_addr_reason {
2029 let new_addr = value.as_i32();
2030 if new_addr != current_addr {
2031 let old_key = upstream_key(¤t_upstream, current_addr);
2032 let new_key = upstream_key(¤t_upstream, new_addr);
2033 shared.lock().nd_array_addr = new_addr;
2034 match wiring.rewire_by_name(
2035 &sender_port_name,
2036 &old_key,
2037 &new_key,
2038 ) {
2039 Ok(()) => current_addr = new_addr,
2040 Err(e) => {
2041 eprintln!("NDArrayAddr reconnect failed: {e}");
2042 shared.lock().nd_array_addr = current_addr;
2043 }
2044 }
2045 }
2046 }
2047 if reason == process_plugin_reason && value.as_i32() != 0 {
2050 let (process_output, senders, port) = {
2051 let mut guard = shared.lock();
2052 let output = guard.process_plugin();
2053 let senders = guard.output.lock().senders_clone();
2054 let port = guard.port_handle.clone();
2055 (output, senders, port)
2056 };
2057 if let Some(po) = process_output {
2058 po.publish_arrays(&senders, &mut scatter_cursor).await;
2059 po.batch.flush(&port).await;
2060 } else {
2061 #[cfg(feature = "ioc")]
2075 if let Some(entry) =
2076 asyn_rs::asyn_record::get_port(&sender_port_name)
2077 {
2078 asyn_rs::asyn_trace!(
2079 entry.trace,
2080 sender_port_name.as_str(),
2081 asyn_rs::trace::TraceMask::WARNING,
2082 "plugin {sender_port_name}: ProcessPlugin \
2083 requested but no input array cached"
2084 );
2085 }
2086 }
2087 }
2088 if reason == array_counter_reason {
2092 shared.lock().array_counter = value.as_i32();
2093 }
2094 if reason == sort_mode_reason {
2096 let mode = value.as_i32();
2097 let flush_work = {
2100 let mut guard = shared.lock();
2101 guard.sort_mode = mode;
2102 if mode == 0 {
2103 let output = guard.flush_sort_buffer();
2104 let senders = guard.output.lock().senders_clone();
2105 let port = guard.port_handle.clone();
2106 sort_flush_active = false;
2107 Some((output, senders, port))
2108 } else {
2109 sort_flush_active = guard.sort_time > 0.0;
2110 if sort_flush_active {
2111 let dur = std::time::Duration::from_secs_f64(guard.sort_time);
2112 sort_flush_interval = tokio::time::interval(dur);
2113 }
2114 None
2115 }
2116 };
2117 if let Some((output, senders, port)) = flush_work {
2118 output.publish_arrays(&senders, &mut scatter_cursor).await;
2119 output.batch.flush(&port).await;
2120 }
2121 }
2122 if reason == sort_time_reason {
2123 let t = value.as_f64();
2124 let mut guard = shared.lock();
2125 guard.sort_time = t;
2126 if guard.sort_mode != 0 && t > 0.0 {
2127 sort_flush_active = true;
2128 let dur = std::time::Duration::from_secs_f64(t);
2129 sort_flush_interval = tokio::time::interval(dur);
2130 } else {
2131 sort_flush_active = false;
2132 }
2133 drop(guard);
2134 }
2135 if reason == sort_size_reason {
2136 shared.lock().sort_size = value.as_i32();
2137 }
2138 if reason == nd_array_port_reason {
2140 if let Some(new_port) = value.as_string() {
2141 if new_port != current_upstream {
2142 let old_key =
2143 upstream_key(¤t_upstream, current_addr);
2144 let new_key = upstream_key(new_port, current_addr);
2145 match wiring.rewire_by_name(
2146 &sender_port_name,
2147 &old_key,
2148 &new_key,
2149 ) {
2150 Ok(()) => current_upstream = new_port.to_string(),
2151 Err(e) => {
2152 eprintln!("NDArrayPort rewire failed: {e}")
2153 }
2154 }
2155 }
2156 }
2157 }
2158 let snapshot = PluginParamSnapshot {
2159 enable_callbacks: enabled.load(Ordering::Acquire),
2160 reason,
2161 addr,
2162 value,
2163 };
2164 let (process_output, senders, port) = {
2165 let mut guard = shared.lock();
2166 let t0 = std::time::Instant::now();
2167 let result = guard.processor.on_param_change(reason, &snapshot);
2168 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
2169 let output = if !result.output_arrays.is_empty() || !result.param_updates.is_empty() {
2170 let deliver = guard.array_callbacks;
2171 Some(guard.build_publish_batch(result.output_arrays, result.param_updates, false, None, elapsed_ms, deliver, true))
2172 } else {
2173 None
2174 };
2175 let senders = guard.output.lock().senders_clone();
2176 (output, senders, guard.port_handle.clone())
2177 };
2178 if let Some(po) = process_output {
2179 po.publish_arrays(&senders, &mut scatter_cursor).await;
2180 po.batch.flush(&port).await;
2181 }
2182 }
2183 None => break,
2184 }
2185 }
2186 _ = sort_flush_interval.tick(), if sort_flush_active => {
2187 let (output, senders, port) = {
2190 let mut guard = shared.lock();
2191 let output = guard.tick_sort_buffer();
2192 let senders = guard.output.lock().senders_clone();
2193 let port = guard.port_handle.clone();
2194 (output, senders, port)
2195 };
2196 output.publish_arrays(&senders, &mut scatter_cursor).await;
2197 output.batch.flush(&port).await;
2198 }
2199 }
2200 }
2201 });
2202}
2203
2204pub fn wire_downstream(upstream: &PluginRuntimeHandle, downstream_sender: NDArraySender) {
2211 upstream.array_output().lock().add(downstream_sender);
2212}
2213
2214pub fn create_plugin_runtime_with_output<P: NDPluginProcess>(
2216 port_name: &str,
2217 mut processor: P,
2218 pool: Arc<NDArrayPool>,
2219 queue_size: usize,
2220 output: NDArrayOutput,
2221 ndarray_port: &str,
2222 wiring: Arc<WiringRegistry>,
2223) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
2224 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
2228 let handle_param_tx = param_tx.clone();
2229
2230 let plugin_type_name = processor.plugin_type().to_string();
2231 let compression_aware = processor.compression_aware();
2232 let does_array_callbacks = processor.does_array_callbacks();
2233 let array_data = processor.array_data_handle();
2234 let driver = PluginPortDriver::new(
2235 port_name,
2236 &plugin_type_name,
2237 queue_size,
2238 ndarray_port,
2239 1,
2240 param_tx,
2241 &mut processor,
2242 array_data,
2243 )
2244 .expect("failed to create plugin port driver");
2245
2246 let ndarray_params = driver.ndarray_params;
2247 let plugin_params = driver.plugin_params;
2248 let std_array_data_param = driver.std_array_data_param;
2249
2250 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default());
2251
2252 let port_handle = port_runtime.port_handle().clone();
2253
2254 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
2255
2256 let enabled = Arc::new(AtomicBool::new(false));
2257 let blocking_mode = Arc::new(AtomicBool::new(false));
2258
2259 let array_output = Arc::new(parking_lot::Mutex::new(output));
2260 let array_output_for_handle = array_output.clone();
2261 wiring.register_output(port_name, array_output.clone());
2265 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
2267 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
2268 processor,
2269 output: array_output,
2270 pool,
2271 ndarray_params,
2272 plugin_params,
2273 port_handle,
2274 array_counter: 0,
2275 std_array_data_param,
2276 array_callbacks: does_array_callbacks,
2279 min_callback_time: 0.0,
2280 last_process_time: None,
2281 sort_mode: 0,
2282 sort_time: 0.0,
2283 sort_size: 10,
2284 sort_buffer: SortBuffer::new(),
2285 dropped_arrays: dropped_arrays_counter,
2286 compression_aware,
2287 max_byte_rate: 0.0,
2288 throttler: super::throttler::Throttler::new(0.0),
2289 prev_input_array: None,
2290 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
2291 nd_array_addr: 0,
2292 max_threads: 1,
2293 num_threads: 1,
2294 }));
2295
2296 let data_enabled = enabled.clone();
2297 let data_blocking = blocking_mode.clone();
2298
2299 let mut array_sender = array_sender;
2300 array_sender.set_mode_flags(enabled, blocking_mode);
2301
2302 let sender_port_name = port_name.to_string();
2304 let initial_upstream = ndarray_port.to_string();
2305
2306 let data_jh = thread::Builder::new()
2307 .name(format!("plugin-data-{port_name}"))
2308 .spawn(move || {
2309 plugin_data_loop(
2310 shared,
2311 array_rx,
2312 param_rx,
2313 plugin_params,
2314 ndarray_params.array_counter,
2315 data_enabled,
2316 data_blocking,
2317 sender_port_name,
2318 initial_upstream,
2319 wiring,
2320 );
2321 })
2322 .expect("failed to spawn plugin data thread");
2323
2324 let handle = PluginRuntimeHandle {
2325 port_runtime,
2326 array_sender,
2327 array_output: array_output_for_handle,
2328 port_name: port_name.to_string(),
2329 param_tx: handle_param_tx,
2330 ndarray_params,
2331 plugin_params,
2332 };
2333
2334 (handle, data_jh)
2335}
2336
2337#[cfg(test)]
2338mod tests {
2339 use super::*;
2340 use crate::ndarray::{NDDataType, NDDimension};
2341 use crate::plugin::channel::ndarray_channel;
2342
2343 struct PassthroughProcessor;
2345
2346 impl NDPluginProcess for PassthroughProcessor {
2347 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2348 ProcessResult::arrays(vec![Arc::new(array.clone())])
2349 }
2350 fn plugin_type(&self) -> &str {
2351 "Passthrough"
2352 }
2353 }
2354
2355 struct SinkProcessor {
2357 count: usize,
2358 }
2359
2360 impl NDPluginProcess for SinkProcessor {
2361 fn process_array(&mut self, _array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2362 self.count += 1;
2363 ProcessResult::empty()
2364 }
2365 fn plugin_type(&self) -> &str {
2366 "Sink"
2367 }
2368 }
2369
2370 fn make_test_array(id: i32) -> Arc<NDArray> {
2371 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2372 arr.unique_id = id;
2373 Arc::new(arr)
2374 }
2375
2376 fn test_wiring() -> Arc<WiringRegistry> {
2377 Arc::new(WiringRegistry::new())
2378 }
2379
2380 fn params_applied(handle: &PluginRuntimeHandle) {
2385 assert!(
2386 handle.wait_params_applied(std::time::Duration::from_secs(10)),
2387 "data thread did not apply queued param changes"
2388 );
2389 }
2390
2391 fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
2395 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2396 while !cond() {
2397 assert!(
2398 std::time::Instant::now() < deadline,
2399 "timed out waiting for {what}"
2400 );
2401 std::thread::sleep(std::time::Duration::from_millis(2));
2402 }
2403 }
2404
2405 fn enable_callbacks(handle: &PluginRuntimeHandle) {
2408 handle
2409 .port_runtime()
2410 .port_handle()
2411 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2412 .unwrap();
2413 params_applied(handle);
2414 }
2415
2416 fn send_array(sender: &NDArraySender, array: Arc<NDArray>) {
2420 let sender = sender.clone();
2421 let jh = std::thread::spawn(move || {
2422 let rt = tokio::runtime::Builder::new_current_thread()
2423 .enable_all()
2424 .build()
2425 .unwrap();
2426 rt.block_on(sender.publish(array));
2427 });
2428 jh.join().unwrap();
2429 }
2430
2431 #[test]
2432 fn test_passthrough_runtime() {
2433 let pool = Arc::new(NDArrayPool::new(1_000_000));
2434
2435 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2437 let mut output = NDArrayOutput::new();
2438 output.add(downstream_sender);
2439
2440 let (handle, _data_jh) = create_plugin_runtime_with_output(
2441 "PASS1",
2442 PassthroughProcessor,
2443 pool,
2444 10,
2445 output,
2446 "",
2447 test_wiring(),
2448 );
2449 enable_callbacks(&handle);
2450
2451 send_array(handle.array_sender(), make_test_array(42));
2453
2454 let received = downstream_rx.blocking_recv().unwrap();
2456 assert_eq!(received.unique_id, 42);
2457 }
2458
2459 #[test]
2460 fn test_sink_runtime() {
2461 let pool = Arc::new(NDArrayPool::new(1_000_000));
2462
2463 let (handle, _data_jh) = create_plugin_runtime(
2464 "SINK1",
2465 SinkProcessor { count: 0 },
2466 pool,
2467 10,
2468 "",
2469 test_wiring(),
2470 );
2471 enable_callbacks(&handle);
2472
2473 send_array(handle.array_sender(), make_test_array(1));
2475 send_array(handle.array_sender(), make_test_array(2));
2476
2477 let port = handle.port_runtime().port_handle().clone();
2479 let counter = handle.ndarray_params.array_counter;
2480 wait_until("sink to process both arrays", || {
2481 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 2)
2482 });
2483 assert_eq!(handle.port_name(), "SINK1");
2484 }
2485
2486 #[test]
2487 fn test_plugin_type_param() {
2488 let pool = Arc::new(NDArrayPool::new(1_000_000));
2489
2490 let (handle, _data_jh) = create_plugin_runtime(
2491 "TYPE_TEST",
2492 PassthroughProcessor,
2493 pool,
2494 10,
2495 "",
2496 test_wiring(),
2497 );
2498
2499 assert_eq!(handle.port_name(), "TYPE_TEST");
2501 assert_eq!(handle.port_runtime().port_name(), "TYPE_TEST");
2502 }
2503
2504 #[test]
2505 fn test_shutdown_on_handle_drop() {
2506 let pool = Arc::new(NDArrayPool::new(1_000_000));
2507
2508 let (handle, data_jh) = create_plugin_runtime(
2509 "SHUTDOWN_TEST",
2510 PassthroughProcessor,
2511 pool,
2512 10,
2513 "",
2514 test_wiring(),
2515 );
2516
2517 let sender = handle.array_sender().clone();
2519 drop(handle);
2520 drop(sender);
2521
2522 let result = data_jh.join();
2524 assert!(result.is_ok());
2525 }
2526
2527 #[test]
2528 fn test_wire_to_nonzero_ndarray_addr() {
2529 use crate::plugin::wiring::upstream_key;
2535 let pool = Arc::new(NDArrayPool::new(1_000_000));
2536 let wiring = test_wiring();
2537
2538 let (up_handle, _up_jh) = create_plugin_runtime_multi_addr(
2540 "UP_MULTI",
2541 PassthroughProcessor,
2542 pool,
2543 10,
2544 "",
2545 wiring.clone(),
2546 2,
2547 );
2548 enable_callbacks(&up_handle);
2549
2550 let addr0 = wiring.lookup_output("UP_MULTI");
2552 let addr1 = wiring.lookup_output(&upstream_key("UP_MULTI", 1));
2553 assert!(addr0.is_some(), "addr 0 output must be registered");
2554 assert!(
2555 addr1.is_some(),
2556 "addr 1 output must be registered for a max_addr=2 port"
2557 );
2558
2559 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWN_ADDR1", 10);
2561 wiring
2562 .rewire(&downstream_sender, "", &upstream_key("UP_MULTI", 1))
2563 .expect("wiring a consumer to NDArrayAddr=1 must succeed");
2564
2565 send_array(up_handle.array_sender(), make_test_array(99));
2567 let received = downstream_rx.blocking_recv().unwrap();
2568 assert_eq!(
2569 received.unique_id, 99,
2570 "consumer wired to NDArrayAddr=1 must receive upstream arrays"
2571 );
2572 }
2573
2574 #[test]
2575 fn test_nonblocking_passthrough() {
2576 let pool = Arc::new(NDArrayPool::new(1_000_000));
2577 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2578 let mut output = NDArrayOutput::new();
2579 output.add(downstream_sender);
2580
2581 let (handle, _data_jh) = create_plugin_runtime_with_output(
2582 "NB_TEST",
2583 PassthroughProcessor,
2584 pool,
2585 10,
2586 output,
2587 "",
2588 test_wiring(),
2589 );
2590 enable_callbacks(&handle);
2591
2592 send_array(handle.array_sender(), make_test_array(42));
2593
2594 let received = downstream_rx.blocking_recv().unwrap();
2595 assert_eq!(received.unique_id, 42);
2596 }
2597
2598 #[test]
2599 fn test_blocking_to_nonblocking_switch() {
2600 let pool = Arc::new(NDArrayPool::new(1_000_000));
2601 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2602 let mut output = NDArrayOutput::new();
2603 output.add(downstream_sender);
2604
2605 let (handle, _data_jh) = create_plugin_runtime_with_output(
2606 "SWITCH_TEST",
2607 PassthroughProcessor,
2608 pool,
2609 10,
2610 output,
2611 "",
2612 test_wiring(),
2613 );
2614 enable_callbacks(&handle);
2615
2616 handle
2618 .port_runtime()
2619 .port_handle()
2620 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 1)
2621 .unwrap();
2622 params_applied(&handle);
2623
2624 send_array(handle.array_sender(), make_test_array(1));
2625 let received = downstream_rx.blocking_recv().unwrap();
2626 assert_eq!(received.unique_id, 1);
2627
2628 handle
2630 .port_runtime()
2631 .port_handle()
2632 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 0)
2633 .unwrap();
2634 params_applied(&handle);
2635
2636 send_array(handle.array_sender(), make_test_array(2));
2638 let received = downstream_rx.blocking_recv().unwrap();
2639 assert_eq!(received.unique_id, 2);
2640 }
2641
2642 #[test]
2643 fn test_enable_callbacks_disables_processing() {
2644 let pool = Arc::new(NDArrayPool::new(1_000_000));
2645 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2646 let mut output = NDArrayOutput::new();
2647 output.add(downstream_sender);
2648
2649 let (handle, _data_jh) = create_plugin_runtime_with_output(
2650 "ENABLE_TEST",
2651 PassthroughProcessor,
2652 pool,
2653 10,
2654 output,
2655 "",
2656 test_wiring(),
2657 );
2658
2659 handle
2661 .port_runtime()
2662 .port_handle()
2663 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 0)
2664 .unwrap();
2665 params_applied(&handle);
2666
2667 send_array(handle.array_sender(), make_test_array(99));
2669
2670 let rt = tokio::runtime::Builder::new_current_thread()
2672 .enable_all()
2673 .build()
2674 .unwrap();
2675 let result = rt.block_on(async {
2676 tokio::time::timeout(std::time::Duration::from_millis(100), downstream_rx.recv()).await
2677 });
2678 assert!(
2679 result.is_err(),
2680 "should not receive array when callbacks disabled"
2681 );
2682 }
2683
2684 #[test]
2685 fn test_downstream_receives_multiple() {
2686 let pool = Arc::new(NDArrayPool::new(1_000_000));
2687
2688 let (ds1, mut rx1) = ndarray_channel("DS1", 10);
2689 let (ds2, mut rx2) = ndarray_channel("DS2", 10);
2690 let mut output = NDArrayOutput::new();
2691 output.add(ds1);
2692 output.add(ds2);
2693
2694 let (handle, _data_jh) = create_plugin_runtime_with_output(
2695 "DS_TEST",
2696 PassthroughProcessor,
2697 pool,
2698 10,
2699 output,
2700 "",
2701 test_wiring(),
2702 );
2703 enable_callbacks(&handle);
2704
2705 send_array(handle.array_sender(), make_test_array(77));
2706
2707 let r1 = rx1.blocking_recv().unwrap();
2709 let r2 = rx2.blocking_recv().unwrap();
2710 assert_eq!(r1.unique_id, 77);
2711 assert_eq!(r2.unique_id, 77);
2712 }
2713
2714 #[test]
2715 fn test_param_updates_after_send() {
2716 let pool = Arc::new(NDArrayPool::new(1_000_000));
2717
2718 struct ParamTracker;
2719 impl NDPluginProcess for ParamTracker {
2720 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2721 ProcessResult::arrays(vec![Arc::new(array.clone())])
2722 }
2723 fn plugin_type(&self) -> &str {
2724 "ParamTracker"
2725 }
2726 }
2727
2728 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2729 let mut output = NDArrayOutput::new();
2730 output.add(downstream_sender);
2731
2732 let (handle, _data_jh) = create_plugin_runtime_with_output(
2733 "PARAM_TEST",
2734 ParamTracker,
2735 pool,
2736 10,
2737 output,
2738 "",
2739 test_wiring(),
2740 );
2741 enable_callbacks(&handle);
2742
2743 send_array(handle.array_sender(), make_test_array(1));
2745 let received = downstream_rx.blocking_recv().unwrap();
2746 assert_eq!(received.unique_id, 1);
2747
2748 handle
2750 .port_runtime()
2751 .port_handle()
2752 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2753 .unwrap();
2754 params_applied(&handle);
2755
2756 send_array(handle.array_sender(), make_test_array(2));
2758 let received = downstream_rx.blocking_recv().unwrap();
2759 assert_eq!(received.unique_id, 2);
2760 }
2761
2762 #[test]
2763 fn test_sort_buffer_reorders_by_unique_id() {
2764 let mut buf = SortBuffer::new();
2765
2766 buf.insert(3, vec![make_test_array(3)], 10);
2768 buf.insert(1, vec![make_test_array(1)], 10);
2769 buf.insert(2, vec![make_test_array(2)], 10);
2770
2771 assert_eq!(buf.len(), 3);
2772
2773 let drained = buf.drain_all();
2774 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2775 assert_eq!(ids, vec![1, 2, 3], "should drain in sorted uniqueId order");
2776 assert_eq!(buf.len(), 0);
2777 assert_eq!(buf.prev_unique_id, 3);
2778 }
2779
2780 #[test]
2781 fn test_sort_buffer_drain_ready_contiguous() {
2782 let mut buf = SortBuffer::new();
2785 buf.note_emitted(0);
2788 buf.insert(1, vec![make_test_array(1)], 10);
2789 buf.insert(2, vec![make_test_array(2)], 10);
2790 buf.insert(5, vec![make_test_array(5)], 10); let drained = buf.drain_ready(100.0);
2794 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2795 assert_eq!(ids, vec![1, 2], "contiguous run released; id=5 held by gap");
2796 assert_eq!(buf.len(), 1);
2797 }
2798
2799 #[test]
2800 fn test_sort_buffer_drain_ready_deadline() {
2801 let mut buf = SortBuffer::new();
2803 buf.note_emitted(1); buf.insert(5, vec![make_test_array(5)], 10); std::thread::sleep(std::time::Duration::from_millis(30));
2806 let drained = buf.drain_ready(0.01);
2808 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2809 assert_eq!(ids, vec![5], "stale head released via deadline");
2810 }
2811
2812 #[test]
2813 fn test_sort_buffer_detects_disordered_on_emit() {
2814 let mut buf = SortBuffer::new();
2816 buf.note_emitted(5); buf.note_emitted(3); assert_eq!(buf.disordered_arrays, 1);
2819 buf.note_emitted(4); assert_eq!(buf.disordered_arrays, 1);
2821 }
2822
2823 #[test]
2824 fn test_sort_buffer_drops_when_full() {
2825 let mut buf = SortBuffer::new();
2826
2827 assert!(buf.insert(1, vec![make_test_array(1)], 2));
2829 assert!(buf.insert(2, vec![make_test_array(2)], 2));
2830 assert!(!buf.insert(3, vec![make_test_array(3)], 2));
2831
2832 assert_eq!(buf.len(), 2);
2833 assert_eq!(buf.dropped_output_arrays, 1);
2834 }
2835
2836 #[test]
2837 fn test_sort_mode_runtime_integration() {
2838 let pool = Arc::new(NDArrayPool::new(1_000_000));
2839 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2840 let mut output = NDArrayOutput::new();
2841 output.add(downstream_sender);
2842
2843 let (handle, _data_jh) = create_plugin_runtime_with_output(
2844 "SORT_TEST",
2845 PassthroughProcessor,
2846 pool,
2847 10,
2848 output,
2849 "",
2850 test_wiring(),
2851 );
2852 enable_callbacks(&handle);
2853
2854 handle
2856 .port_runtime()
2857 .port_handle()
2858 .write_int32_blocking(handle.plugin_params.sort_size, 0, 10)
2859 .unwrap();
2860 handle
2861 .port_runtime()
2862 .port_handle()
2863 .write_float64_blocking(handle.plugin_params.sort_time, 0, 0.1)
2864 .unwrap();
2865 handle
2866 .port_runtime()
2867 .port_handle()
2868 .write_int32_blocking(handle.plugin_params.sort_mode, 0, 1)
2869 .unwrap();
2870 params_applied(&handle);
2871
2872 send_array(handle.array_sender(), make_test_array(1));
2875 send_array(handle.array_sender(), make_test_array(2));
2876 send_array(handle.array_sender(), make_test_array(3));
2877
2878 let rt = tokio::runtime::Builder::new_current_thread()
2879 .enable_all()
2880 .build()
2881 .unwrap();
2882 let fast = rt.block_on(async {
2883 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
2884 });
2885 assert!(
2886 fast.is_ok(),
2887 "in-order arrays must be emitted immediately, not buffered"
2888 );
2889 assert_eq!(fast.unwrap().unwrap().unique_id, 1);
2890 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
2891 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 3);
2892
2893 send_array(handle.array_sender(), make_test_array(5));
2897 send_array(handle.array_sender(), make_test_array(4));
2898 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 4);
2901 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 5);
2902 }
2903
2904 #[test]
2905 fn test_throttle_drops_output_arrays() {
2906 let pool = Arc::new(NDArrayPool::new(1_000_000));
2909 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2910 let mut output = NDArrayOutput::new();
2911 output.add(downstream_sender);
2912
2913 let (handle, _data_jh) = create_plugin_runtime_with_output(
2914 "THROTTLE_TEST",
2915 PassthroughProcessor,
2916 pool,
2917 10,
2918 output,
2919 "",
2920 test_wiring(),
2921 );
2922 enable_callbacks(&handle);
2923
2924 handle
2927 .port_runtime()
2928 .port_handle()
2929 .write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 8.0)
2930 .unwrap();
2931 params_applied(&handle);
2932
2933 for id in 1..=5 {
2934 send_array(handle.array_sender(), make_test_array(id));
2935 }
2936 let port = handle.port_runtime().port_handle().clone();
2941 let counter = handle.ndarray_params.array_counter;
2942 wait_until("all 5 frames to be processed", || {
2943 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 5)
2944 });
2945
2946 let rt = tokio::runtime::Builder::new_current_thread()
2948 .enable_all()
2949 .build()
2950 .unwrap();
2951 let mut received = 0;
2952 while rt
2953 .block_on(async {
2954 tokio::time::timeout(std::time::Duration::from_millis(20), downstream_rx.recv())
2955 .await
2956 })
2957 .map(|o| o.is_some())
2958 .unwrap_or(false)
2959 {
2960 received += 1;
2961 }
2962 assert!(
2963 received < 5,
2964 "throttle must drop some arrays (got {received})"
2965 );
2966 assert!(received >= 1, "first array within budget must pass");
2967 }
2968
2969 #[test]
2970 fn test_process_plugin_reprocesses_last_input() {
2971 let pool = Arc::new(NDArrayPool::new(1_000_000));
2973 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2974 let mut output = NDArrayOutput::new();
2975 output.add(downstream_sender);
2976
2977 let (handle, _data_jh) = create_plugin_runtime_with_output(
2978 "PROCESS_PLUGIN_TEST",
2979 PassthroughProcessor,
2980 pool,
2981 10,
2982 output,
2983 "",
2984 test_wiring(),
2985 );
2986 enable_callbacks(&handle);
2987
2988 send_array(handle.array_sender(), make_test_array(7));
2989 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 7);
2990
2991 handle
2993 .port_runtime()
2994 .port_handle()
2995 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
2996 .unwrap();
2997 let reprocessed = downstream_rx.blocking_recv().unwrap();
2998 assert_eq!(
2999 reprocessed.unique_id, 7,
3000 "ProcessPlugin re-emits last input"
3001 );
3002 }
3003
3004 #[test]
3005 fn test_min_callback_time_throttle_not_counted() {
3006 let pool = Arc::new(NDArrayPool::new(1_000_000));
3014 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3015 let mut output = NDArrayOutput::new();
3016 output.add(downstream_sender);
3017
3018 let (handle, _data_jh) = create_plugin_runtime_with_output(
3019 "MIN_CB_TEST",
3020 PassthroughProcessor,
3021 pool,
3022 10,
3023 output,
3024 "",
3025 test_wiring(),
3026 );
3027 enable_callbacks(&handle);
3028 let dropped = handle.array_sender().dropped_arrays_counter().clone();
3029
3030 handle
3032 .port_runtime()
3033 .port_handle()
3034 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3035 .unwrap();
3036 params_applied(&handle);
3037
3038 send_array(handle.array_sender(), make_test_array(1));
3039 send_array(handle.array_sender(), make_test_array(2));
3040
3041 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3042 params_applied(&handle);
3046 let rt = tokio::runtime::Builder::new_current_thread()
3047 .enable_all()
3048 .build()
3049 .unwrap();
3050 let second = rt.block_on(async {
3051 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3052 });
3053 assert!(
3054 second.is_err(),
3055 "second array throttled out by MinCallbackTime"
3056 );
3057 assert_eq!(
3058 dropped.load(Ordering::Acquire),
3059 0,
3060 "a MinCallbackTime-throttled frame must NOT increment DroppedArrays"
3061 );
3062 }
3063
3064 #[test]
3065 fn test_array_callbacks_zero_withholds_downstream_delivery() {
3066 let pool = Arc::new(NDArrayPool::new(1_000_000));
3072 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3073 let mut output = NDArrayOutput::new();
3074 output.add(downstream_sender);
3075
3076 let (handle, _data_jh) = create_plugin_runtime_with_output(
3077 "ARRAY_CB_TEST",
3078 PassthroughProcessor,
3079 pool,
3080 10,
3081 output,
3082 "",
3083 test_wiring(),
3084 );
3085 enable_callbacks(&handle);
3086 let port = handle.port_runtime().port_handle().clone();
3087
3088 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
3090 .unwrap();
3091 params_applied(&handle);
3092
3093 send_array(handle.array_sender(), make_test_array(1));
3094 wait_until("frame 1 to be processed", || {
3097 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3098 .is_ok_and(|v| v == 1)
3099 });
3100
3101 let rt = tokio::runtime::Builder::new_current_thread()
3103 .enable_all()
3104 .build()
3105 .unwrap();
3106 let got = rt.block_on(async {
3107 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3108 });
3109 assert!(
3110 got.is_err(),
3111 "NDArrayCallbacks=0 must withhold downstream delivery"
3112 );
3113 assert_eq!(
3115 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3116 .unwrap(),
3117 1,
3118 "processing (and metadata params) must continue while delivery is off"
3119 );
3120
3121 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 1)
3123 .unwrap();
3124 params_applied(&handle);
3125 send_array(handle.array_sender(), make_test_array(2));
3126 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3127 }
3128
3129 #[test]
3130 fn test_plugin_output_publishes_compressed_size() {
3131 struct CompressProcessor;
3137 impl NDPluginProcess for CompressProcessor {
3138 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3139 let mut out = array.clone();
3140 out.codec = Some(crate::codec::Codec {
3141 name: crate::codec::CodecName::JPEG,
3142 compressed_size: 7,
3143 level: 0,
3144 shuffle: 0,
3145 compressor: 0,
3146 original_data_type: NDDataType::UInt8,
3147 });
3148 ProcessResult::arrays(vec![Arc::new(out)])
3149 }
3150 fn plugin_type(&self) -> &str {
3151 "Compress"
3152 }
3153 }
3154
3155 {
3157 let pool = Arc::new(NDArrayPool::new(1_000_000));
3158 let (ds, _rx) = ndarray_channel("DS_RAW", 10);
3159 let mut output = NDArrayOutput::new();
3160 output.add(ds);
3161 let (handle, _jh) = create_plugin_runtime_with_output(
3162 "CODEC_RAW",
3163 PassthroughProcessor,
3164 pool,
3165 10,
3166 output,
3167 "",
3168 test_wiring(),
3169 );
3170 enable_callbacks(&handle);
3171 let port = handle.port_runtime().port_handle().clone();
3172 send_array(handle.array_sender(), make_test_array(1));
3173 wait_until(
3176 "uncompressed output to publish CompressedSize == raw byte count",
3177 || {
3178 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3179 .is_ok_and(|v| v == 4)
3180 },
3181 );
3182 }
3183
3184 {
3186 let pool = Arc::new(NDArrayPool::new(1_000_000));
3187 let (ds, _rx) = ndarray_channel("DS_CMP", 10);
3188 let mut output = NDArrayOutput::new();
3189 output.add(ds);
3190 let (handle, _jh) = create_plugin_runtime_with_output(
3191 "CODEC_CMP",
3192 CompressProcessor,
3193 pool,
3194 10,
3195 output,
3196 "",
3197 test_wiring(),
3198 );
3199 enable_callbacks(&handle);
3200 let port = handle.port_runtime().port_handle().clone();
3201 send_array(handle.array_sender(), make_test_array(1));
3202 wait_until(
3203 "compressed output to publish CompressedSize == codec.compressed_size",
3204 || {
3205 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3206 .is_ok_and(|v| v == 7)
3207 },
3208 );
3209 }
3210 }
3211
3212 #[test]
3213 fn test_process_plugin_skips_throttled_input() {
3214 let pool = Arc::new(NDArrayPool::new(1_000_000));
3219 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3220 let mut output = NDArrayOutput::new();
3221 output.add(downstream_sender);
3222
3223 let (handle, _data_jh) = create_plugin_runtime_with_output(
3224 "PROCESS_THROTTLE_TEST",
3225 PassthroughProcessor,
3226 pool,
3227 10,
3228 output,
3229 "",
3230 test_wiring(),
3231 );
3232 enable_callbacks(&handle);
3233
3234 handle
3236 .port_runtime()
3237 .port_handle()
3238 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3239 .unwrap();
3240 params_applied(&handle);
3241
3242 send_array(handle.array_sender(), make_test_array(1));
3243 send_array(handle.array_sender(), make_test_array(2));
3244
3245 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3247 params_applied(&handle);
3251
3252 handle
3257 .port_runtime()
3258 .port_handle()
3259 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 0.0)
3260 .unwrap();
3261 handle
3265 .port_runtime()
3266 .port_handle()
3267 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3268 .unwrap();
3269 let reprocessed = downstream_rx.blocking_recv().unwrap();
3270 assert_eq!(
3271 reprocessed.unique_id, 1,
3272 "ProcessPlugin must re-inject the last processed array (1), not the throttled array (2)"
3273 );
3274 }
3275
3276 #[test]
3277 fn test_g3_compressed_array_dropped_on_non_aware_plugin() {
3278 let pool = Arc::new(NDArrayPool::new(1_000_000));
3280 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3281 let mut output = NDArrayOutput::new();
3282 output.add(downstream_sender);
3283
3284 let (handle, _data_jh) = create_plugin_runtime_with_output(
3285 "G3_TEST",
3286 PassthroughProcessor, pool,
3288 10,
3289 output,
3290 "",
3291 test_wiring(),
3292 );
3293 enable_callbacks(&handle);
3294
3295 let mut compressed = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
3297 compressed.unique_id = 1;
3298 compressed.codec = Some(crate::codec::Codec {
3299 name: crate::codec::CodecName::JPEG,
3300 compressed_size: 16,
3301 level: 0,
3302 shuffle: 0,
3303 compressor: 0,
3304 original_data_type: NDDataType::UInt8,
3305 });
3306 send_array(handle.array_sender(), Arc::new(compressed));
3307
3308 send_array(handle.array_sender(), make_test_array(2));
3310
3311 let r = downstream_rx.blocking_recv().unwrap();
3312 assert_eq!(
3313 r.unique_id, 2,
3314 "compressed array dropped; only the raw array reaches downstream"
3315 );
3316 }
3317
3318 #[test]
3319 fn test_drop_on_full_increments_dropped_counter() {
3320 struct SlowProcessor;
3324 impl NDPluginProcess for SlowProcessor {
3325 fn process_array(&mut self, _a: &NDArray, _p: &NDArrayPool) -> ProcessResult {
3326 std::thread::sleep(std::time::Duration::from_millis(200));
3327 ProcessResult::empty()
3328 }
3329 fn plugin_type(&self) -> &str {
3330 "Slow"
3331 }
3332 }
3333 let pool = Arc::new(NDArrayPool::new(1_000_000));
3334
3335 let (downstream_handle, _ds_jh) =
3337 create_plugin_runtime("B1_DOWNSTREAM", SlowProcessor, pool, 1, "", test_wiring());
3338 enable_callbacks(&downstream_handle);
3339 let ds_sender = downstream_handle.array_sender().clone();
3340 let dropped = ds_sender.dropped_arrays_counter().clone();
3341
3342 send_array(&ds_sender, make_test_array(1));
3345 send_array(&ds_sender, make_test_array(2));
3346 send_array(&ds_sender, make_test_array(3));
3347 send_array(&ds_sender, make_test_array(4));
3348
3349 assert!(
3350 dropped.load(Ordering::Acquire) >= 1,
3351 "arrays dropped on a full queue must be counted (got {})",
3352 dropped.load(Ordering::Acquire)
3353 );
3354 }
3355
3356 #[test]
3357 fn test_cross_width_narrowing_array_read_truncates() {
3358 let mut out = [0i8; 1];
3367 let n = copy_ccast(&[300u16], &mut out);
3368 assert_eq!(n, 1);
3369 assert_eq!(out[0], 44, "(epicsInt8)(epicsUInt16)300 == 44 (low 8 bits)");
3370 let mut sat = [0i8; 1];
3372 copy_convert(&[300u16], &mut sat);
3373 assert_eq!(sat[0], 127, "f64 round-trip saturates — the wrong behavior");
3374
3375 let mut out2 = [0i8; 1];
3377 copy_ccast(&[0x1234_5678i32], &mut out2);
3378 assert_eq!(out2[0], 0x78);
3379
3380 let mut out3 = [0i8; 1];
3382 copy_ccast(&[-1i32], &mut out3);
3383 assert_eq!(out3[0], -1);
3384
3385 let mut out4 = [0i8; 1];
3387 copy_ccast(&[255u16], &mut out4);
3388 assert_eq!(out4[0], -1);
3389
3390 let mut out5 = [0i32; 1];
3392 copy_ccast(&[0x0000_0001_0000_002Ai64], &mut out5);
3393 assert_eq!(out5[0], 42);
3394
3395 let mut out6 = [0i16; 1];
3397 copy_ccast(&[70000u32], &mut out6);
3398 assert_eq!(out6[0], 4464);
3399
3400 let mut out7 = [0i8; 1];
3403 copy_ccast(&[255u8], &mut out7);
3404 assert_eq!(out7[0], -1);
3405
3406 let mut fout = [0i32; 1];
3412 copy_convert(&[42.9f64], &mut fout);
3413 assert_eq!(fout[0], 42, "f64 -> i32 truncates toward zero");
3414 }
3415
3416 fn block<F: std::future::Future>(f: F) -> F::Output {
3420 tokio::runtime::Builder::new_current_thread()
3421 .enable_all()
3422 .build()
3423 .unwrap()
3424 .block_on(f)
3425 }
3426
3427 #[test]
3428 fn test_scatter_reroutes_past_full_consumer() {
3429 let (sa, mut ra) = ndarray_channel("A", 1);
3434 let (sb, mut rb) = ndarray_channel("B", 1);
3435 let (sc, _rc) = ndarray_channel("C", 1);
3436 block(async {
3437 assert_eq!(
3438 sa.publish(make_test_array(99)).await,
3439 PublishOutcome::Delivered
3440 );
3441 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3442 let mut cursor = 0usize;
3443 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3444 assert_eq!(cursor, 2);
3446 assert_eq!(rb.recv().await.unwrap().unique_id, 1);
3447 assert_eq!(ra.recv().await.unwrap().unique_id, 99);
3449 assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3450 });
3451 }
3452
3453 #[test]
3454 fn test_scatter_drops_on_last_when_all_full_counts_once() {
3455 let (sa, mut ra) = ndarray_channel("A", 1);
3459 let (sb, mut rb) = ndarray_channel("B", 1);
3460 block(async {
3461 sa.publish(make_test_array(91)).await;
3462 sb.publish(make_test_array(92)).await;
3463 let senders = vec![sa.clone(), sb.clone()];
3464 let mut cursor = 0usize;
3465 ProcessOutput::scatter_publish(&make_test_array(7), &senders, &mut cursor).await;
3466 assert_eq!(cursor, 2); assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3469 assert_eq!(sb.dropped_arrays_counter().load(Ordering::Acquire), 1);
3470 assert_eq!(ra.recv().await.unwrap().unique_id, 91);
3472 assert_eq!(rb.recv().await.unwrap().unique_id, 92);
3473 });
3474 }
3475
3476 #[test]
3477 fn test_scatter_cursor_advances_per_attempt_across_frames() {
3478 let (sa, _ra) = ndarray_channel("A", 1);
3483 let (sb, mut rb) = ndarray_channel("B", 10);
3484 let (sc, mut rc) = ndarray_channel("C", 10);
3485 block(async {
3486 sa.publish(make_test_array(90)).await; let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3488 let mut cursor = 0usize;
3489 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3490 assert_eq!(cursor, 2); assert_eq!(rb.recv().await.unwrap().unique_id, 0);
3492 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3493 assert_eq!(cursor, 3); assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3495 });
3496 }
3497
3498 #[test]
3499 fn test_scatter_skips_disabled_consumer() {
3500 let (sa, mut ra) = ndarray_channel("A", 10);
3503 let (mut sb, _rb) = ndarray_channel("B", 10);
3504 let (sc, mut rc) = ndarray_channel("C", 10);
3505 sb.set_mode_flags(
3506 Arc::new(AtomicBool::new(false)),
3507 Arc::new(AtomicBool::new(false)),
3508 );
3509 block(async {
3510 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3511 let mut cursor = 0usize;
3512 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3514 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3515 assert_eq!(ra.recv().await.unwrap().unique_id, 0);
3516 assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3517 });
3518 }
3519}