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;
32use asyn_rs::param::ParamValue;
33
34use super::channel::{
35 NDArrayOutput, NDArrayReceiver, NDArraySender, PublishOutcome, ndarray_channel,
36};
37use super::params::PluginBaseParams;
38use super::wiring::{WiringRegistry, upstream_key};
39
40#[derive(Debug)]
47enum PluginParamMsg {
48 Change(usize, i32, ParamChangeValue),
50 Barrier(std::sync::mpsc::SyncSender<()>),
57}
58
59#[derive(Debug, Clone)]
61pub enum ParamChangeValue {
62 Int32(i32),
63 Float64(f64),
64 Octet(String),
65}
66
67impl ParamChangeValue {
68 pub fn as_i32(&self) -> i32 {
69 match self {
70 ParamChangeValue::Int32(v) => *v,
71 ParamChangeValue::Float64(v) => *v as i32,
72 ParamChangeValue::Octet(_) => 0,
73 }
74 }
75
76 pub fn as_f64(&self) -> f64 {
77 match self {
78 ParamChangeValue::Int32(v) => *v as f64,
79 ParamChangeValue::Float64(v) => *v,
80 ParamChangeValue::Octet(_) => 0.0,
81 }
82 }
83
84 pub fn as_string(&self) -> Option<&str> {
85 match self {
86 ParamChangeValue::Octet(s) => Some(s),
87 _ => None,
88 }
89 }
90}
91
92pub enum ParamUpdate {
94 Int32 {
95 reason: usize,
96 addr: i32,
97 value: i32,
98 },
99 Float64 {
100 reason: usize,
101 addr: i32,
102 value: f64,
103 },
104 Octet {
105 reason: usize,
106 addr: i32,
107 value: String,
108 },
109 Float64Array {
110 reason: usize,
111 addr: i32,
112 value: Vec<f64>,
113 },
114}
115
116impl ParamUpdate {
117 pub fn int32(reason: usize, value: i32) -> Self {
119 Self::Int32 {
120 reason,
121 addr: 0,
122 value,
123 }
124 }
125 pub fn float64(reason: usize, value: f64) -> Self {
127 Self::Float64 {
128 reason,
129 addr: 0,
130 value,
131 }
132 }
133 pub fn int32_addr(reason: usize, addr: i32, value: i32) -> Self {
135 Self::Int32 {
136 reason,
137 addr,
138 value,
139 }
140 }
141 pub fn float64_addr(reason: usize, addr: i32, value: f64) -> Self {
143 Self::Float64 {
144 reason,
145 addr,
146 value,
147 }
148 }
149 pub fn float64_array(reason: usize, value: Vec<f64>) -> Self {
151 Self::Float64Array {
152 reason,
153 addr: 0,
154 value,
155 }
156 }
157 pub fn float64_array_addr(reason: usize, addr: i32, value: Vec<f64>) -> Self {
159 Self::Float64Array {
160 reason,
161 addr,
162 value,
163 }
164 }
165 pub fn octet(reason: usize, value: String) -> Self {
167 Self::Octet {
168 reason,
169 addr: 0,
170 value,
171 }
172 }
173 pub fn octet_addr(reason: usize, addr: i32, value: String) -> Self {
175 Self::Octet {
176 reason,
177 addr,
178 value,
179 }
180 }
181}
182
183pub struct ProcessResult {
185 pub output_arrays: Vec<Arc<NDArray>>,
186 pub param_updates: Vec<ParamUpdate>,
187 pub scatter: bool,
194}
195
196impl ProcessResult {
197 pub fn sink(param_updates: Vec<ParamUpdate>) -> Self {
199 Self {
200 output_arrays: vec![],
201 param_updates,
202 scatter: false,
203 }
204 }
205
206 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
208 Self {
209 output_arrays,
210 param_updates: vec![],
211 scatter: false,
212 }
213 }
214
215 pub fn empty() -> Self {
217 Self {
218 output_arrays: vec![],
219 param_updates: vec![],
220 scatter: false,
221 }
222 }
223
224 pub fn scatter(output_arrays: Vec<Arc<NDArray>>) -> Self {
227 Self {
228 output_arrays,
229 param_updates: vec![],
230 scatter: true,
231 }
232 }
233}
234
235pub struct ParamChangeResult {
237 pub output_arrays: Vec<Arc<NDArray>>,
238 pub param_updates: Vec<ParamUpdate>,
239}
240
241impl ParamChangeResult {
242 pub fn updates(param_updates: Vec<ParamUpdate>) -> Self {
243 Self {
244 output_arrays: vec![],
245 param_updates,
246 }
247 }
248
249 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
250 Self {
251 output_arrays,
252 param_updates: vec![],
253 }
254 }
255
256 pub fn combined(output_arrays: Vec<Arc<NDArray>>, param_updates: Vec<ParamUpdate>) -> Self {
257 Self {
258 output_arrays,
259 param_updates,
260 }
261 }
262
263 pub fn empty() -> Self {
264 Self {
265 output_arrays: vec![],
266 param_updates: vec![],
267 }
268 }
269}
270
271pub trait NDPluginProcess: Send + 'static {
273 fn process_array(&mut self, array: &NDArray, pool: &NDArrayPool) -> ProcessResult;
275
276 fn plugin_type(&self) -> &str;
278
279 fn compression_aware(&self) -> bool {
285 false
286 }
287
288 fn does_array_callbacks(&self) -> bool {
295 true
296 }
297
298 fn register_params(
300 &mut self,
301 _base: &mut PortDriverBase,
302 ) -> Result<(), asyn_rs::error::AsynError> {
303 Ok(())
304 }
305
306 fn on_param_change(
309 &mut self,
310 _reason: usize,
311 _params: &PluginParamSnapshot,
312 ) -> ParamChangeResult {
313 ParamChangeResult::empty()
314 }
315
316 fn array_data_handle(&self) -> Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>> {
320 None
321 }
322}
323
324pub struct PluginParamSnapshot {
326 pub enable_callbacks: bool,
327 pub reason: usize,
329 pub addr: i32,
331 pub value: ParamChangeValue,
333}
334
335struct SortEntry {
339 arrays: Vec<Arc<NDArray>>,
340 inserted: std::time::Instant,
341}
342
343struct SortBuffer {
351 entries: BTreeMap<i32, SortEntry>,
353 prev_unique_id: i32,
355 first_output: bool,
357 disordered_arrays: i32,
359 dropped_output_arrays: i32,
362}
363
364impl SortBuffer {
365 fn new() -> Self {
366 Self {
367 entries: BTreeMap::new(),
368 prev_unique_id: 0,
369 first_output: true,
370 disordered_arrays: 0,
371 dropped_output_arrays: 0,
372 }
373 }
374
375 fn order_ok(&self, unique_id: i32) -> bool {
377 unique_id == self.prev_unique_id || unique_id == self.prev_unique_id + 1
378 }
379
380 fn note_emitted(&mut self, unique_id: i32) {
383 if !self.first_output && !self.order_ok(unique_id) {
384 self.disordered_arrays += 1;
385 }
386 self.first_output = false;
387 self.prev_unique_id = unique_id;
388 }
389
390 fn insert(&mut self, unique_id: i32, arrays: Vec<Arc<NDArray>>, sort_size: i32) -> bool {
395 if sort_size > 0 && self.entries.len() as i32 >= sort_size {
396 self.dropped_output_arrays += 1;
397 return false;
398 }
399 self.entries
400 .entry(unique_id)
401 .or_insert_with(|| SortEntry {
402 arrays: Vec::new(),
403 inserted: std::time::Instant::now(),
404 })
405 .arrays
406 .extend(arrays);
407 true
408 }
409
410 fn drain_ready(&mut self, sort_time: f64) -> Vec<(i32, Vec<Arc<NDArray>>)> {
414 let now = std::time::Instant::now();
415 let mut out = Vec::new();
416 while let Some((&head_id, entry)) = self.entries.iter().next() {
417 let delta = now.duration_since(entry.inserted).as_secs_f64();
418 let order_ok = self.order_ok(head_id);
419 if (!self.first_output && order_ok) || delta > sort_time {
420 let entry = self.entries.remove(&head_id).unwrap();
421 self.note_emitted(head_id);
422 out.push((head_id, entry.arrays));
423 } else {
424 break;
425 }
426 }
427 out
428 }
429
430 fn drain_all(&mut self) -> Vec<(i32, Vec<Arc<NDArray>>)> {
433 let entries = std::mem::take(&mut self.entries);
434 let mut out = Vec::with_capacity(entries.len());
435 for (id, entry) in entries {
436 self.note_emitted(id);
437 out.push((id, entry.arrays));
438 }
439 out
440 }
441
442 fn len(&self) -> i32 {
444 self.entries.len() as i32
445 }
446}
447
448struct SharedProcessorInner<P: NDPluginProcess> {
451 processor: P,
452 output: Arc<parking_lot::Mutex<NDArrayOutput>>,
453 pool: Arc<NDArrayPool>,
454 ndarray_params: NDArrayDriverParams,
455 plugin_params: PluginBaseParams,
456 port_handle: PortHandle,
457 array_counter: i32,
461 std_array_data_param: Option<usize>,
463 array_callbacks: bool,
470 min_callback_time: f64,
472 last_process_time: Option<std::time::Instant>,
474 sort_mode: i32,
476 sort_time: f64,
478 sort_size: i32,
480 sort_buffer: SortBuffer,
482 dropped_arrays: Arc<std::sync::atomic::AtomicI32>,
486 compression_aware: bool,
489 max_byte_rate: f64,
491 throttler: super::throttler::Throttler,
493 prev_input_array: Option<Arc<NDArray>>,
496 dims_prev: Vec<i32>,
499 nd_array_addr: i32,
501 max_threads: i32,
503 num_threads: i32,
505}
506
507impl<P: NDPluginProcess> SharedProcessorInner<P> {
508 fn should_throttle(&self) -> bool {
509 if self.min_callback_time <= 0.0 {
510 return false;
511 }
512 if let Some(last) = self.last_process_time {
513 last.elapsed().as_secs_f64() < self.min_callback_time
514 } else {
515 false
516 }
517 }
518
519 fn array_byte_cost(array: &NDArray) -> f64 {
522 match &array.codec {
523 Some(c) => c.compressed_size as f64,
524 None => array.info().total_bytes as f64,
525 }
526 }
527
528 fn throttle_ok(&mut self, array: &NDArray) -> bool {
531 if self.max_byte_rate == 0.0 {
532 return true;
533 }
534 let cost = Self::array_byte_cost(array);
535 if self.throttler.try_take(cost) {
536 true
537 } else {
538 self.sort_buffer.dropped_output_arrays += 1;
539 false
540 }
541 }
542
543 fn route_output_arrays(&mut self, arrays: Vec<Arc<NDArray>>) -> Vec<Arc<NDArray>> {
551 let mut ready = Vec::new();
552 for arr in arrays {
553 if !self.throttle_ok(&arr) {
554 continue; }
556 let uid = arr.unique_id;
557 if self.sort_mode != 0
558 && !self.sort_buffer.first_output
559 && !self.sort_buffer.order_ok(uid)
560 {
561 self.sort_buffer.insert(uid, vec![arr], self.sort_size);
563 } else {
564 self.sort_buffer.note_emitted(uid);
566 ready.push(arr);
567 }
568 }
569 if self.sort_mode != 0 {
572 for (_id, mut bucket) in self.sort_buffer.drain_ready(self.sort_time) {
573 ready.append(&mut bucket);
574 }
575 }
576 ready
577 }
578
579 fn process_and_publish(&mut self, array: &Arc<NDArray>) -> Option<ProcessOutput> {
583 if self.should_throttle() {
590 return None;
591 }
592 self.prev_input_array = Some(Arc::clone(array));
597 let t0 = std::time::Instant::now();
598 let result = self.processor.process_array(array, &self.pool);
599 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
600 self.last_process_time = Some(t0);
601
602 let produced = result.output_arrays.len();
616 let ready = if self.array_callbacks || self.std_array_data_param.is_some() {
617 self.route_output_arrays(result.output_arrays)
618 } else {
619 Vec::new()
620 };
621 let count_frame =
626 !(self.std_array_data_param.is_some() && produced > 0 && ready.is_empty());
627 let mut output = self.build_publish_batch(
628 ready,
629 result.param_updates,
630 result.scatter,
631 Some(array.as_ref()),
632 elapsed_ms,
633 self.array_callbacks,
634 count_frame,
635 );
636 output.batch.merge(self.build_status_params_batch());
637 Some(output)
638 }
639
640 fn dropped_arrays_only_batch(&self) -> ProcessOutput {
643 ProcessOutput {
644 arrays: vec![],
645 scatter: false,
646 batch: self.build_status_params_batch(),
647 }
648 }
649
650 fn process_plugin(&mut self) -> Option<ProcessOutput> {
653 let prev = self.prev_input_array.clone()?;
654 self.process_and_publish(&prev)
655 }
656
657 fn tick_sort_buffer(&mut self) -> ProcessOutput {
660 let entries = self.sort_buffer.drain_ready(self.sort_time);
661 self.emit_drained(entries)
662 }
663
664 fn flush_sort_buffer(&mut self) -> ProcessOutput {
666 let entries = self.sort_buffer.drain_all();
667 self.emit_drained(entries)
668 }
669
670 fn emit_drained(&mut self, entries: Vec<(i32, Vec<Arc<NDArray>>)>) -> ProcessOutput {
671 let mut all_arrays = Vec::new();
672 let mut combined = ParamBatch::empty();
673 for (_unique_id, arrays) in entries {
674 let output = self.build_publish_batch(arrays, vec![], false, None, 0.0, true, true);
679 all_arrays.extend(output.arrays);
680 combined.merge(output.batch);
681 }
682 combined.merge(self.build_sort_params_batch());
683 ProcessOutput {
684 arrays: all_arrays,
685 scatter: false,
686 batch: combined,
687 }
688 }
689
690 fn build_sort_params_batch(&self) -> ParamBatch {
691 use asyn_rs::request::ParamSetValue;
692 let sort_free = self.sort_size - self.sort_buffer.len();
693 ParamBatch {
694 addr0: vec![
695 ParamSetValue::new(
696 self.plugin_params.sort_free,
697 0,
698 ParamValue::Int32(sort_free),
699 ),
700 ParamSetValue::new(
701 self.plugin_params.disordered_arrays,
702 0,
703 ParamValue::Int32(self.sort_buffer.disordered_arrays),
704 ),
705 ParamSetValue::new(
706 self.plugin_params.dropped_output_arrays,
707 0,
708 ParamValue::Int32(self.sort_buffer.dropped_output_arrays),
709 ),
710 ],
711 extra: std::collections::HashMap::new(),
712 }
713 }
714
715 fn build_status_params_batch(&self) -> ParamBatch {
718 use asyn_rs::request::ParamSetValue;
719 let mut batch = self.build_sort_params_batch();
720 batch.addr0.push(ParamSetValue::new(
721 self.plugin_params.dropped_arrays,
722 0,
723 ParamValue::Int32(
724 self.dropped_arrays
725 .load(std::sync::atomic::Ordering::Acquire),
726 ),
727 ));
728 batch
729 }
730
731 fn build_publish_batch(
741 &mut self,
742 output_arrays: Vec<Arc<NDArray>>,
743 param_updates: Vec<ParamUpdate>,
744 scatter: bool,
745 fallback_array: Option<&NDArray>,
746 elapsed_ms: f64,
747 deliver: bool,
748 count_frame: bool,
749 ) -> ProcessOutput {
750 use asyn_rs::request::ParamSetValue;
751
752 let mut addr0: Vec<ParamSetValue> = Vec::new();
753 let mut extra: std::collections::HashMap<i32, Vec<ParamSetValue>> =
754 std::collections::HashMap::new();
755
756 if let Some(report_arr) = output_arrays.first().map(|a| a.as_ref()).or(fallback_array) {
757 if count_frame {
762 self.array_counter += 1;
763 }
764
765 if let (Some(param), Some(served)) = (
776 self.std_array_data_param,
777 output_arrays.first().map(|a| a.as_ref()),
778 ) {
779 use crate::ndarray::NDDataBuffer;
780 use asyn_rs::param::ParamValue;
781 let value = match &served.data {
782 NDDataBuffer::I8(v) => {
783 Some(ParamValue::Int8Array(std::sync::Arc::from(v.as_slice())))
784 }
785 NDDataBuffer::U8(v) => Some(ParamValue::Int8Array(std::sync::Arc::from(
786 v.iter().map(|&x| x as i8).collect::<Vec<_>>().as_slice(),
787 ))),
788 NDDataBuffer::I16(v) => {
789 Some(ParamValue::Int16Array(std::sync::Arc::from(v.as_slice())))
790 }
791 NDDataBuffer::U16(v) => Some(ParamValue::Int16Array(std::sync::Arc::from(
792 v.iter().map(|&x| x as i16).collect::<Vec<_>>().as_slice(),
793 ))),
794 NDDataBuffer::I32(v) => {
795 Some(ParamValue::Int32Array(std::sync::Arc::from(v.as_slice())))
796 }
797 NDDataBuffer::U32(v) => Some(ParamValue::Int32Array(std::sync::Arc::from(
798 v.iter().map(|&x| x as i32).collect::<Vec<_>>().as_slice(),
799 ))),
800 NDDataBuffer::I64(v) => {
801 Some(ParamValue::Int64Array(std::sync::Arc::from(v.as_slice())))
802 }
803 NDDataBuffer::U64(v) => Some(ParamValue::Int64Array(std::sync::Arc::from(
804 v.iter().map(|&x| x as i64).collect::<Vec<_>>().as_slice(),
805 ))),
806 NDDataBuffer::F32(v) => {
807 Some(ParamValue::Float32Array(std::sync::Arc::from(v.as_slice())))
808 }
809 NDDataBuffer::F64(v) => {
810 Some(ParamValue::Float64Array(std::sync::Arc::from(v.as_slice())))
811 }
812 };
813 if let Some(value) = value {
814 let ts = served.timestamp.to_system_time();
815 self.port_handle
816 .interrupts()
817 .notify(asyn_rs::interrupt::InterruptValue {
818 reason: param,
819 addr: 0,
820 value,
821 timestamp: ts,
822 uint32_changed_mask: 0,
823 ..Default::default()
824 });
825 }
826 }
827
828 let info = report_arr.info();
829 let color_mode = report_arr
833 .attributes
834 .get("ColorMode")
835 .and_then(|a| a.value.as_i64())
836 .map(|v| v as i32)
837 .unwrap_or(info.color_mode as i32);
838 let bayer_pattern = report_arr
839 .attributes
840 .get("BayerPattern")
841 .and_then(|a| a.value.as_i64())
842 .map(|v| v as i32)
843 .unwrap_or(0);
844
845 let mut cur_dims = vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS];
852 for (slot, d) in cur_dims.iter_mut().zip(
853 report_arr
854 .dims
855 .iter()
856 .take(crate::ndarray::ND_ARRAY_MAX_DIMS),
857 ) {
858 *slot = d.size as i32;
859 }
860 if cur_dims != self.dims_prev {
861 self.dims_prev = cur_dims.clone();
862 self.port_handle
863 .interrupts()
864 .notify(asyn_rs::interrupt::InterruptValue {
865 reason: self.ndarray_params.array_dimensions,
866 addr: 0,
867 value: asyn_rs::param::ParamValue::Int32Array(std::sync::Arc::from(
868 cur_dims.as_slice(),
869 )),
870 timestamp: report_arr.timestamp.to_system_time(),
871 uint32_changed_mask: 0,
872 ..Default::default()
873 });
874 }
875
876 addr0.extend([
877 ParamSetValue::new(
878 self.ndarray_params.array_counter,
879 0,
880 ParamValue::Int32(self.array_counter),
881 ),
882 ParamSetValue::new(
883 self.ndarray_params.unique_id,
884 0,
885 ParamValue::Int32(report_arr.unique_id),
886 ),
887 ParamSetValue::new(
888 self.ndarray_params.n_dimensions,
889 0,
890 ParamValue::Int32(report_arr.dims.len() as i32),
891 ),
892 ParamSetValue::new(
893 self.ndarray_params.array_size_x,
894 0,
895 ParamValue::Int32(info.x_size as i32),
896 ),
897 ParamSetValue::new(
898 self.ndarray_params.array_size_y,
899 0,
900 ParamValue::Int32(info.y_size as i32),
901 ),
902 ParamSetValue::new(
903 self.ndarray_params.array_size_z,
904 0,
905 ParamValue::Int32(info.color_size as i32),
906 ),
907 ParamSetValue::new(
908 self.ndarray_params.array_size,
909 0,
910 ParamValue::Int32(info.total_bytes as i32),
911 ),
912 ParamSetValue::new(
913 self.ndarray_params.data_type,
914 0,
915 ParamValue::Int32(report_arr.data.data_type() as i32),
916 ),
917 ParamSetValue::new(
918 self.ndarray_params.color_mode,
919 0,
920 ParamValue::Int32(color_mode),
921 ),
922 ParamSetValue::new(
923 self.ndarray_params.bayer_pattern,
924 0,
925 ParamValue::Int32(bayer_pattern),
926 ),
927 ParamSetValue::new(
928 self.ndarray_params.timestamp_rbv,
929 0,
930 ParamValue::Float64(report_arr.time_stamp),
935 ),
936 ParamSetValue::new(
937 self.ndarray_params.epics_ts_sec,
938 0,
939 ParamValue::Int32(report_arr.timestamp.sec as i32),
940 ),
941 ParamSetValue::new(
942 self.ndarray_params.epics_ts_nsec,
943 0,
944 ParamValue::Int32(report_arr.timestamp.nsec as i32),
945 ),
946 ]);
947
948 match &report_arr.codec {
954 Some(codec) => {
955 addr0.push(ParamSetValue::new(
956 self.ndarray_params.codec,
957 0,
958 ParamValue::Octet(codec.name.as_str().to_string()),
959 ));
960 addr0.push(ParamSetValue::new(
961 self.ndarray_params.compressed_size,
962 0,
963 ParamValue::Int32(codec.compressed_size as i32),
964 ));
965 }
966 None => {
967 addr0.push(ParamSetValue::new(
968 self.ndarray_params.codec,
969 0,
970 ParamValue::Octet(String::new()),
971 ));
972 addr0.push(ParamSetValue::new(
973 self.ndarray_params.compressed_size,
974 0,
975 ParamValue::Int32(info.total_bytes as i32),
976 ));
977 }
978 }
979 }
980
981 addr0.push(ParamSetValue::new(
982 self.plugin_params.execution_time,
983 0,
984 ParamValue::Float64(elapsed_ms),
985 ));
986
987 for update in ¶m_updates {
992 match update {
993 ParamUpdate::Int32 {
994 reason,
995 addr,
996 value,
997 } => {
998 let pv = ParamSetValue::new(*reason, *addr, ParamValue::Int32(*value));
999 if *addr == 0 {
1000 addr0.push(pv);
1001 } else {
1002 extra.entry(*addr).or_default().push(pv);
1003 }
1004 }
1005 ParamUpdate::Float64 {
1006 reason,
1007 addr,
1008 value,
1009 } => {
1010 let pv = ParamSetValue::new(*reason, *addr, ParamValue::Float64(*value));
1011 if *addr == 0 {
1012 addr0.push(pv);
1013 } else {
1014 extra.entry(*addr).or_default().push(pv);
1015 }
1016 }
1017 ParamUpdate::Octet {
1018 reason,
1019 addr,
1020 value,
1021 } => {
1022 let pv = ParamSetValue::new(*reason, *addr, ParamValue::Octet(value.clone()));
1023 if *addr == 0 {
1024 addr0.push(pv);
1025 } else {
1026 extra.entry(*addr).or_default().push(pv);
1027 }
1028 }
1029 ParamUpdate::Float64Array {
1030 reason,
1031 addr,
1032 value,
1033 } => {
1034 let pv = ParamSetValue::new(
1035 *reason,
1036 *addr,
1037 ParamValue::Float64Array(value.clone().into()),
1038 );
1039 if *addr == 0 {
1040 addr0.push(pv);
1041 } else {
1042 extra.entry(*addr).or_default().push(pv);
1043 }
1044 }
1045 }
1046 }
1047
1048 ProcessOutput {
1049 arrays: if deliver { output_arrays } else { Vec::new() },
1053 scatter,
1054 batch: ParamBatch { addr0, extra },
1055 }
1056 }
1057}
1058
1059struct ProcessOutput {
1061 arrays: Vec<Arc<NDArray>>,
1062 scatter: bool,
1063 batch: ParamBatch,
1064}
1065
1066impl ProcessOutput {
1067 async fn publish_arrays(&self, senders: &[NDArraySender], scatter_cursor: &mut usize) {
1075 for arr in &self.arrays {
1076 if self.scatter {
1077 Self::scatter_publish(arr, senders, scatter_cursor).await;
1078 } else {
1079 let futs = senders.iter().map(|s| s.publish(arr.clone()));
1080 futures_util::future::join_all(futs).await;
1081 }
1082 }
1083 }
1084
1085 async fn scatter_publish(arr: &Arc<NDArray>, senders: &[NDArraySender], cursor: &mut usize) {
1105 let active: Vec<&NDArraySender> = senders.iter().filter(|s| s.is_enabled()).collect();
1106 let n = active.len();
1107 if n == 0 {
1108 return;
1109 }
1110 for attempt in 0..n {
1111 let target = *cursor % n;
1112 *cursor = cursor.wrapping_add(1);
1113 let is_last = attempt == n - 1;
1114 match active[target].publish_scatter(arr.clone(), is_last).await {
1115 PublishOutcome::Delivered => break,
1119 PublishOutcome::DroppedQueueFull
1123 | PublishOutcome::Disabled
1124 | PublishOutcome::ChannelClosed => {
1125 if is_last {
1126 break;
1127 }
1128 }
1129 }
1130 }
1131 }
1132}
1133
1134struct ParamBatch {
1137 addr0: Vec<asyn_rs::request::ParamSetValue>,
1138 extra: std::collections::HashMap<i32, Vec<asyn_rs::request::ParamSetValue>>,
1139}
1140
1141impl ParamBatch {
1142 fn empty() -> Self {
1143 Self {
1144 addr0: Vec::new(),
1145 extra: std::collections::HashMap::new(),
1146 }
1147 }
1148
1149 fn merge(&mut self, other: ParamBatch) {
1150 self.addr0.extend(other.addr0);
1151 for (addr, updates) in other.extra {
1152 self.extra.entry(addr).or_default().extend(updates);
1153 }
1154 }
1155
1156 async fn flush(self, port: &asyn_rs::port_handle::PortHandle) {
1158 if !self.addr0.is_empty() {
1159 if let Err(e) = port.set_params_and_notify(0, self.addr0).await {
1160 eprintln!("plugin param flush error (addr 0): {e}");
1161 }
1162 }
1163 for (addr, updates) in self.extra {
1164 if let Err(e) = port.set_params_and_notify(addr, updates).await {
1165 eprintln!("plugin param flush error (addr {addr}): {e}");
1166 }
1167 }
1168 }
1169}
1170
1171#[allow(dead_code)]
1173pub struct PluginPortDriver {
1174 base: PortDriverBase,
1175 ndarray_params: NDArrayDriverParams,
1176 plugin_params: PluginBaseParams,
1177 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1178 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1180 std_array_data_param: Option<usize>,
1182}
1183
1184impl PluginPortDriver {
1185 fn new<P: NDPluginProcess>(
1186 port_name: &str,
1187 plugin_type_name: &str,
1188 queue_size: usize,
1189 ndarray_port: &str,
1190 max_addr: usize,
1191 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1192 processor: &mut P,
1193 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1194 ) -> AsynResult<Self> {
1195 let mut base = PortDriverBase::new(
1196 port_name,
1197 max_addr,
1198 PortFlags {
1199 can_block: true,
1200 ..Default::default()
1201 },
1202 );
1203
1204 let ndarray_params = NDArrayDriverParams::create(&mut base)?;
1205 let plugin_params = PluginBaseParams::create(&mut base)?;
1206
1207 base.set_int32_param(plugin_params.enable_callbacks, 0, 0)?;
1209 base.set_int32_param(plugin_params.blocking_callbacks, 0, 0)?;
1210 base.set_int32_param(plugin_params.queue_size, 0, queue_size as i32)?;
1211 base.set_int32_param(plugin_params.dropped_arrays, 0, 0)?;
1212 base.set_int32_param(plugin_params.queue_use, 0, 0)?;
1213 base.set_string_param(plugin_params.plugin_type, 0, plugin_type_name.into())?;
1214 base.set_int32_param(
1215 ndarray_params.array_callbacks,
1216 0,
1217 processor.does_array_callbacks() as i32,
1218 )?;
1219 base.set_int32_param(ndarray_params.write_file, 0, 0)?;
1220 base.set_int32_param(ndarray_params.read_file, 0, 0)?;
1221 base.set_int32_param(ndarray_params.capture, 0, 0)?;
1222 base.set_int32_param(ndarray_params.file_write_status, 0, 0)?;
1223 base.set_string_param(ndarray_params.file_write_message, 0, "".into())?;
1224 base.set_string_param(ndarray_params.file_path, 0, "".into())?;
1225 base.set_string_param(ndarray_params.file_name, 0, "".into())?;
1226 base.set_int32_param(ndarray_params.file_number, 0, 0)?;
1227 base.set_int32_param(ndarray_params.auto_increment, 0, 0)?;
1228 base.set_string_param(ndarray_params.file_template, 0, "%s%s_%3.3d.dat".into())?;
1229 base.set_string_param(ndarray_params.full_file_name, 0, "".into())?;
1230 base.set_int32_param(ndarray_params.create_dir, 0, 0)?;
1231 base.set_string_param(ndarray_params.temp_suffix, 0, "".into())?;
1232
1233 base.set_string_param(ndarray_params.port_name_self, 0, port_name.into())?;
1235 base.set_string_param(
1236 ndarray_params.ad_core_version,
1237 0,
1238 env!("CARGO_PKG_VERSION").into(),
1239 )?;
1240 base.set_string_param(
1241 ndarray_params.driver_version,
1242 0,
1243 env!("CARGO_PKG_VERSION").into(),
1244 )?;
1245 if !ndarray_port.is_empty() {
1246 base.set_string_param(plugin_params.nd_array_port, 0, ndarray_port.into())?;
1247 }
1248
1249 let std_array_data_param = if array_data.is_some() {
1251 Some(base.create_param("STD_ARRAY_DATA", asyn_rs::param::ParamType::GenericPointer)?)
1252 } else {
1253 None
1254 };
1255
1256 processor.register_params(&mut base)?;
1258
1259 Ok(Self {
1260 base,
1261 ndarray_params,
1262 plugin_params,
1263 param_change_tx,
1264 array_data,
1265 std_array_data_param,
1266 })
1267 }
1268}
1269
1270fn copy_direct<T: Copy>(src: &[T], dst: &mut [T]) -> usize {
1272 let n = src.len().min(dst.len());
1273 dst[..n].copy_from_slice(&src[..n]);
1274 n
1275}
1276
1277fn copy_convert<S, D>(src: &[S], dst: &mut [D]) -> usize
1279where
1280 S: CastToF64 + Copy,
1281 D: CastFromF64 + Copy,
1282{
1283 let n = src.len().min(dst.len());
1284 for i in 0..n {
1285 dst[i] = D::cast_from_f64(src[i].cast_to_f64());
1286 }
1287 n
1288}
1289
1290trait CCastTo<D> {
1306 fn ccast(self) -> D;
1307}
1308macro_rules! impl_ccast {
1309 ( $src:ty => $( $dst:ty ),+ ) => {
1310 $(
1311 impl CCastTo<$dst> for $src {
1312 #[inline]
1313 fn ccast(self) -> $dst {
1314 self as $dst
1315 }
1316 }
1317 )+
1318 };
1319}
1320impl_ccast!(i8 => i16, i32, i64);
1321impl_ccast!(u8 => i8, i16, i32, i64);
1322impl_ccast!(i16 => i8, i32, i64);
1323impl_ccast!(u16 => i8, i16, i32, i64);
1324impl_ccast!(i32 => i8, i16, i64);
1325impl_ccast!(u32 => i8, i16, i32, i64);
1326impl_ccast!(i64 => i8, i16, i32);
1327impl_ccast!(u64 => i8, i16, i32, i64);
1328
1329fn copy_ccast<S, D>(src: &[S], dst: &mut [D]) -> usize
1333where
1334 S: CCastTo<D> + Copy,
1335 D: Copy,
1336{
1337 let n = src.len().min(dst.len());
1338 for i in 0..n {
1339 dst[i] = src[i].ccast();
1340 }
1341 n
1342}
1343
1344trait CastToF64 {
1346 fn cast_to_f64(self) -> f64;
1347}
1348
1349impl CastToF64 for i8 {
1350 fn cast_to_f64(self) -> f64 {
1351 self as f64
1352 }
1353}
1354impl CastToF64 for u8 {
1355 fn cast_to_f64(self) -> f64 {
1356 self as f64
1357 }
1358}
1359impl CastToF64 for i16 {
1360 fn cast_to_f64(self) -> f64 {
1361 self as f64
1362 }
1363}
1364impl CastToF64 for u16 {
1365 fn cast_to_f64(self) -> f64 {
1366 self as f64
1367 }
1368}
1369impl CastToF64 for i32 {
1370 fn cast_to_f64(self) -> f64 {
1371 self as f64
1372 }
1373}
1374impl CastToF64 for u32 {
1375 fn cast_to_f64(self) -> f64 {
1376 self as f64
1377 }
1378}
1379impl CastToF64 for i64 {
1380 fn cast_to_f64(self) -> f64 {
1381 self as f64
1382 }
1383}
1384impl CastToF64 for u64 {
1385 fn cast_to_f64(self) -> f64 {
1386 self as f64
1387 }
1388}
1389impl CastToF64 for f32 {
1390 fn cast_to_f64(self) -> f64 {
1391 self as f64
1392 }
1393}
1394impl CastToF64 for f64 {
1395 fn cast_to_f64(self) -> f64 {
1396 self
1397 }
1398}
1399
1400trait CastFromF64 {
1402 fn cast_from_f64(v: f64) -> Self;
1403}
1404
1405impl CastFromF64 for i8 {
1406 fn cast_from_f64(v: f64) -> Self {
1407 v as i8
1408 }
1409}
1410impl CastFromF64 for i16 {
1411 fn cast_from_f64(v: f64) -> Self {
1412 v as i16
1413 }
1414}
1415impl CastFromF64 for i32 {
1416 fn cast_from_f64(v: f64) -> Self {
1417 v as i32
1418 }
1419}
1420impl CastFromF64 for i64 {
1421 fn cast_from_f64(v: f64) -> Self {
1422 v as i64
1423 }
1424}
1425impl CastFromF64 for f32 {
1426 fn cast_from_f64(v: f64) -> Self {
1427 v as f32
1428 }
1429}
1430impl CastFromF64 for f64 {
1431 fn cast_from_f64(v: f64) -> Self {
1432 v
1433 }
1434}
1435
1436macro_rules! impl_read_array {
1439 (
1440 $self:expr, $buf:expr, $direct_variant:ident,
1441 ccast: [ $( $ccast_variant:ident ),* ],
1442 convert: [ $( $variant:ident ),* ]
1443 ) => {{
1444 use crate::ndarray::NDDataBuffer;
1445 let handle = match &$self.array_data {
1446 Some(h) => h,
1447 None => return Ok(0),
1448 };
1449 let guard = handle.lock();
1450 let array = match &*guard {
1451 Some(a) => a,
1452 None => return Ok(0),
1453 };
1454 let n = match &array.data {
1455 NDDataBuffer::$direct_variant(v) => copy_direct(v, $buf),
1456 $( NDDataBuffer::$ccast_variant(v) => copy_ccast(v, $buf), )*
1457 $( NDDataBuffer::$variant(v) => copy_convert(v, $buf), )*
1458 };
1459 Ok(n)
1460 }};
1461}
1462
1463impl PortDriver for PluginPortDriver {
1464 fn base(&self) -> &PortDriverBase {
1465 &self.base
1466 }
1467
1468 fn base_mut(&mut self) -> &mut PortDriverBase {
1469 &mut self.base
1470 }
1471
1472 fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1473 let reason = user.reason;
1474 let addr = user.addr;
1475 self.base.set_int32_param(reason, addr, value)?;
1476 self.base.call_param_callbacks(addr)?;
1477 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1479 reason,
1480 addr,
1481 ParamChangeValue::Int32(value),
1482 ));
1483 Ok(())
1484 }
1485
1486 fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1487 let reason = user.reason;
1488 let addr = user.addr;
1489 self.base.set_float64_param(reason, addr, value)?;
1490 self.base.call_param_callbacks(addr)?;
1491 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1492 reason,
1493 addr,
1494 ParamChangeValue::Float64(value),
1495 ));
1496 Ok(())
1497 }
1498
1499 fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1500 let reason = user.reason;
1501 let addr = user.addr;
1502 let s = String::from_utf8_lossy(data).into_owned();
1503 self.base.set_string_param(reason, addr, s.clone())?;
1504 self.base.call_param_callbacks(addr)?;
1505 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1506 reason,
1507 addr,
1508 ParamChangeValue::Octet(s),
1509 ));
1510 Ok(data.len())
1511 }
1512
1513 fn read_int8_array(&mut self, _user: &AsynUser, buf: &mut [i8]) -> AsynResult<usize> {
1514 impl_read_array!(
1517 self, buf, I8,
1518 ccast: [U8, I16, U16, I32, U32, I64, U64],
1519 convert: [F32, F64]
1520 )
1521 }
1522
1523 fn read_int16_array(&mut self, _user: &AsynUser, buf: &mut [i16]) -> AsynResult<usize> {
1524 impl_read_array!(
1525 self, buf, I16,
1526 ccast: [I8, U8, U16, I32, U32, I64, U64],
1527 convert: [F32, F64]
1528 )
1529 }
1530
1531 fn read_int32_array(&mut self, _user: &AsynUser, buf: &mut [i32]) -> AsynResult<usize> {
1532 impl_read_array!(
1533 self, buf, I32,
1534 ccast: [I8, U8, I16, U16, U32, I64, U64],
1535 convert: [F32, F64]
1536 )
1537 }
1538
1539 fn read_int64_array(&mut self, _user: &AsynUser, buf: &mut [i64]) -> AsynResult<usize> {
1540 impl_read_array!(
1541 self, buf, I64,
1542 ccast: [I8, U8, I16, U16, I32, U32, U64],
1543 convert: [F32, F64]
1544 )
1545 }
1546
1547 fn read_float32_array(&mut self, _user: &AsynUser, buf: &mut [f32]) -> AsynResult<usize> {
1548 impl_read_array!(
1549 self, buf, F32,
1550 ccast: [],
1551 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F64]
1552 )
1553 }
1554
1555 fn read_float64_array(&mut self, _user: &AsynUser, buf: &mut [f64]) -> AsynResult<usize> {
1556 impl_read_array!(
1557 self, buf, F64,
1558 ccast: [],
1559 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F32]
1560 )
1561 }
1562}
1563
1564#[derive(Clone)]
1566pub struct PluginRuntimeHandle {
1567 port_runtime: PortRuntimeHandle,
1568 array_sender: NDArraySender,
1569 array_output: Arc<parking_lot::Mutex<NDArrayOutput>>,
1570 port_name: String,
1571 param_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1572 pub ndarray_params: NDArrayDriverParams,
1573 pub plugin_params: PluginBaseParams,
1574}
1575
1576impl PluginRuntimeHandle {
1577 pub fn port_runtime(&self) -> &PortRuntimeHandle {
1578 &self.port_runtime
1579 }
1580
1581 pub fn array_sender(&self) -> &NDArraySender {
1582 &self.array_sender
1583 }
1584
1585 pub fn array_output(&self) -> &Arc<parking_lot::Mutex<NDArrayOutput>> {
1586 &self.array_output
1587 }
1588
1589 pub fn wait_params_applied(&self, timeout: std::time::Duration) -> bool {
1607 let (ack_tx, ack_rx) = std::sync::mpsc::sync_channel(1);
1608 if self.param_tx.send(PluginParamMsg::Barrier(ack_tx)).is_err() {
1609 return false;
1610 }
1611 ack_rx.recv_timeout(timeout).is_ok()
1612 }
1613
1614 pub fn port_name(&self) -> &str {
1615 &self.port_name
1616 }
1617}
1618
1619pub fn create_plugin_runtime<P: NDPluginProcess>(
1626 port_name: &str,
1627 processor: P,
1628 pool: Arc<NDArrayPool>,
1629 queue_size: usize,
1630 ndarray_port: &str,
1631 wiring: Arc<WiringRegistry>,
1632) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1633 create_plugin_runtime_multi_addr(
1634 port_name,
1635 processor,
1636 pool,
1637 queue_size,
1638 ndarray_port,
1639 wiring,
1640 1,
1641 )
1642}
1643
1644pub fn create_plugin_runtime_multi_addr<P: NDPluginProcess>(
1648 port_name: &str,
1649 mut processor: P,
1650 pool: Arc<NDArrayPool>,
1651 queue_size: usize,
1652 ndarray_port: &str,
1653 wiring: Arc<WiringRegistry>,
1654 max_addr: usize,
1655) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1656 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
1661 let handle_param_tx = param_tx.clone();
1662
1663 let plugin_type_name = processor.plugin_type().to_string();
1665 let compression_aware = processor.compression_aware();
1666 let does_array_callbacks = processor.does_array_callbacks();
1667 let array_data = processor.array_data_handle();
1668
1669 let driver = PluginPortDriver::new(
1671 port_name,
1672 &plugin_type_name,
1673 queue_size,
1674 ndarray_port,
1675 max_addr,
1676 param_tx,
1677 &mut processor,
1678 array_data,
1679 )
1680 .expect("failed to create plugin port driver");
1681
1682 let ndarray_params = driver.ndarray_params;
1683 let plugin_params = driver.plugin_params;
1684 let std_array_data_param = driver.std_array_data_param;
1685
1686 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default());
1688
1689 let port_handle = port_runtime.port_handle().clone();
1691
1692 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
1694
1695 let enabled = Arc::new(AtomicBool::new(false));
1697 let blocking_mode = Arc::new(AtomicBool::new(false));
1698
1699 let array_output = Arc::new(parking_lot::Mutex::new(NDArrayOutput::new()));
1701 let array_output_for_handle = array_output.clone();
1702 wiring.register_output_addrs(port_name, max_addr, array_output.clone());
1708 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
1711 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
1712 processor,
1713 output: array_output,
1714 pool,
1715 ndarray_params,
1716 plugin_params,
1717 port_handle,
1718 array_counter: 0,
1719 std_array_data_param,
1720 array_callbacks: does_array_callbacks,
1723 min_callback_time: 0.0,
1724 last_process_time: None,
1725 sort_mode: 0,
1726 sort_time: 0.0,
1727 sort_size: 10,
1728 sort_buffer: SortBuffer::new(),
1729 dropped_arrays: dropped_arrays_counter,
1730 compression_aware,
1731 max_byte_rate: 0.0,
1732 throttler: super::throttler::Throttler::new(0.0),
1733 prev_input_array: None,
1734 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
1735 nd_array_addr: 0,
1736 max_threads: 1,
1737 num_threads: 1,
1738 }));
1739
1740 let data_enabled = enabled.clone();
1741 let data_blocking = blocking_mode.clone();
1742
1743 let mut array_sender = array_sender;
1744 array_sender.set_mode_flags(enabled, blocking_mode);
1745
1746 let sender_port_name = port_name.to_string();
1748 let initial_upstream = ndarray_port.to_string();
1749
1750 let data_jh = thread::Builder::new()
1752 .name(format!("plugin-data-{port_name}"))
1753 .spawn(move || {
1754 plugin_data_loop(
1755 shared,
1756 array_rx,
1757 param_rx,
1758 plugin_params,
1759 ndarray_params.array_counter,
1760 data_enabled,
1761 data_blocking,
1762 sender_port_name,
1763 initial_upstream,
1764 wiring,
1765 );
1766 })
1767 .expect("failed to spawn plugin data thread");
1768
1769 let handle = PluginRuntimeHandle {
1770 port_runtime,
1771 array_sender,
1772 array_output: array_output_for_handle,
1773 port_name: port_name.to_string(),
1774 param_tx: handle_param_tx,
1775 ndarray_params,
1776 plugin_params,
1777 };
1778
1779 (handle, data_jh)
1780}
1781
1782fn queue_status_batch(
1788 plugin_params: &PluginBaseParams,
1789 max_capacity: usize,
1790 free: i32,
1791) -> ParamBatch {
1792 use asyn_rs::request::ParamSetValue;
1793 ParamBatch {
1794 addr0: vec![
1795 ParamSetValue::new(
1796 plugin_params.queue_size,
1797 0,
1798 ParamValue::Int32(max_capacity as i32),
1799 ),
1800 ParamSetValue::new(plugin_params.queue_use, 0, ParamValue::Int32(free)),
1801 ],
1802 extra: std::collections::HashMap::new(),
1803 }
1804}
1805
1806async fn clamp_writeback(port: &PortHandle, reason: usize, value: i32) {
1809 use asyn_rs::request::ParamSetValue;
1810 let _ = port
1811 .set_params_and_notify(
1812 0,
1813 vec![ParamSetValue::new(
1814 reason,
1815 0,
1816 asyn_rs::param::ParamValue::Int32(value),
1817 )],
1818 )
1819 .await;
1820}
1821
1822fn plugin_data_loop<P: NDPluginProcess>(
1823 shared: Arc<parking_lot::Mutex<SharedProcessorInner<P>>>,
1824 mut array_rx: NDArrayReceiver,
1825 mut param_rx: tokio::sync::mpsc::UnboundedReceiver<PluginParamMsg>,
1826 plugin_params: PluginBaseParams,
1827 array_counter_reason: usize,
1828 enabled: Arc<AtomicBool>,
1829 blocking_mode: Arc<AtomicBool>,
1830 sender_port_name: String,
1831 initial_upstream: String,
1832 wiring: Arc<WiringRegistry>,
1833) {
1834 let enable_callbacks_reason = plugin_params.enable_callbacks;
1835 let blocking_callbacks_reason = plugin_params.blocking_callbacks;
1836 let min_callback_time_reason = plugin_params.min_callback_time;
1837 let sort_mode_reason = plugin_params.sort_mode;
1838 let sort_time_reason = plugin_params.sort_time;
1839 let sort_size_reason = plugin_params.sort_size;
1840 let nd_array_port_reason = plugin_params.nd_array_port;
1841 let nd_array_addr_reason = plugin_params.nd_array_addr;
1842 let process_plugin_reason = plugin_params.process_plugin;
1843 let max_byte_rate_reason = plugin_params.max_byte_rate;
1844 let num_threads_reason = plugin_params.num_threads;
1845 let max_threads_reason = plugin_params.max_threads;
1846 let array_callbacks_reason = shared.lock().ndarray_params.array_callbacks;
1847 let mut current_upstream = initial_upstream;
1851 let mut current_addr: i32 = 0;
1852 let rt = tokio::runtime::Builder::new_current_thread()
1853 .enable_all()
1854 .build()
1855 .unwrap();
1856 rt.block_on(async {
1857 let mut sort_flush_interval = tokio::time::interval(std::time::Duration::from_secs(3600));
1860 let mut sort_flush_active = false;
1861 let mut last_queue_free: Option<i32> = None;
1864 let mut scatter_cursor: usize = 0;
1869 let mut held_barriers: Vec<std::sync::mpsc::SyncSender<()>> = Vec::new();
1875
1876 loop {
1877 if !held_barriers.is_empty() && array_rx.pending() == 0 {
1881 for ack in held_barriers.drain(..) {
1882 let _ = ack.try_send(());
1883 }
1884 }
1885 tokio::select! {
1886 msg = array_rx.recv_msg() => {
1887 match msg {
1888 Some(msg) => {
1889 if !enabled.load(Ordering::Acquire) {
1894 continue;
1895 }
1896 let (process_output, senders, port) = {
1898 let mut guard = shared.lock();
1899 let compressed = msg.array.codec.is_some();
1903 let output = if compressed && !guard.compression_aware {
1904 guard
1905 .dropped_arrays
1906 .fetch_add(1, Ordering::AcqRel);
1907 Some(guard.dropped_arrays_only_batch())
1908 } else {
1909 guard.process_and_publish(&msg.array)
1914 };
1915 let senders = guard.output.lock().senders_clone();
1916 let port = guard.port_handle.clone();
1917 (output, senders, port)
1918 };
1919 let max_cap = array_rx.max_capacity();
1924 let free = max_cap.saturating_sub(array_rx.pending()) as i32;
1925 let queue_batch = if last_queue_free != Some(free) {
1926 last_queue_free = Some(free);
1927 Some(queue_status_batch(&plugin_params, max_cap, free))
1928 } else {
1929 None
1930 };
1931 if let Some(po) = process_output {
1934 po.publish_arrays(&senders, &mut scatter_cursor).await;
1935 po.batch.flush(&port).await;
1936 }
1937 if let Some(qb) = queue_batch {
1938 qb.flush(&port).await;
1939 }
1940 }
1941 None => break,
1942 }
1943 }
1944 param = param_rx.recv() => {
1945 match param {
1946 Some(PluginParamMsg::Barrier(ack)) => {
1952 held_barriers.push(ack);
1953 }
1954 Some(PluginParamMsg::Change(reason, addr, value)) => {
1955 if reason == enable_callbacks_reason {
1956 let on = value.as_i32() != 0;
1957 enabled.store(on, Ordering::Release);
1958 if !on {
1961 shared.lock().prev_input_array = None;
1962 }
1963 }
1964 if reason == blocking_callbacks_reason {
1965 blocking_mode.store(value.as_i32() != 0, Ordering::Release);
1966 }
1967 if reason == array_callbacks_reason {
1972 shared.lock().array_callbacks = value.as_i32() != 0;
1973 }
1974 if reason == min_callback_time_reason {
1976 shared.lock().min_callback_time = value.as_f64();
1977 }
1978 if reason == max_byte_rate_reason {
1981 let rate = value.as_f64();
1982 let mut guard = shared.lock();
1983 guard.max_byte_rate = rate;
1984 guard.throttler.reset(rate);
1985 }
1986 if reason == max_threads_reason {
1993 let (port, clamped, mt) = {
1995 let mut guard = shared.lock();
1996 guard.max_threads = value.as_i32().max(1);
1997 let clamped =
1998 guard.num_threads.clamp(1, guard.max_threads);
1999 guard.num_threads = clamped;
2000 (guard.port_handle.clone(), clamped, guard.max_threads)
2001 };
2002 clamp_writeback(&port, num_threads_reason, clamped).await;
2003 clamp_writeback(&port, max_threads_reason, mt).await;
2004 }
2005 if reason == num_threads_reason {
2006 let (port, clamped) = {
2007 let mut guard = shared.lock();
2008 let clamped =
2009 value.as_i32().clamp(1, guard.max_threads.max(1));
2010 guard.num_threads = clamped;
2011 (guard.port_handle.clone(), clamped)
2012 };
2013 clamp_writeback(&port, num_threads_reason, clamped).await;
2014 }
2015 if reason == nd_array_addr_reason {
2019 let new_addr = value.as_i32();
2020 if new_addr != current_addr {
2021 let old_key = upstream_key(¤t_upstream, current_addr);
2022 let new_key = upstream_key(¤t_upstream, new_addr);
2023 shared.lock().nd_array_addr = new_addr;
2024 match wiring.rewire_by_name(
2025 &sender_port_name,
2026 &old_key,
2027 &new_key,
2028 ) {
2029 Ok(()) => current_addr = new_addr,
2030 Err(e) => {
2031 eprintln!("NDArrayAddr reconnect failed: {e}");
2032 shared.lock().nd_array_addr = current_addr;
2033 }
2034 }
2035 }
2036 }
2037 if reason == process_plugin_reason && value.as_i32() != 0 {
2040 let (process_output, senders, port) = {
2041 let mut guard = shared.lock();
2042 let output = guard.process_plugin();
2043 let senders = guard.output.lock().senders_clone();
2044 let port = guard.port_handle.clone();
2045 (output, senders, port)
2046 };
2047 if let Some(po) = process_output {
2048 po.publish_arrays(&senders, &mut scatter_cursor).await;
2049 po.batch.flush(&port).await;
2050 } else {
2051 #[cfg(feature = "ioc")]
2065 if let Some(entry) =
2066 asyn_rs::asyn_record::get_port(&sender_port_name)
2067 {
2068 asyn_rs::asyn_trace!(
2069 entry.trace,
2070 sender_port_name.as_str(),
2071 asyn_rs::trace::TraceMask::WARNING,
2072 "plugin {sender_port_name}: ProcessPlugin \
2073 requested but no input array cached"
2074 );
2075 }
2076 }
2077 }
2078 if reason == array_counter_reason {
2082 shared.lock().array_counter = value.as_i32();
2083 }
2084 if reason == sort_mode_reason {
2086 let mode = value.as_i32();
2087 let flush_work = {
2090 let mut guard = shared.lock();
2091 guard.sort_mode = mode;
2092 if mode == 0 {
2093 let output = guard.flush_sort_buffer();
2094 let senders = guard.output.lock().senders_clone();
2095 let port = guard.port_handle.clone();
2096 sort_flush_active = false;
2097 Some((output, senders, port))
2098 } else {
2099 sort_flush_active = guard.sort_time > 0.0;
2100 if sort_flush_active {
2101 let dur = std::time::Duration::from_secs_f64(guard.sort_time);
2102 sort_flush_interval = tokio::time::interval(dur);
2103 }
2104 None
2105 }
2106 };
2107 if let Some((output, senders, port)) = flush_work {
2108 output.publish_arrays(&senders, &mut scatter_cursor).await;
2109 output.batch.flush(&port).await;
2110 }
2111 }
2112 if reason == sort_time_reason {
2113 let t = value.as_f64();
2114 let mut guard = shared.lock();
2115 guard.sort_time = t;
2116 if guard.sort_mode != 0 && t > 0.0 {
2117 sort_flush_active = true;
2118 let dur = std::time::Duration::from_secs_f64(t);
2119 sort_flush_interval = tokio::time::interval(dur);
2120 } else {
2121 sort_flush_active = false;
2122 }
2123 drop(guard);
2124 }
2125 if reason == sort_size_reason {
2126 shared.lock().sort_size = value.as_i32();
2127 }
2128 if reason == nd_array_port_reason {
2130 if let Some(new_port) = value.as_string() {
2131 if new_port != current_upstream {
2132 let old_key =
2133 upstream_key(¤t_upstream, current_addr);
2134 let new_key = upstream_key(new_port, current_addr);
2135 match wiring.rewire_by_name(
2136 &sender_port_name,
2137 &old_key,
2138 &new_key,
2139 ) {
2140 Ok(()) => current_upstream = new_port.to_string(),
2141 Err(e) => {
2142 eprintln!("NDArrayPort rewire failed: {e}")
2143 }
2144 }
2145 }
2146 }
2147 }
2148 let snapshot = PluginParamSnapshot {
2149 enable_callbacks: enabled.load(Ordering::Acquire),
2150 reason,
2151 addr,
2152 value,
2153 };
2154 let (process_output, senders, port) = {
2155 let mut guard = shared.lock();
2156 let t0 = std::time::Instant::now();
2157 let result = guard.processor.on_param_change(reason, &snapshot);
2158 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
2159 let output = if !result.output_arrays.is_empty() || !result.param_updates.is_empty() {
2160 let deliver = guard.array_callbacks;
2161 Some(guard.build_publish_batch(result.output_arrays, result.param_updates, false, None, elapsed_ms, deliver, true))
2162 } else {
2163 None
2164 };
2165 let senders = guard.output.lock().senders_clone();
2166 (output, senders, guard.port_handle.clone())
2167 };
2168 if let Some(po) = process_output {
2169 po.publish_arrays(&senders, &mut scatter_cursor).await;
2170 po.batch.flush(&port).await;
2171 }
2172 }
2173 None => break,
2174 }
2175 }
2176 _ = sort_flush_interval.tick(), if sort_flush_active => {
2177 let (output, senders, port) = {
2180 let mut guard = shared.lock();
2181 let output = guard.tick_sort_buffer();
2182 let senders = guard.output.lock().senders_clone();
2183 let port = guard.port_handle.clone();
2184 (output, senders, port)
2185 };
2186 output.publish_arrays(&senders, &mut scatter_cursor).await;
2187 output.batch.flush(&port).await;
2188 }
2189 }
2190 }
2191 });
2192}
2193
2194pub fn wire_downstream(upstream: &PluginRuntimeHandle, downstream_sender: NDArraySender) {
2201 upstream.array_output().lock().add(downstream_sender);
2202}
2203
2204pub fn create_plugin_runtime_with_output<P: NDPluginProcess>(
2206 port_name: &str,
2207 mut processor: P,
2208 pool: Arc<NDArrayPool>,
2209 queue_size: usize,
2210 output: NDArrayOutput,
2211 ndarray_port: &str,
2212 wiring: Arc<WiringRegistry>,
2213) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
2214 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
2218 let handle_param_tx = param_tx.clone();
2219
2220 let plugin_type_name = processor.plugin_type().to_string();
2221 let compression_aware = processor.compression_aware();
2222 let does_array_callbacks = processor.does_array_callbacks();
2223 let array_data = processor.array_data_handle();
2224 let driver = PluginPortDriver::new(
2225 port_name,
2226 &plugin_type_name,
2227 queue_size,
2228 ndarray_port,
2229 1,
2230 param_tx,
2231 &mut processor,
2232 array_data,
2233 )
2234 .expect("failed to create plugin port driver");
2235
2236 let ndarray_params = driver.ndarray_params;
2237 let plugin_params = driver.plugin_params;
2238 let std_array_data_param = driver.std_array_data_param;
2239
2240 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default());
2241
2242 let port_handle = port_runtime.port_handle().clone();
2243
2244 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
2245
2246 let enabled = Arc::new(AtomicBool::new(false));
2247 let blocking_mode = Arc::new(AtomicBool::new(false));
2248
2249 let array_output = Arc::new(parking_lot::Mutex::new(output));
2250 let array_output_for_handle = array_output.clone();
2251 wiring.register_output(port_name, array_output.clone());
2255 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
2257 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
2258 processor,
2259 output: array_output,
2260 pool,
2261 ndarray_params,
2262 plugin_params,
2263 port_handle,
2264 array_counter: 0,
2265 std_array_data_param,
2266 array_callbacks: does_array_callbacks,
2269 min_callback_time: 0.0,
2270 last_process_time: None,
2271 sort_mode: 0,
2272 sort_time: 0.0,
2273 sort_size: 10,
2274 sort_buffer: SortBuffer::new(),
2275 dropped_arrays: dropped_arrays_counter,
2276 compression_aware,
2277 max_byte_rate: 0.0,
2278 throttler: super::throttler::Throttler::new(0.0),
2279 prev_input_array: None,
2280 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
2281 nd_array_addr: 0,
2282 max_threads: 1,
2283 num_threads: 1,
2284 }));
2285
2286 let data_enabled = enabled.clone();
2287 let data_blocking = blocking_mode.clone();
2288
2289 let mut array_sender = array_sender;
2290 array_sender.set_mode_flags(enabled, blocking_mode);
2291
2292 let sender_port_name = port_name.to_string();
2294 let initial_upstream = ndarray_port.to_string();
2295
2296 let data_jh = thread::Builder::new()
2297 .name(format!("plugin-data-{port_name}"))
2298 .spawn(move || {
2299 plugin_data_loop(
2300 shared,
2301 array_rx,
2302 param_rx,
2303 plugin_params,
2304 ndarray_params.array_counter,
2305 data_enabled,
2306 data_blocking,
2307 sender_port_name,
2308 initial_upstream,
2309 wiring,
2310 );
2311 })
2312 .expect("failed to spawn plugin data thread");
2313
2314 let handle = PluginRuntimeHandle {
2315 port_runtime,
2316 array_sender,
2317 array_output: array_output_for_handle,
2318 port_name: port_name.to_string(),
2319 param_tx: handle_param_tx,
2320 ndarray_params,
2321 plugin_params,
2322 };
2323
2324 (handle, data_jh)
2325}
2326
2327#[cfg(test)]
2328mod tests {
2329 use super::*;
2330 use crate::ndarray::{NDDataType, NDDimension};
2331 use crate::plugin::channel::ndarray_channel;
2332
2333 struct PassthroughProcessor;
2335
2336 impl NDPluginProcess for PassthroughProcessor {
2337 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2338 ProcessResult::arrays(vec![Arc::new(array.clone())])
2339 }
2340 fn plugin_type(&self) -> &str {
2341 "Passthrough"
2342 }
2343 }
2344
2345 struct SinkProcessor {
2347 count: usize,
2348 }
2349
2350 impl NDPluginProcess for SinkProcessor {
2351 fn process_array(&mut self, _array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2352 self.count += 1;
2353 ProcessResult::empty()
2354 }
2355 fn plugin_type(&self) -> &str {
2356 "Sink"
2357 }
2358 }
2359
2360 fn make_test_array(id: i32) -> Arc<NDArray> {
2361 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2362 arr.unique_id = id;
2363 Arc::new(arr)
2364 }
2365
2366 fn test_wiring() -> Arc<WiringRegistry> {
2367 Arc::new(WiringRegistry::new())
2368 }
2369
2370 fn params_applied(handle: &PluginRuntimeHandle) {
2375 assert!(
2376 handle.wait_params_applied(std::time::Duration::from_secs(10)),
2377 "data thread did not apply queued param changes"
2378 );
2379 }
2380
2381 fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
2385 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2386 while !cond() {
2387 assert!(
2388 std::time::Instant::now() < deadline,
2389 "timed out waiting for {what}"
2390 );
2391 std::thread::sleep(std::time::Duration::from_millis(2));
2392 }
2393 }
2394
2395 fn enable_callbacks(handle: &PluginRuntimeHandle) {
2398 handle
2399 .port_runtime()
2400 .port_handle()
2401 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2402 .unwrap();
2403 params_applied(handle);
2404 }
2405
2406 fn send_array(sender: &NDArraySender, array: Arc<NDArray>) {
2410 let sender = sender.clone();
2411 let jh = std::thread::spawn(move || {
2412 let rt = tokio::runtime::Builder::new_current_thread()
2413 .enable_all()
2414 .build()
2415 .unwrap();
2416 rt.block_on(sender.publish(array));
2417 });
2418 jh.join().unwrap();
2419 }
2420
2421 #[test]
2422 fn test_passthrough_runtime() {
2423 let pool = Arc::new(NDArrayPool::new(1_000_000));
2424
2425 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2427 let mut output = NDArrayOutput::new();
2428 output.add(downstream_sender);
2429
2430 let (handle, _data_jh) = create_plugin_runtime_with_output(
2431 "PASS1",
2432 PassthroughProcessor,
2433 pool,
2434 10,
2435 output,
2436 "",
2437 test_wiring(),
2438 );
2439 enable_callbacks(&handle);
2440
2441 send_array(handle.array_sender(), make_test_array(42));
2443
2444 let received = downstream_rx.blocking_recv().unwrap();
2446 assert_eq!(received.unique_id, 42);
2447 }
2448
2449 #[test]
2450 fn test_sink_runtime() {
2451 let pool = Arc::new(NDArrayPool::new(1_000_000));
2452
2453 let (handle, _data_jh) = create_plugin_runtime(
2454 "SINK1",
2455 SinkProcessor { count: 0 },
2456 pool,
2457 10,
2458 "",
2459 test_wiring(),
2460 );
2461 enable_callbacks(&handle);
2462
2463 send_array(handle.array_sender(), make_test_array(1));
2465 send_array(handle.array_sender(), make_test_array(2));
2466
2467 let port = handle.port_runtime().port_handle().clone();
2469 let counter = handle.ndarray_params.array_counter;
2470 wait_until("sink to process both arrays", || {
2471 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 2)
2472 });
2473 assert_eq!(handle.port_name(), "SINK1");
2474 }
2475
2476 #[test]
2477 fn test_plugin_type_param() {
2478 let pool = Arc::new(NDArrayPool::new(1_000_000));
2479
2480 let (handle, _data_jh) = create_plugin_runtime(
2481 "TYPE_TEST",
2482 PassthroughProcessor,
2483 pool,
2484 10,
2485 "",
2486 test_wiring(),
2487 );
2488
2489 assert_eq!(handle.port_name(), "TYPE_TEST");
2491 assert_eq!(handle.port_runtime().port_name(), "TYPE_TEST");
2492 }
2493
2494 #[test]
2495 fn test_ndtimestamp_param_is_the_standalone_double() {
2496 let pool = Arc::new(NDArrayPool::new(1_000_000));
2503 let (ds, _rx) = ndarray_channel("DS_TS", 10);
2504 let mut output = NDArrayOutput::new();
2505 output.add(ds);
2506 let (handle, _jh) = create_plugin_runtime_with_output(
2507 "TS_PARAM",
2508 PassthroughProcessor,
2509 pool,
2510 10,
2511 output,
2512 "",
2513 test_wiring(),
2514 );
2515 enable_callbacks(&handle);
2516 let port = handle.port_runtime().port_handle().clone();
2517
2518 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2519 arr.timestamp = crate::timestamp::EpicsTimestamp {
2520 sec: 1234,
2521 nsec: 5678,
2522 };
2523 arr.time_stamp = 100.5; send_array(handle.array_sender(), Arc::new(arr));
2525 std::thread::sleep(std::time::Duration::from_millis(50));
2526
2527 assert_eq!(
2528 port.read_float64_blocking(handle.ndarray_params.timestamp_rbv, 0)
2529 .unwrap(),
2530 100.5,
2531 "NDTimeStamp publishes pArray->timeStamp"
2532 );
2533 assert_eq!(
2534 port.read_int32_blocking(handle.ndarray_params.epics_ts_sec, 0)
2535 .unwrap(),
2536 1234
2537 );
2538 assert_eq!(
2539 port.read_int32_blocking(handle.ndarray_params.epics_ts_nsec, 0)
2540 .unwrap(),
2541 5678
2542 );
2543 }
2544
2545 #[test]
2546 fn test_shutdown_on_handle_drop() {
2547 let pool = Arc::new(NDArrayPool::new(1_000_000));
2548
2549 let (handle, data_jh) = create_plugin_runtime(
2550 "SHUTDOWN_TEST",
2551 PassthroughProcessor,
2552 pool,
2553 10,
2554 "",
2555 test_wiring(),
2556 );
2557
2558 let sender = handle.array_sender().clone();
2560 drop(handle);
2561 drop(sender);
2562
2563 let result = data_jh.join();
2565 assert!(result.is_ok());
2566 }
2567
2568 #[test]
2569 fn test_wire_to_nonzero_ndarray_addr() {
2570 use crate::plugin::wiring::upstream_key;
2576 let pool = Arc::new(NDArrayPool::new(1_000_000));
2577 let wiring = test_wiring();
2578
2579 let (up_handle, _up_jh) = create_plugin_runtime_multi_addr(
2581 "UP_MULTI",
2582 PassthroughProcessor,
2583 pool,
2584 10,
2585 "",
2586 wiring.clone(),
2587 2,
2588 );
2589 enable_callbacks(&up_handle);
2590
2591 let addr0 = wiring.lookup_output("UP_MULTI");
2593 let addr1 = wiring.lookup_output(&upstream_key("UP_MULTI", 1));
2594 assert!(addr0.is_some(), "addr 0 output must be registered");
2595 assert!(
2596 addr1.is_some(),
2597 "addr 1 output must be registered for a max_addr=2 port"
2598 );
2599
2600 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWN_ADDR1", 10);
2602 wiring
2603 .rewire(&downstream_sender, "", &upstream_key("UP_MULTI", 1))
2604 .expect("wiring a consumer to NDArrayAddr=1 must succeed");
2605
2606 send_array(up_handle.array_sender(), make_test_array(99));
2608 let received = downstream_rx.blocking_recv().unwrap();
2609 assert_eq!(
2610 received.unique_id, 99,
2611 "consumer wired to NDArrayAddr=1 must receive upstream arrays"
2612 );
2613 }
2614
2615 #[test]
2616 fn test_nonblocking_passthrough() {
2617 let pool = Arc::new(NDArrayPool::new(1_000_000));
2618 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2619 let mut output = NDArrayOutput::new();
2620 output.add(downstream_sender);
2621
2622 let (handle, _data_jh) = create_plugin_runtime_with_output(
2623 "NB_TEST",
2624 PassthroughProcessor,
2625 pool,
2626 10,
2627 output,
2628 "",
2629 test_wiring(),
2630 );
2631 enable_callbacks(&handle);
2632
2633 send_array(handle.array_sender(), make_test_array(42));
2634
2635 let received = downstream_rx.blocking_recv().unwrap();
2636 assert_eq!(received.unique_id, 42);
2637 }
2638
2639 #[test]
2640 fn test_blocking_to_nonblocking_switch() {
2641 let pool = Arc::new(NDArrayPool::new(1_000_000));
2642 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2643 let mut output = NDArrayOutput::new();
2644 output.add(downstream_sender);
2645
2646 let (handle, _data_jh) = create_plugin_runtime_with_output(
2647 "SWITCH_TEST",
2648 PassthroughProcessor,
2649 pool,
2650 10,
2651 output,
2652 "",
2653 test_wiring(),
2654 );
2655 enable_callbacks(&handle);
2656
2657 handle
2659 .port_runtime()
2660 .port_handle()
2661 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 1)
2662 .unwrap();
2663 params_applied(&handle);
2664
2665 send_array(handle.array_sender(), make_test_array(1));
2666 let received = downstream_rx.blocking_recv().unwrap();
2667 assert_eq!(received.unique_id, 1);
2668
2669 handle
2671 .port_runtime()
2672 .port_handle()
2673 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 0)
2674 .unwrap();
2675 params_applied(&handle);
2676
2677 send_array(handle.array_sender(), make_test_array(2));
2679 let received = downstream_rx.blocking_recv().unwrap();
2680 assert_eq!(received.unique_id, 2);
2681 }
2682
2683 #[test]
2684 fn test_enable_callbacks_disables_processing() {
2685 let pool = Arc::new(NDArrayPool::new(1_000_000));
2686 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2687 let mut output = NDArrayOutput::new();
2688 output.add(downstream_sender);
2689
2690 let (handle, _data_jh) = create_plugin_runtime_with_output(
2691 "ENABLE_TEST",
2692 PassthroughProcessor,
2693 pool,
2694 10,
2695 output,
2696 "",
2697 test_wiring(),
2698 );
2699
2700 handle
2702 .port_runtime()
2703 .port_handle()
2704 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 0)
2705 .unwrap();
2706 params_applied(&handle);
2707
2708 send_array(handle.array_sender(), make_test_array(99));
2710
2711 let rt = tokio::runtime::Builder::new_current_thread()
2713 .enable_all()
2714 .build()
2715 .unwrap();
2716 let result = rt.block_on(async {
2717 tokio::time::timeout(std::time::Duration::from_millis(100), downstream_rx.recv()).await
2718 });
2719 assert!(
2720 result.is_err(),
2721 "should not receive array when callbacks disabled"
2722 );
2723 }
2724
2725 #[test]
2726 fn test_downstream_receives_multiple() {
2727 let pool = Arc::new(NDArrayPool::new(1_000_000));
2728
2729 let (ds1, mut rx1) = ndarray_channel("DS1", 10);
2730 let (ds2, mut rx2) = ndarray_channel("DS2", 10);
2731 let mut output = NDArrayOutput::new();
2732 output.add(ds1);
2733 output.add(ds2);
2734
2735 let (handle, _data_jh) = create_plugin_runtime_with_output(
2736 "DS_TEST",
2737 PassthroughProcessor,
2738 pool,
2739 10,
2740 output,
2741 "",
2742 test_wiring(),
2743 );
2744 enable_callbacks(&handle);
2745
2746 send_array(handle.array_sender(), make_test_array(77));
2747
2748 let r1 = rx1.blocking_recv().unwrap();
2750 let r2 = rx2.blocking_recv().unwrap();
2751 assert_eq!(r1.unique_id, 77);
2752 assert_eq!(r2.unique_id, 77);
2753 }
2754
2755 #[test]
2756 fn test_param_updates_after_send() {
2757 let pool = Arc::new(NDArrayPool::new(1_000_000));
2758
2759 struct ParamTracker;
2760 impl NDPluginProcess for ParamTracker {
2761 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2762 ProcessResult::arrays(vec![Arc::new(array.clone())])
2763 }
2764 fn plugin_type(&self) -> &str {
2765 "ParamTracker"
2766 }
2767 }
2768
2769 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2770 let mut output = NDArrayOutput::new();
2771 output.add(downstream_sender);
2772
2773 let (handle, _data_jh) = create_plugin_runtime_with_output(
2774 "PARAM_TEST",
2775 ParamTracker,
2776 pool,
2777 10,
2778 output,
2779 "",
2780 test_wiring(),
2781 );
2782 enable_callbacks(&handle);
2783
2784 send_array(handle.array_sender(), make_test_array(1));
2786 let received = downstream_rx.blocking_recv().unwrap();
2787 assert_eq!(received.unique_id, 1);
2788
2789 handle
2791 .port_runtime()
2792 .port_handle()
2793 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2794 .unwrap();
2795 params_applied(&handle);
2796
2797 send_array(handle.array_sender(), make_test_array(2));
2799 let received = downstream_rx.blocking_recv().unwrap();
2800 assert_eq!(received.unique_id, 2);
2801 }
2802
2803 #[test]
2804 fn test_sort_buffer_reorders_by_unique_id() {
2805 let mut buf = SortBuffer::new();
2806
2807 buf.insert(3, vec![make_test_array(3)], 10);
2809 buf.insert(1, vec![make_test_array(1)], 10);
2810 buf.insert(2, vec![make_test_array(2)], 10);
2811
2812 assert_eq!(buf.len(), 3);
2813
2814 let drained = buf.drain_all();
2815 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2816 assert_eq!(ids, vec![1, 2, 3], "should drain in sorted uniqueId order");
2817 assert_eq!(buf.len(), 0);
2818 assert_eq!(buf.prev_unique_id, 3);
2819 }
2820
2821 #[test]
2822 fn test_sort_buffer_drain_ready_contiguous() {
2823 let mut buf = SortBuffer::new();
2826 buf.note_emitted(0);
2829 buf.insert(1, vec![make_test_array(1)], 10);
2830 buf.insert(2, vec![make_test_array(2)], 10);
2831 buf.insert(5, vec![make_test_array(5)], 10); let drained = buf.drain_ready(100.0);
2835 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2836 assert_eq!(ids, vec![1, 2], "contiguous run released; id=5 held by gap");
2837 assert_eq!(buf.len(), 1);
2838 }
2839
2840 #[test]
2841 fn test_sort_buffer_drain_ready_deadline() {
2842 let mut buf = SortBuffer::new();
2844 buf.note_emitted(1); buf.insert(5, vec![make_test_array(5)], 10); std::thread::sleep(std::time::Duration::from_millis(30));
2847 let drained = buf.drain_ready(0.01);
2849 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2850 assert_eq!(ids, vec![5], "stale head released via deadline");
2851 }
2852
2853 #[test]
2854 fn test_sort_buffer_detects_disordered_on_emit() {
2855 let mut buf = SortBuffer::new();
2857 buf.note_emitted(5); buf.note_emitted(3); assert_eq!(buf.disordered_arrays, 1);
2860 buf.note_emitted(4); assert_eq!(buf.disordered_arrays, 1);
2862 }
2863
2864 #[test]
2865 fn test_sort_buffer_drops_when_full() {
2866 let mut buf = SortBuffer::new();
2867
2868 assert!(buf.insert(1, vec![make_test_array(1)], 2));
2870 assert!(buf.insert(2, vec![make_test_array(2)], 2));
2871 assert!(!buf.insert(3, vec![make_test_array(3)], 2));
2872
2873 assert_eq!(buf.len(), 2);
2874 assert_eq!(buf.dropped_output_arrays, 1);
2875 }
2876
2877 #[test]
2878 fn test_sort_mode_runtime_integration() {
2879 let pool = Arc::new(NDArrayPool::new(1_000_000));
2880 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2881 let mut output = NDArrayOutput::new();
2882 output.add(downstream_sender);
2883
2884 let (handle, _data_jh) = create_plugin_runtime_with_output(
2885 "SORT_TEST",
2886 PassthroughProcessor,
2887 pool,
2888 10,
2889 output,
2890 "",
2891 test_wiring(),
2892 );
2893 enable_callbacks(&handle);
2894
2895 handle
2897 .port_runtime()
2898 .port_handle()
2899 .write_int32_blocking(handle.plugin_params.sort_size, 0, 10)
2900 .unwrap();
2901 handle
2902 .port_runtime()
2903 .port_handle()
2904 .write_float64_blocking(handle.plugin_params.sort_time, 0, 0.1)
2905 .unwrap();
2906 handle
2907 .port_runtime()
2908 .port_handle()
2909 .write_int32_blocking(handle.plugin_params.sort_mode, 0, 1)
2910 .unwrap();
2911 params_applied(&handle);
2912
2913 send_array(handle.array_sender(), make_test_array(1));
2916 send_array(handle.array_sender(), make_test_array(2));
2917 send_array(handle.array_sender(), make_test_array(3));
2918
2919 let rt = tokio::runtime::Builder::new_current_thread()
2920 .enable_all()
2921 .build()
2922 .unwrap();
2923 let fast = rt.block_on(async {
2924 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
2925 });
2926 assert!(
2927 fast.is_ok(),
2928 "in-order arrays must be emitted immediately, not buffered"
2929 );
2930 assert_eq!(fast.unwrap().unwrap().unique_id, 1);
2931 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
2932 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 3);
2933
2934 send_array(handle.array_sender(), make_test_array(5));
2938 send_array(handle.array_sender(), make_test_array(4));
2939 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 4);
2942 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 5);
2943 }
2944
2945 #[test]
2946 fn test_throttle_drops_output_arrays() {
2947 let pool = Arc::new(NDArrayPool::new(1_000_000));
2950 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2951 let mut output = NDArrayOutput::new();
2952 output.add(downstream_sender);
2953
2954 let (handle, _data_jh) = create_plugin_runtime_with_output(
2955 "THROTTLE_TEST",
2956 PassthroughProcessor,
2957 pool,
2958 10,
2959 output,
2960 "",
2961 test_wiring(),
2962 );
2963 enable_callbacks(&handle);
2964
2965 handle
2968 .port_runtime()
2969 .port_handle()
2970 .write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 8.0)
2971 .unwrap();
2972 params_applied(&handle);
2973
2974 for id in 1..=5 {
2975 send_array(handle.array_sender(), make_test_array(id));
2976 }
2977 let port = handle.port_runtime().port_handle().clone();
2982 let counter = handle.ndarray_params.array_counter;
2983 wait_until("all 5 frames to be processed", || {
2984 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 5)
2985 });
2986
2987 let rt = tokio::runtime::Builder::new_current_thread()
2989 .enable_all()
2990 .build()
2991 .unwrap();
2992 let mut received = 0;
2993 while rt
2994 .block_on(async {
2995 tokio::time::timeout(std::time::Duration::from_millis(20), downstream_rx.recv())
2996 .await
2997 })
2998 .map(|o| o.is_some())
2999 .unwrap_or(false)
3000 {
3001 received += 1;
3002 }
3003 assert!(
3004 received < 5,
3005 "throttle must drop some arrays (got {received})"
3006 );
3007 assert!(received >= 1, "first array within budget must pass");
3008 }
3009
3010 #[test]
3011 fn test_process_plugin_reprocesses_last_input() {
3012 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 "PROCESS_PLUGIN_TEST",
3020 PassthroughProcessor,
3021 pool,
3022 10,
3023 output,
3024 "",
3025 test_wiring(),
3026 );
3027 enable_callbacks(&handle);
3028
3029 send_array(handle.array_sender(), make_test_array(7));
3030 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 7);
3031
3032 handle
3034 .port_runtime()
3035 .port_handle()
3036 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3037 .unwrap();
3038 let reprocessed = downstream_rx.blocking_recv().unwrap();
3039 assert_eq!(
3040 reprocessed.unique_id, 7,
3041 "ProcessPlugin re-emits last input"
3042 );
3043 }
3044
3045 #[test]
3046 fn test_min_callback_time_throttle_not_counted() {
3047 let pool = Arc::new(NDArrayPool::new(1_000_000));
3055 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3056 let mut output = NDArrayOutput::new();
3057 output.add(downstream_sender);
3058
3059 let (handle, _data_jh) = create_plugin_runtime_with_output(
3060 "MIN_CB_TEST",
3061 PassthroughProcessor,
3062 pool,
3063 10,
3064 output,
3065 "",
3066 test_wiring(),
3067 );
3068 enable_callbacks(&handle);
3069 let dropped = handle.array_sender().dropped_arrays_counter().clone();
3070
3071 handle
3073 .port_runtime()
3074 .port_handle()
3075 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3076 .unwrap();
3077 params_applied(&handle);
3078
3079 send_array(handle.array_sender(), make_test_array(1));
3080 send_array(handle.array_sender(), make_test_array(2));
3081
3082 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3083 params_applied(&handle);
3087 let rt = tokio::runtime::Builder::new_current_thread()
3088 .enable_all()
3089 .build()
3090 .unwrap();
3091 let second = rt.block_on(async {
3092 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3093 });
3094 assert!(
3095 second.is_err(),
3096 "second array throttled out by MinCallbackTime"
3097 );
3098 assert_eq!(
3099 dropped.load(Ordering::Acquire),
3100 0,
3101 "a MinCallbackTime-throttled frame must NOT increment DroppedArrays"
3102 );
3103 }
3104
3105 #[test]
3106 fn test_array_callbacks_zero_withholds_downstream_delivery() {
3107 let pool = Arc::new(NDArrayPool::new(1_000_000));
3113 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3114 let mut output = NDArrayOutput::new();
3115 output.add(downstream_sender);
3116
3117 let (handle, _data_jh) = create_plugin_runtime_with_output(
3118 "ARRAY_CB_TEST",
3119 PassthroughProcessor,
3120 pool,
3121 10,
3122 output,
3123 "",
3124 test_wiring(),
3125 );
3126 enable_callbacks(&handle);
3127 let port = handle.port_runtime().port_handle().clone();
3128
3129 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
3131 .unwrap();
3132 params_applied(&handle);
3133
3134 send_array(handle.array_sender(), make_test_array(1));
3135 wait_until("frame 1 to be processed", || {
3138 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3139 .is_ok_and(|v| v == 1)
3140 });
3141
3142 let rt = tokio::runtime::Builder::new_current_thread()
3144 .enable_all()
3145 .build()
3146 .unwrap();
3147 let got = rt.block_on(async {
3148 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3149 });
3150 assert!(
3151 got.is_err(),
3152 "NDArrayCallbacks=0 must withhold downstream delivery"
3153 );
3154 assert_eq!(
3156 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3157 .unwrap(),
3158 1,
3159 "processing (and metadata params) must continue while delivery is off"
3160 );
3161
3162 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 1)
3164 .unwrap();
3165 params_applied(&handle);
3166 send_array(handle.array_sender(), make_test_array(2));
3167 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3168 }
3169
3170 #[test]
3171 fn test_plugin_output_publishes_compressed_size() {
3172 struct CompressProcessor;
3178 impl NDPluginProcess for CompressProcessor {
3179 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3180 let mut out = array.clone();
3181 out.codec = Some(crate::codec::Codec {
3182 name: crate::codec::CodecName::JPEG,
3183 compressed_size: 7,
3184 level: 0,
3185 shuffle: 0,
3186 compressor: 0,
3187 original_data_type: NDDataType::UInt8,
3188 });
3189 ProcessResult::arrays(vec![Arc::new(out)])
3190 }
3191 fn plugin_type(&self) -> &str {
3192 "Compress"
3193 }
3194 }
3195
3196 {
3198 let pool = Arc::new(NDArrayPool::new(1_000_000));
3199 let (ds, _rx) = ndarray_channel("DS_RAW", 10);
3200 let mut output = NDArrayOutput::new();
3201 output.add(ds);
3202 let (handle, _jh) = create_plugin_runtime_with_output(
3203 "CODEC_RAW",
3204 PassthroughProcessor,
3205 pool,
3206 10,
3207 output,
3208 "",
3209 test_wiring(),
3210 );
3211 enable_callbacks(&handle);
3212 let port = handle.port_runtime().port_handle().clone();
3213 send_array(handle.array_sender(), make_test_array(1));
3214 wait_until(
3217 "uncompressed output to publish CompressedSize == raw byte count",
3218 || {
3219 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3220 .is_ok_and(|v| v == 4)
3221 },
3222 );
3223 }
3224
3225 {
3227 let pool = Arc::new(NDArrayPool::new(1_000_000));
3228 let (ds, _rx) = ndarray_channel("DS_CMP", 10);
3229 let mut output = NDArrayOutput::new();
3230 output.add(ds);
3231 let (handle, _jh) = create_plugin_runtime_with_output(
3232 "CODEC_CMP",
3233 CompressProcessor,
3234 pool,
3235 10,
3236 output,
3237 "",
3238 test_wiring(),
3239 );
3240 enable_callbacks(&handle);
3241 let port = handle.port_runtime().port_handle().clone();
3242 send_array(handle.array_sender(), make_test_array(1));
3243 wait_until(
3244 "compressed output to publish CompressedSize == codec.compressed_size",
3245 || {
3246 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3247 .is_ok_and(|v| v == 7)
3248 },
3249 );
3250 }
3251 }
3252
3253 #[test]
3254 fn test_process_plugin_skips_throttled_input() {
3255 let pool = Arc::new(NDArrayPool::new(1_000_000));
3260 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3261 let mut output = NDArrayOutput::new();
3262 output.add(downstream_sender);
3263
3264 let (handle, _data_jh) = create_plugin_runtime_with_output(
3265 "PROCESS_THROTTLE_TEST",
3266 PassthroughProcessor,
3267 pool,
3268 10,
3269 output,
3270 "",
3271 test_wiring(),
3272 );
3273 enable_callbacks(&handle);
3274
3275 handle
3277 .port_runtime()
3278 .port_handle()
3279 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3280 .unwrap();
3281 params_applied(&handle);
3282
3283 send_array(handle.array_sender(), make_test_array(1));
3284 send_array(handle.array_sender(), make_test_array(2));
3285
3286 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3288 params_applied(&handle);
3292
3293 handle
3298 .port_runtime()
3299 .port_handle()
3300 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 0.0)
3301 .unwrap();
3302 handle
3306 .port_runtime()
3307 .port_handle()
3308 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3309 .unwrap();
3310 let reprocessed = downstream_rx.blocking_recv().unwrap();
3311 assert_eq!(
3312 reprocessed.unique_id, 1,
3313 "ProcessPlugin must re-inject the last processed array (1), not the throttled array (2)"
3314 );
3315 }
3316
3317 #[test]
3318 fn test_g3_compressed_array_dropped_on_non_aware_plugin() {
3319 let pool = Arc::new(NDArrayPool::new(1_000_000));
3321 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3322 let mut output = NDArrayOutput::new();
3323 output.add(downstream_sender);
3324
3325 let (handle, _data_jh) = create_plugin_runtime_with_output(
3326 "G3_TEST",
3327 PassthroughProcessor, pool,
3329 10,
3330 output,
3331 "",
3332 test_wiring(),
3333 );
3334 enable_callbacks(&handle);
3335
3336 let mut compressed = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
3338 compressed.unique_id = 1;
3339 compressed.codec = Some(crate::codec::Codec {
3340 name: crate::codec::CodecName::JPEG,
3341 compressed_size: 16,
3342 level: 0,
3343 shuffle: 0,
3344 compressor: 0,
3345 original_data_type: NDDataType::UInt8,
3346 });
3347 send_array(handle.array_sender(), Arc::new(compressed));
3348
3349 send_array(handle.array_sender(), make_test_array(2));
3351
3352 let r = downstream_rx.blocking_recv().unwrap();
3353 assert_eq!(
3354 r.unique_id, 2,
3355 "compressed array dropped; only the raw array reaches downstream"
3356 );
3357 }
3358
3359 #[test]
3360 fn test_drop_on_full_increments_dropped_counter() {
3361 struct SlowProcessor;
3365 impl NDPluginProcess for SlowProcessor {
3366 fn process_array(&mut self, _a: &NDArray, _p: &NDArrayPool) -> ProcessResult {
3367 std::thread::sleep(std::time::Duration::from_millis(200));
3368 ProcessResult::empty()
3369 }
3370 fn plugin_type(&self) -> &str {
3371 "Slow"
3372 }
3373 }
3374 let pool = Arc::new(NDArrayPool::new(1_000_000));
3375
3376 let (downstream_handle, _ds_jh) =
3378 create_plugin_runtime("B1_DOWNSTREAM", SlowProcessor, pool, 1, "", test_wiring());
3379 enable_callbacks(&downstream_handle);
3380 let ds_sender = downstream_handle.array_sender().clone();
3381 let dropped = ds_sender.dropped_arrays_counter().clone();
3382
3383 send_array(&ds_sender, make_test_array(1));
3386 send_array(&ds_sender, make_test_array(2));
3387 send_array(&ds_sender, make_test_array(3));
3388 send_array(&ds_sender, make_test_array(4));
3389
3390 assert!(
3391 dropped.load(Ordering::Acquire) >= 1,
3392 "arrays dropped on a full queue must be counted (got {})",
3393 dropped.load(Ordering::Acquire)
3394 );
3395 }
3396
3397 #[test]
3398 fn test_cross_width_narrowing_array_read_truncates() {
3399 let mut out = [0i8; 1];
3408 let n = copy_ccast(&[300u16], &mut out);
3409 assert_eq!(n, 1);
3410 assert_eq!(out[0], 44, "(epicsInt8)(epicsUInt16)300 == 44 (low 8 bits)");
3411 let mut sat = [0i8; 1];
3413 copy_convert(&[300u16], &mut sat);
3414 assert_eq!(sat[0], 127, "f64 round-trip saturates — the wrong behavior");
3415
3416 let mut out2 = [0i8; 1];
3418 copy_ccast(&[0x1234_5678i32], &mut out2);
3419 assert_eq!(out2[0], 0x78);
3420
3421 let mut out3 = [0i8; 1];
3423 copy_ccast(&[-1i32], &mut out3);
3424 assert_eq!(out3[0], -1);
3425
3426 let mut out4 = [0i8; 1];
3428 copy_ccast(&[255u16], &mut out4);
3429 assert_eq!(out4[0], -1);
3430
3431 let mut out5 = [0i32; 1];
3433 copy_ccast(&[0x0000_0001_0000_002Ai64], &mut out5);
3434 assert_eq!(out5[0], 42);
3435
3436 let mut out6 = [0i16; 1];
3438 copy_ccast(&[70000u32], &mut out6);
3439 assert_eq!(out6[0], 4464);
3440
3441 let mut out7 = [0i8; 1];
3444 copy_ccast(&[255u8], &mut out7);
3445 assert_eq!(out7[0], -1);
3446
3447 let mut fout = [0i32; 1];
3453 copy_convert(&[42.9f64], &mut fout);
3454 assert_eq!(fout[0], 42, "f64 -> i32 truncates toward zero");
3455 }
3456
3457 fn block<F: std::future::Future>(f: F) -> F::Output {
3461 tokio::runtime::Builder::new_current_thread()
3462 .enable_all()
3463 .build()
3464 .unwrap()
3465 .block_on(f)
3466 }
3467
3468 #[test]
3469 fn test_scatter_reroutes_past_full_consumer() {
3470 let (sa, mut ra) = ndarray_channel("A", 1);
3475 let (sb, mut rb) = ndarray_channel("B", 1);
3476 let (sc, _rc) = ndarray_channel("C", 1);
3477 block(async {
3478 assert_eq!(
3479 sa.publish(make_test_array(99)).await,
3480 PublishOutcome::Delivered
3481 );
3482 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3483 let mut cursor = 0usize;
3484 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3485 assert_eq!(cursor, 2);
3487 assert_eq!(rb.recv().await.unwrap().unique_id, 1);
3488 assert_eq!(ra.recv().await.unwrap().unique_id, 99);
3490 assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3491 });
3492 }
3493
3494 #[test]
3495 fn test_scatter_drops_on_last_when_all_full_counts_once() {
3496 let (sa, mut ra) = ndarray_channel("A", 1);
3500 let (sb, mut rb) = ndarray_channel("B", 1);
3501 block(async {
3502 sa.publish(make_test_array(91)).await;
3503 sb.publish(make_test_array(92)).await;
3504 let senders = vec![sa.clone(), sb.clone()];
3505 let mut cursor = 0usize;
3506 ProcessOutput::scatter_publish(&make_test_array(7), &senders, &mut cursor).await;
3507 assert_eq!(cursor, 2); assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3510 assert_eq!(sb.dropped_arrays_counter().load(Ordering::Acquire), 1);
3511 assert_eq!(ra.recv().await.unwrap().unique_id, 91);
3513 assert_eq!(rb.recv().await.unwrap().unique_id, 92);
3514 });
3515 }
3516
3517 #[test]
3518 fn test_scatter_cursor_advances_per_attempt_across_frames() {
3519 let (sa, _ra) = ndarray_channel("A", 1);
3524 let (sb, mut rb) = ndarray_channel("B", 10);
3525 let (sc, mut rc) = ndarray_channel("C", 10);
3526 block(async {
3527 sa.publish(make_test_array(90)).await; let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3529 let mut cursor = 0usize;
3530 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3531 assert_eq!(cursor, 2); assert_eq!(rb.recv().await.unwrap().unique_id, 0);
3533 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3534 assert_eq!(cursor, 3); assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3536 });
3537 }
3538
3539 #[test]
3540 fn test_scatter_skips_disabled_consumer() {
3541 let (sa, mut ra) = ndarray_channel("A", 10);
3544 let (mut sb, _rb) = ndarray_channel("B", 10);
3545 let (sc, mut rc) = ndarray_channel("C", 10);
3546 sb.set_mode_flags(
3547 Arc::new(AtomicBool::new(false)),
3548 Arc::new(AtomicBool::new(false)),
3549 );
3550 block(async {
3551 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3552 let mut cursor = 0usize;
3553 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3555 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3556 assert_eq!(ra.recv().await.unwrap().unique_id, 0);
3557 assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3558 });
3559 }
3560}