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, port_runtime_unavailable};
25use asyn_rs::user::AsynUser;
26use epics_libcom_rs::runtime::task::{MandatoryThread, StackSizeClass, ThreadPriority};
27
28use asyn_rs::port_handle::PortHandle;
29
30use crate::ndarray::NDArray;
31use crate::ndarray_pool::NDArrayPool;
32use crate::params::ndarray_driver::NDArrayDriverParams;
33use asyn_rs::param::ParamValue;
34
35use super::channel::{
36 NDArrayOutput, NDArrayReceiver, NDArraySender, PublishOutcome, ndarray_channel,
37};
38use super::params::PluginBaseParams;
39use super::wiring::{WiringRegistry, upstream_key};
40
41#[derive(Debug)]
48enum PluginParamMsg {
49 Change(usize, i32, ParamChangeValue),
51 Barrier(std::sync::mpsc::SyncSender<()>),
58}
59
60#[derive(Debug, Clone)]
62pub enum ParamChangeValue {
63 Int32(i32),
64 Float64(f64),
65 Octet(String),
66}
67
68impl ParamChangeValue {
69 pub fn as_i32(&self) -> i32 {
70 match self {
71 ParamChangeValue::Int32(v) => *v,
72 ParamChangeValue::Float64(v) => *v as i32,
73 ParamChangeValue::Octet(_) => 0,
74 }
75 }
76
77 pub fn as_f64(&self) -> f64 {
78 match self {
79 ParamChangeValue::Int32(v) => *v as f64,
80 ParamChangeValue::Float64(v) => *v,
81 ParamChangeValue::Octet(_) => 0.0,
82 }
83 }
84
85 pub fn as_string(&self) -> Option<&str> {
86 match self {
87 ParamChangeValue::Octet(s) => Some(s),
88 _ => None,
89 }
90 }
91}
92
93pub enum ParamUpdate {
95 Int32 {
96 reason: usize,
97 addr: i32,
98 value: i32,
99 },
100 Float64 {
101 reason: usize,
102 addr: i32,
103 value: f64,
104 },
105 Octet {
106 reason: usize,
107 addr: i32,
108 value: String,
109 },
110 Float64Array {
111 reason: usize,
112 addr: i32,
113 value: Vec<f64>,
114 },
115}
116
117impl ParamUpdate {
118 pub fn int32(reason: usize, value: i32) -> Self {
120 Self::Int32 {
121 reason,
122 addr: 0,
123 value,
124 }
125 }
126 pub fn float64(reason: usize, value: f64) -> Self {
128 Self::Float64 {
129 reason,
130 addr: 0,
131 value,
132 }
133 }
134 pub fn int32_addr(reason: usize, addr: i32, value: i32) -> Self {
136 Self::Int32 {
137 reason,
138 addr,
139 value,
140 }
141 }
142 pub fn float64_addr(reason: usize, addr: i32, value: f64) -> Self {
144 Self::Float64 {
145 reason,
146 addr,
147 value,
148 }
149 }
150 pub fn float64_array(reason: usize, value: Vec<f64>) -> Self {
152 Self::Float64Array {
153 reason,
154 addr: 0,
155 value,
156 }
157 }
158 pub fn float64_array_addr(reason: usize, addr: i32, value: Vec<f64>) -> Self {
160 Self::Float64Array {
161 reason,
162 addr,
163 value,
164 }
165 }
166 pub fn octet(reason: usize, value: String) -> Self {
168 Self::Octet {
169 reason,
170 addr: 0,
171 value,
172 }
173 }
174 pub fn octet_addr(reason: usize, addr: i32, value: String) -> Self {
176 Self::Octet {
177 reason,
178 addr,
179 value,
180 }
181 }
182}
183
184pub struct ProcessResult {
186 pub output_arrays: Vec<Arc<NDArray>>,
187 pub param_updates: Vec<ParamUpdate>,
188 pub scatter: bool,
195}
196
197impl ProcessResult {
198 pub fn sink(param_updates: Vec<ParamUpdate>) -> Self {
200 Self {
201 output_arrays: vec![],
202 param_updates,
203 scatter: false,
204 }
205 }
206
207 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
209 Self {
210 output_arrays,
211 param_updates: vec![],
212 scatter: false,
213 }
214 }
215
216 pub fn empty() -> Self {
218 Self {
219 output_arrays: vec![],
220 param_updates: vec![],
221 scatter: false,
222 }
223 }
224
225 pub fn scatter(output_arrays: Vec<Arc<NDArray>>) -> Self {
228 Self {
229 output_arrays,
230 param_updates: vec![],
231 scatter: true,
232 }
233 }
234}
235
236pub struct ParamChangeResult {
238 pub output_arrays: Vec<Arc<NDArray>>,
239 pub param_updates: Vec<ParamUpdate>,
240}
241
242impl ParamChangeResult {
243 pub fn updates(param_updates: Vec<ParamUpdate>) -> Self {
244 Self {
245 output_arrays: vec![],
246 param_updates,
247 }
248 }
249
250 pub fn arrays(output_arrays: Vec<Arc<NDArray>>) -> Self {
251 Self {
252 output_arrays,
253 param_updates: vec![],
254 }
255 }
256
257 pub fn combined(output_arrays: Vec<Arc<NDArray>>, param_updates: Vec<ParamUpdate>) -> Self {
258 Self {
259 output_arrays,
260 param_updates,
261 }
262 }
263
264 pub fn empty() -> Self {
265 Self {
266 output_arrays: vec![],
267 param_updates: vec![],
268 }
269 }
270}
271
272pub trait NDPluginProcess: Send + 'static {
274 fn process_array(&mut self, array: &NDArray, pool: &NDArrayPool) -> ProcessResult;
276
277 fn plugin_type(&self) -> &str;
279
280 fn compression_aware(&self) -> bool {
286 false
287 }
288
289 fn does_array_callbacks(&self) -> bool {
296 true
297 }
298
299 fn register_params(
301 &mut self,
302 _base: &mut PortDriverBase,
303 ) -> Result<(), asyn_rs::error::AsynError> {
304 Ok(())
305 }
306
307 fn on_param_change(
310 &mut self,
311 _reason: usize,
312 _params: &PluginParamSnapshot,
313 ) -> ParamChangeResult {
314 ParamChangeResult::empty()
315 }
316
317 fn array_data_handle(&self) -> Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>> {
321 None
322 }
323}
324
325pub struct PluginParamSnapshot {
327 pub enable_callbacks: bool,
328 pub reason: usize,
330 pub addr: i32,
332 pub value: ParamChangeValue,
334}
335
336struct SortEntry {
340 arrays: Vec<Arc<NDArray>>,
341 inserted: std::time::Instant,
342}
343
344struct SortBuffer {
352 entries: BTreeMap<i32, SortEntry>,
354 prev_unique_id: i32,
356 first_output: bool,
358 disordered_arrays: i32,
360 dropped_output_arrays: i32,
363}
364
365impl SortBuffer {
366 fn new() -> Self {
367 Self {
368 entries: BTreeMap::new(),
369 prev_unique_id: 0,
370 first_output: true,
371 disordered_arrays: 0,
372 dropped_output_arrays: 0,
373 }
374 }
375
376 fn order_ok(&self, unique_id: i32) -> bool {
378 unique_id == self.prev_unique_id || unique_id == self.prev_unique_id + 1
379 }
380
381 fn note_emitted(&mut self, unique_id: i32) {
384 if !self.first_output && !self.order_ok(unique_id) {
385 self.disordered_arrays += 1;
386 }
387 self.first_output = false;
388 self.prev_unique_id = unique_id;
389 }
390
391 fn insert(&mut self, unique_id: i32, arrays: Vec<Arc<NDArray>>, sort_size: i32) -> bool {
396 if sort_size > 0 && self.entries.len() as i32 >= sort_size {
397 self.dropped_output_arrays += 1;
398 return false;
399 }
400 self.entries
401 .entry(unique_id)
402 .or_insert_with(|| SortEntry {
403 arrays: Vec::new(),
404 inserted: std::time::Instant::now(),
405 })
406 .arrays
407 .extend(arrays);
408 true
409 }
410
411 fn drain_ready(&mut self, sort_time: f64) -> Vec<(i32, Vec<Arc<NDArray>>)> {
415 let now = std::time::Instant::now();
416 let mut out = Vec::new();
417 while let Some((&head_id, entry)) = self.entries.iter().next() {
418 let delta = now.duration_since(entry.inserted).as_secs_f64();
419 let order_ok = self.order_ok(head_id);
420 if (!self.first_output && order_ok) || delta > sort_time {
421 let entry = self.entries.remove(&head_id).unwrap();
422 self.note_emitted(head_id);
423 out.push((head_id, entry.arrays));
424 } else {
425 break;
426 }
427 }
428 out
429 }
430
431 fn drain_all(&mut self) -> Vec<(i32, Vec<Arc<NDArray>>)> {
434 let entries = std::mem::take(&mut self.entries);
435 let mut out = Vec::with_capacity(entries.len());
436 for (id, entry) in entries {
437 self.note_emitted(id);
438 out.push((id, entry.arrays));
439 }
440 out
441 }
442
443 fn len(&self) -> i32 {
445 self.entries.len() as i32
446 }
447}
448
449struct SharedProcessorInner<P: NDPluginProcess> {
452 processor: P,
453 output: Arc<parking_lot::Mutex<NDArrayOutput>>,
454 pool: Arc<NDArrayPool>,
455 ndarray_params: NDArrayDriverParams,
456 plugin_params: PluginBaseParams,
457 port_handle: PortHandle,
458 array_counter: i32,
462 std_array_data_param: Option<usize>,
464 array_callbacks: bool,
471 min_callback_time: f64,
473 last_process_time: Option<std::time::Instant>,
475 sort_mode: i32,
477 sort_time: f64,
479 sort_size: i32,
481 sort_buffer: SortBuffer,
483 dropped_arrays: Arc<std::sync::atomic::AtomicI32>,
487 compression_aware: bool,
490 max_byte_rate: f64,
492 throttler: super::throttler::Throttler,
494 prev_input_array: Option<Arc<NDArray>>,
497 dims_prev: Vec<i32>,
500 nd_array_addr: i32,
502 max_threads: i32,
504 num_threads: i32,
506}
507
508impl<P: NDPluginProcess> SharedProcessorInner<P> {
509 fn should_throttle(&self) -> bool {
510 if self.min_callback_time <= 0.0 {
511 return false;
512 }
513 if let Some(last) = self.last_process_time {
514 last.elapsed().as_secs_f64() < self.min_callback_time
515 } else {
516 false
517 }
518 }
519
520 fn array_byte_cost(array: &NDArray) -> f64 {
523 match &array.codec {
524 Some(c) => c.compressed_size as f64,
525 None => array.info().total_bytes as f64,
526 }
527 }
528
529 fn throttle_ok(&mut self, array: &NDArray) -> bool {
532 if self.max_byte_rate == 0.0 {
533 return true;
534 }
535 let cost = Self::array_byte_cost(array);
536 if self.throttler.try_take(cost) {
537 true
538 } else {
539 self.sort_buffer.dropped_output_arrays += 1;
540 false
541 }
542 }
543
544 fn route_output_arrays(&mut self, arrays: Vec<Arc<NDArray>>) -> Vec<Arc<NDArray>> {
552 let mut ready = Vec::new();
553 for arr in arrays {
554 if !self.throttle_ok(&arr) {
555 continue; }
557 let uid = arr.unique_id;
558 if self.sort_mode != 0
559 && !self.sort_buffer.first_output
560 && !self.sort_buffer.order_ok(uid)
561 {
562 self.sort_buffer.insert(uid, vec![arr], self.sort_size);
564 } else {
565 self.sort_buffer.note_emitted(uid);
567 ready.push(arr);
568 }
569 }
570 if self.sort_mode != 0 {
573 for (_id, mut bucket) in self.sort_buffer.drain_ready(self.sort_time) {
574 ready.append(&mut bucket);
575 }
576 }
577 ready
578 }
579
580 fn process_and_publish(&mut self, array: &Arc<NDArray>) -> Option<ProcessOutput> {
584 if self.should_throttle() {
591 return None;
592 }
593 self.prev_input_array = Some(Arc::clone(array));
598 let t0 = std::time::Instant::now();
599 let result = self.processor.process_array(array, &self.pool);
600 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
601 self.last_process_time = Some(t0);
602
603 let produced = result.output_arrays.len();
617 let ready = if self.array_callbacks || self.std_array_data_param.is_some() {
618 self.route_output_arrays(result.output_arrays)
619 } else {
620 Vec::new()
621 };
622 let count_frame =
627 !(self.std_array_data_param.is_some() && produced > 0 && ready.is_empty());
628 let mut output = self.build_publish_batch(
629 ready,
630 result.param_updates,
631 result.scatter,
632 Some(array.as_ref()),
633 elapsed_ms,
634 self.array_callbacks,
635 count_frame,
636 );
637 output.batch.merge(self.build_status_params_batch());
638 Some(output)
639 }
640
641 fn dropped_arrays_only_batch(&self) -> ProcessOutput {
644 ProcessOutput {
645 arrays: vec![],
646 scatter: false,
647 batch: self.build_status_params_batch(),
648 }
649 }
650
651 fn process_plugin(&mut self) -> Option<ProcessOutput> {
654 let prev = self.prev_input_array.clone()?;
655 self.process_and_publish(&prev)
656 }
657
658 fn tick_sort_buffer(&mut self) -> ProcessOutput {
661 let entries = self.sort_buffer.drain_ready(self.sort_time);
662 self.emit_drained(entries)
663 }
664
665 fn flush_sort_buffer(&mut self) -> ProcessOutput {
667 let entries = self.sort_buffer.drain_all();
668 self.emit_drained(entries)
669 }
670
671 fn emit_drained(&mut self, entries: Vec<(i32, Vec<Arc<NDArray>>)>) -> ProcessOutput {
672 let mut all_arrays = Vec::new();
673 let mut combined = ParamBatch::empty();
674 for (_unique_id, arrays) in entries {
675 let output = self.build_publish_batch(arrays, vec![], false, None, 0.0, true, true);
680 all_arrays.extend(output.arrays);
681 combined.merge(output.batch);
682 }
683 combined.merge(self.build_sort_params_batch());
684 ProcessOutput {
685 arrays: all_arrays,
686 scatter: false,
687 batch: combined,
688 }
689 }
690
691 fn build_sort_params_batch(&self) -> ParamBatch {
692 use asyn_rs::request::ParamSetValue;
693 let sort_free = self.sort_size - self.sort_buffer.len();
694 ParamBatch {
695 addr0: vec![
696 ParamSetValue::new(
697 self.plugin_params.sort_free,
698 0,
699 ParamValue::Int32(sort_free),
700 ),
701 ParamSetValue::new(
702 self.plugin_params.disordered_arrays,
703 0,
704 ParamValue::Int32(self.sort_buffer.disordered_arrays),
705 ),
706 ParamSetValue::new(
707 self.plugin_params.dropped_output_arrays,
708 0,
709 ParamValue::Int32(self.sort_buffer.dropped_output_arrays),
710 ),
711 ],
712 extra: std::collections::HashMap::new(),
713 }
714 }
715
716 fn build_status_params_batch(&self) -> ParamBatch {
719 use asyn_rs::request::ParamSetValue;
720 let mut batch = self.build_sort_params_batch();
721 batch.addr0.push(ParamSetValue::new(
722 self.plugin_params.dropped_arrays,
723 0,
724 ParamValue::Int32(
725 self.dropped_arrays
726 .load(std::sync::atomic::Ordering::Acquire),
727 ),
728 ));
729 batch
730 }
731
732 fn build_publish_batch(
742 &mut self,
743 output_arrays: Vec<Arc<NDArray>>,
744 param_updates: Vec<ParamUpdate>,
745 scatter: bool,
746 fallback_array: Option<&NDArray>,
747 elapsed_ms: f64,
748 deliver: bool,
749 count_frame: bool,
750 ) -> ProcessOutput {
751 use asyn_rs::request::ParamSetValue;
752
753 let mut addr0: Vec<ParamSetValue> = Vec::new();
754 let mut extra: std::collections::HashMap<i32, Vec<ParamSetValue>> =
755 std::collections::HashMap::new();
756
757 if let Some(report_arr) = output_arrays.first().map(|a| a.as_ref()).or(fallback_array) {
758 if count_frame {
763 self.array_counter += 1;
764 }
765
766 if let (Some(param), Some(served)) = (
777 self.std_array_data_param,
778 output_arrays.first().map(|a| a.as_ref()),
779 ) {
780 use crate::ndarray::NDDataBuffer;
781 use asyn_rs::param::ParamValue;
782 let value = match &served.data {
783 NDDataBuffer::I8(v) => {
784 Some(ParamValue::Int8Array(std::sync::Arc::from(v.as_slice())))
785 }
786 NDDataBuffer::U8(v) => Some(ParamValue::Int8Array(std::sync::Arc::from(
787 v.iter().map(|&x| x as i8).collect::<Vec<_>>().as_slice(),
788 ))),
789 NDDataBuffer::I16(v) => {
790 Some(ParamValue::Int16Array(std::sync::Arc::from(v.as_slice())))
791 }
792 NDDataBuffer::U16(v) => Some(ParamValue::Int16Array(std::sync::Arc::from(
793 v.iter().map(|&x| x as i16).collect::<Vec<_>>().as_slice(),
794 ))),
795 NDDataBuffer::I32(v) => {
796 Some(ParamValue::Int32Array(std::sync::Arc::from(v.as_slice())))
797 }
798 NDDataBuffer::U32(v) => Some(ParamValue::Int32Array(std::sync::Arc::from(
799 v.iter().map(|&x| x as i32).collect::<Vec<_>>().as_slice(),
800 ))),
801 NDDataBuffer::I64(v) => {
802 Some(ParamValue::Int64Array(std::sync::Arc::from(v.as_slice())))
803 }
804 NDDataBuffer::U64(v) => Some(ParamValue::Int64Array(std::sync::Arc::from(
805 v.iter().map(|&x| x as i64).collect::<Vec<_>>().as_slice(),
806 ))),
807 NDDataBuffer::F32(v) => {
808 Some(ParamValue::Float32Array(std::sync::Arc::from(v.as_slice())))
809 }
810 NDDataBuffer::F64(v) => {
811 Some(ParamValue::Float64Array(std::sync::Arc::from(v.as_slice())))
812 }
813 };
814 if let Some(value) = value {
815 let ts = served.timestamp.to_system_time();
816 self.port_handle
817 .interrupts()
818 .notify(asyn_rs::interrupt::InterruptValue {
819 reason: param,
820 addr: 0,
821 value,
822 timestamp: ts,
823 uint32_changed_mask: 0,
824 ..Default::default()
825 });
826 }
827 }
828
829 let info = report_arr.info();
830 let color_mode = report_arr
834 .attributes
835 .get("ColorMode")
836 .and_then(|a| a.value.as_i64())
837 .map(|v| v as i32)
838 .unwrap_or(info.color_mode as i32);
839 let bayer_pattern = report_arr
840 .attributes
841 .get("BayerPattern")
842 .and_then(|a| a.value.as_i64())
843 .map(|v| v as i32)
844 .unwrap_or(0);
845
846 let mut cur_dims = vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS];
853 for (slot, d) in cur_dims.iter_mut().zip(
854 report_arr
855 .dims
856 .iter()
857 .take(crate::ndarray::ND_ARRAY_MAX_DIMS),
858 ) {
859 *slot = d.size as i32;
860 }
861 if cur_dims != self.dims_prev {
862 self.dims_prev = cur_dims.clone();
863 self.port_handle
864 .interrupts()
865 .notify(asyn_rs::interrupt::InterruptValue {
866 reason: self.ndarray_params.array_dimensions,
867 addr: 0,
868 value: asyn_rs::param::ParamValue::Int32Array(std::sync::Arc::from(
869 cur_dims.as_slice(),
870 )),
871 timestamp: report_arr.timestamp.to_system_time(),
872 uint32_changed_mask: 0,
873 ..Default::default()
874 });
875 }
876
877 addr0.extend([
878 ParamSetValue::new(
879 self.ndarray_params.array_counter,
880 0,
881 ParamValue::Int32(self.array_counter),
882 ),
883 ParamSetValue::new(
884 self.ndarray_params.unique_id,
885 0,
886 ParamValue::Int32(report_arr.unique_id),
887 ),
888 ParamSetValue::new(
889 self.ndarray_params.n_dimensions,
890 0,
891 ParamValue::Int32(report_arr.dims.len() as i32),
892 ),
893 ParamSetValue::new(
894 self.ndarray_params.array_size_x,
895 0,
896 ParamValue::Int32(info.x_size as i32),
897 ),
898 ParamSetValue::new(
899 self.ndarray_params.array_size_y,
900 0,
901 ParamValue::Int32(info.y_size as i32),
902 ),
903 ParamSetValue::new(
904 self.ndarray_params.array_size_z,
905 0,
906 ParamValue::Int32(info.color_size as i32),
907 ),
908 ParamSetValue::new(
909 self.ndarray_params.array_size,
910 0,
911 ParamValue::Int32(info.total_bytes as i32),
912 ),
913 ParamSetValue::new(
914 self.ndarray_params.data_type,
915 0,
916 ParamValue::Int32(report_arr.data.data_type() as i32),
917 ),
918 ParamSetValue::new(
919 self.ndarray_params.color_mode,
920 0,
921 ParamValue::Int32(color_mode),
922 ),
923 ParamSetValue::new(
924 self.ndarray_params.bayer_pattern,
925 0,
926 ParamValue::Int32(bayer_pattern),
927 ),
928 ParamSetValue::new(
929 self.ndarray_params.timestamp_rbv,
930 0,
931 ParamValue::Float64(report_arr.time_stamp),
936 ),
937 ParamSetValue::new(
938 self.ndarray_params.epics_ts_sec,
939 0,
940 ParamValue::Int32(report_arr.timestamp.sec as i32),
941 ),
942 ParamSetValue::new(
943 self.ndarray_params.epics_ts_nsec,
944 0,
945 ParamValue::Int32(report_arr.timestamp.nsec as i32),
946 ),
947 ]);
948
949 match &report_arr.codec {
955 Some(codec) => {
956 addr0.push(ParamSetValue::new(
957 self.ndarray_params.codec,
958 0,
959 ParamValue::Octet(codec.name.as_str().to_string()),
960 ));
961 addr0.push(ParamSetValue::new(
962 self.ndarray_params.compressed_size,
963 0,
964 ParamValue::Int32(codec.compressed_size as i32),
965 ));
966 }
967 None => {
968 addr0.push(ParamSetValue::new(
969 self.ndarray_params.codec,
970 0,
971 ParamValue::Octet(String::new()),
972 ));
973 addr0.push(ParamSetValue::new(
974 self.ndarray_params.compressed_size,
975 0,
976 ParamValue::Int32(info.total_bytes as i32),
977 ));
978 }
979 }
980 }
981
982 addr0.push(ParamSetValue::new(
983 self.plugin_params.execution_time,
984 0,
985 ParamValue::Float64(elapsed_ms),
986 ));
987
988 for update in ¶m_updates {
993 match update {
994 ParamUpdate::Int32 {
995 reason,
996 addr,
997 value,
998 } => {
999 let pv = ParamSetValue::new(*reason, *addr, ParamValue::Int32(*value));
1000 if *addr == 0 {
1001 addr0.push(pv);
1002 } else {
1003 extra.entry(*addr).or_default().push(pv);
1004 }
1005 }
1006 ParamUpdate::Float64 {
1007 reason,
1008 addr,
1009 value,
1010 } => {
1011 let pv = ParamSetValue::new(*reason, *addr, ParamValue::Float64(*value));
1012 if *addr == 0 {
1013 addr0.push(pv);
1014 } else {
1015 extra.entry(*addr).or_default().push(pv);
1016 }
1017 }
1018 ParamUpdate::Octet {
1019 reason,
1020 addr,
1021 value,
1022 } => {
1023 let pv = ParamSetValue::new(*reason, *addr, ParamValue::Octet(value.clone()));
1024 if *addr == 0 {
1025 addr0.push(pv);
1026 } else {
1027 extra.entry(*addr).or_default().push(pv);
1028 }
1029 }
1030 ParamUpdate::Float64Array {
1031 reason,
1032 addr,
1033 value,
1034 } => {
1035 let pv = ParamSetValue::new(
1036 *reason,
1037 *addr,
1038 ParamValue::Float64Array(value.clone().into()),
1039 );
1040 if *addr == 0 {
1041 addr0.push(pv);
1042 } else {
1043 extra.entry(*addr).or_default().push(pv);
1044 }
1045 }
1046 }
1047 }
1048
1049 ProcessOutput {
1050 arrays: if deliver { output_arrays } else { Vec::new() },
1054 scatter,
1055 batch: ParamBatch { addr0, extra },
1056 }
1057 }
1058}
1059
1060struct ProcessOutput {
1062 arrays: Vec<Arc<NDArray>>,
1063 scatter: bool,
1064 batch: ParamBatch,
1065}
1066
1067impl ProcessOutput {
1068 async fn publish_arrays(&self, senders: &[NDArraySender], scatter_cursor: &mut usize) {
1076 for arr in &self.arrays {
1077 if self.scatter {
1078 Self::scatter_publish(arr, senders, scatter_cursor).await;
1079 } else {
1080 let futs = senders.iter().map(|s| s.publish(arr.clone()));
1081 futures_util::future::join_all(futs).await;
1082 }
1083 }
1084 }
1085
1086 async fn scatter_publish(arr: &Arc<NDArray>, senders: &[NDArraySender], cursor: &mut usize) {
1106 let active: Vec<&NDArraySender> = senders.iter().filter(|s| s.is_enabled()).collect();
1107 let n = active.len();
1108 if n == 0 {
1109 return;
1110 }
1111 for attempt in 0..n {
1112 let target = *cursor % n;
1113 *cursor = cursor.wrapping_add(1);
1114 let is_last = attempt == n - 1;
1115 match active[target].publish_scatter(arr.clone(), is_last).await {
1116 PublishOutcome::Delivered => break,
1120 PublishOutcome::DroppedQueueFull
1124 | PublishOutcome::Disabled
1125 | PublishOutcome::ChannelClosed => {
1126 if is_last {
1127 break;
1128 }
1129 }
1130 }
1131 }
1132 }
1133}
1134
1135struct ParamBatch {
1138 addr0: Vec<asyn_rs::request::ParamSetValue>,
1139 extra: std::collections::HashMap<i32, Vec<asyn_rs::request::ParamSetValue>>,
1140}
1141
1142impl ParamBatch {
1143 fn empty() -> Self {
1144 Self {
1145 addr0: Vec::new(),
1146 extra: std::collections::HashMap::new(),
1147 }
1148 }
1149
1150 fn merge(&mut self, other: ParamBatch) {
1151 self.addr0.extend(other.addr0);
1152 for (addr, updates) in other.extra {
1153 self.extra.entry(addr).or_default().extend(updates);
1154 }
1155 }
1156
1157 async fn flush(self, port: &asyn_rs::port_handle::PortHandle) {
1159 if !self.addr0.is_empty() {
1160 if let Err(e) = port.set_params_and_notify(0, self.addr0).await {
1161 eprintln!("plugin param flush error (addr 0): {e}");
1162 }
1163 }
1164 for (addr, updates) in self.extra {
1165 if let Err(e) = port.set_params_and_notify(addr, updates).await {
1166 eprintln!("plugin param flush error (addr {addr}): {e}");
1167 }
1168 }
1169 }
1170}
1171
1172#[allow(dead_code)]
1174pub struct PluginPortDriver {
1175 base: PortDriverBase,
1176 ndarray_params: NDArrayDriverParams,
1177 plugin_params: PluginBaseParams,
1178 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1179 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1181 std_array_data_param: Option<usize>,
1183}
1184
1185impl PluginPortDriver {
1186 fn new<P: NDPluginProcess>(
1187 port_name: &str,
1188 plugin_type_name: &str,
1189 queue_size: usize,
1190 ndarray_port: &str,
1191 max_addr: usize,
1192 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1193 processor: &mut P,
1194 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1195 ) -> AsynResult<Self> {
1196 let mut base = PortDriverBase::new(
1197 port_name,
1198 max_addr,
1199 PortFlags {
1200 can_block: true,
1201 ..Default::default()
1202 },
1203 );
1204
1205 let ndarray_params = NDArrayDriverParams::create(&mut base)?;
1206 let plugin_params = PluginBaseParams::create(&mut base)?;
1207
1208 base.set_int32_param(plugin_params.enable_callbacks, 0, 0)?;
1210 base.set_int32_param(plugin_params.blocking_callbacks, 0, 0)?;
1211 base.set_int32_param(plugin_params.queue_size, 0, queue_size as i32)?;
1212 base.set_int32_param(plugin_params.dropped_arrays, 0, 0)?;
1213 base.set_int32_param(plugin_params.queue_use, 0, 0)?;
1214 base.set_string_param(plugin_params.plugin_type, 0, plugin_type_name.into())?;
1215 base.set_int32_param(
1216 ndarray_params.array_callbacks,
1217 0,
1218 processor.does_array_callbacks() as i32,
1219 )?;
1220 base.set_int32_param(ndarray_params.write_file, 0, 0)?;
1221 base.set_int32_param(ndarray_params.read_file, 0, 0)?;
1222 base.set_int32_param(ndarray_params.capture, 0, 0)?;
1223 base.set_int32_param(ndarray_params.file_write_status, 0, 0)?;
1224 base.set_string_param(ndarray_params.file_write_message, 0, "".into())?;
1225 base.set_string_param(ndarray_params.file_path, 0, "".into())?;
1226 base.set_string_param(ndarray_params.file_name, 0, "".into())?;
1227 base.set_int32_param(ndarray_params.file_number, 0, 0)?;
1228 base.set_int32_param(ndarray_params.auto_increment, 0, 0)?;
1229 base.set_string_param(ndarray_params.file_template, 0, "%s%s_%3.3d.dat".into())?;
1230 base.set_string_param(ndarray_params.full_file_name, 0, "".into())?;
1231 base.set_int32_param(ndarray_params.create_dir, 0, 0)?;
1232 base.set_string_param(ndarray_params.temp_suffix, 0, "".into())?;
1233
1234 base.set_string_param(ndarray_params.port_name_self, 0, port_name.into())?;
1236 base.set_string_param(
1237 ndarray_params.ad_core_version,
1238 0,
1239 env!("CARGO_PKG_VERSION").into(),
1240 )?;
1241 base.set_string_param(
1242 ndarray_params.driver_version,
1243 0,
1244 env!("CARGO_PKG_VERSION").into(),
1245 )?;
1246 if !ndarray_port.is_empty() {
1247 base.set_string_param(plugin_params.nd_array_port, 0, ndarray_port.into())?;
1248 }
1249
1250 let std_array_data_param = if array_data.is_some() {
1252 Some(base.create_param("STD_ARRAY_DATA", asyn_rs::param::ParamType::GenericPointer)?)
1253 } else {
1254 None
1255 };
1256
1257 processor.register_params(&mut base)?;
1259
1260 Ok(Self {
1261 base,
1262 ndarray_params,
1263 plugin_params,
1264 param_change_tx,
1265 array_data,
1266 std_array_data_param,
1267 })
1268 }
1269}
1270
1271fn copy_direct<T: Copy>(src: &[T], dst: &mut [T]) -> usize {
1273 let n = src.len().min(dst.len());
1274 dst[..n].copy_from_slice(&src[..n]);
1275 n
1276}
1277
1278fn copy_convert<S, D>(src: &[S], dst: &mut [D]) -> usize
1280where
1281 S: CastToF64 + Copy,
1282 D: CastFromF64 + Copy,
1283{
1284 let n = src.len().min(dst.len());
1285 for i in 0..n {
1286 dst[i] = D::cast_from_f64(src[i].cast_to_f64());
1287 }
1288 n
1289}
1290
1291trait CCastTo<D> {
1307 fn ccast(self) -> D;
1308}
1309macro_rules! impl_ccast {
1310 ( $src:ty => $( $dst:ty ),+ ) => {
1311 $(
1312 impl CCastTo<$dst> for $src {
1313 #[inline]
1314 fn ccast(self) -> $dst {
1315 self as $dst
1316 }
1317 }
1318 )+
1319 };
1320}
1321impl_ccast!(i8 => i16, i32, i64);
1322impl_ccast!(u8 => i8, i16, i32, i64);
1323impl_ccast!(i16 => i8, i32, i64);
1324impl_ccast!(u16 => i8, i16, i32, i64);
1325impl_ccast!(i32 => i8, i16, i64);
1326impl_ccast!(u32 => i8, i16, i32, i64);
1327impl_ccast!(i64 => i8, i16, i32);
1328impl_ccast!(u64 => i8, i16, i32, i64);
1329
1330fn copy_ccast<S, D>(src: &[S], dst: &mut [D]) -> usize
1334where
1335 S: CCastTo<D> + Copy,
1336 D: Copy,
1337{
1338 let n = src.len().min(dst.len());
1339 for i in 0..n {
1340 dst[i] = src[i].ccast();
1341 }
1342 n
1343}
1344
1345trait CastToF64 {
1347 fn cast_to_f64(self) -> f64;
1348}
1349
1350impl CastToF64 for i8 {
1351 fn cast_to_f64(self) -> f64 {
1352 self as f64
1353 }
1354}
1355impl CastToF64 for u8 {
1356 fn cast_to_f64(self) -> f64 {
1357 self as f64
1358 }
1359}
1360impl CastToF64 for i16 {
1361 fn cast_to_f64(self) -> f64 {
1362 self as f64
1363 }
1364}
1365impl CastToF64 for u16 {
1366 fn cast_to_f64(self) -> f64 {
1367 self as f64
1368 }
1369}
1370impl CastToF64 for i32 {
1371 fn cast_to_f64(self) -> f64 {
1372 self as f64
1373 }
1374}
1375impl CastToF64 for u32 {
1376 fn cast_to_f64(self) -> f64 {
1377 self as f64
1378 }
1379}
1380impl CastToF64 for i64 {
1381 fn cast_to_f64(self) -> f64 {
1382 self as f64
1383 }
1384}
1385impl CastToF64 for u64 {
1386 fn cast_to_f64(self) -> f64 {
1387 self as f64
1388 }
1389}
1390impl CastToF64 for f32 {
1391 fn cast_to_f64(self) -> f64 {
1392 self as f64
1393 }
1394}
1395impl CastToF64 for f64 {
1396 fn cast_to_f64(self) -> f64 {
1397 self
1398 }
1399}
1400
1401trait CastFromF64 {
1403 fn cast_from_f64(v: f64) -> Self;
1404}
1405
1406impl CastFromF64 for i8 {
1407 fn cast_from_f64(v: f64) -> Self {
1408 v as i8
1409 }
1410}
1411impl CastFromF64 for i16 {
1412 fn cast_from_f64(v: f64) -> Self {
1413 v as i16
1414 }
1415}
1416impl CastFromF64 for i32 {
1417 fn cast_from_f64(v: f64) -> Self {
1418 v as i32
1419 }
1420}
1421impl CastFromF64 for i64 {
1422 fn cast_from_f64(v: f64) -> Self {
1423 v as i64
1424 }
1425}
1426impl CastFromF64 for f32 {
1427 fn cast_from_f64(v: f64) -> Self {
1428 v as f32
1429 }
1430}
1431impl CastFromF64 for f64 {
1432 fn cast_from_f64(v: f64) -> Self {
1433 v
1434 }
1435}
1436
1437macro_rules! impl_read_array {
1440 (
1441 $self:expr, $buf:expr, $direct_variant:ident,
1442 ccast: [ $( $ccast_variant:ident ),* ],
1443 convert: [ $( $variant:ident ),* ]
1444 ) => {{
1445 use crate::ndarray::NDDataBuffer;
1446 let handle = match &$self.array_data {
1447 Some(h) => h,
1448 None => return Ok(0),
1449 };
1450 let guard = handle.lock();
1451 let array = match &*guard {
1452 Some(a) => a,
1453 None => return Ok(0),
1454 };
1455 let n = match &array.data {
1456 NDDataBuffer::$direct_variant(v) => copy_direct(v, $buf),
1457 $( NDDataBuffer::$ccast_variant(v) => copy_ccast(v, $buf), )*
1458 $( NDDataBuffer::$variant(v) => copy_convert(v, $buf), )*
1459 };
1460 Ok(n)
1461 }};
1462}
1463
1464impl PortDriver for PluginPortDriver {
1465 fn base(&self) -> &PortDriverBase {
1466 &self.base
1467 }
1468
1469 fn base_mut(&mut self) -> &mut PortDriverBase {
1470 &mut self.base
1471 }
1472
1473 fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1474 let reason = user.reason;
1475 let addr = user.addr;
1476 self.base.set_int32_param(reason, addr, value)?;
1477 self.base.call_param_callbacks(addr)?;
1478 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1480 reason,
1481 addr,
1482 ParamChangeValue::Int32(value),
1483 ));
1484 Ok(())
1485 }
1486
1487 fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1488 let reason = user.reason;
1489 let addr = user.addr;
1490 self.base.set_float64_param(reason, addr, value)?;
1491 self.base.call_param_callbacks(addr)?;
1492 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1493 reason,
1494 addr,
1495 ParamChangeValue::Float64(value),
1496 ));
1497 Ok(())
1498 }
1499
1500 fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1501 let reason = user.reason;
1502 let addr = user.addr;
1503 let s = String::from_utf8_lossy(data).into_owned();
1504 self.base.set_string_param(reason, addr, s.clone())?;
1505 self.base.call_param_callbacks(addr)?;
1506 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1507 reason,
1508 addr,
1509 ParamChangeValue::Octet(s),
1510 ));
1511 Ok(data.len())
1512 }
1513
1514 fn read_int8_array(&mut self, _user: &AsynUser, buf: &mut [i8]) -> AsynResult<usize> {
1515 impl_read_array!(
1518 self, buf, I8,
1519 ccast: [U8, I16, U16, I32, U32, I64, U64],
1520 convert: [F32, F64]
1521 )
1522 }
1523
1524 fn read_int16_array(&mut self, _user: &AsynUser, buf: &mut [i16]) -> AsynResult<usize> {
1525 impl_read_array!(
1526 self, buf, I16,
1527 ccast: [I8, U8, U16, I32, U32, I64, U64],
1528 convert: [F32, F64]
1529 )
1530 }
1531
1532 fn read_int32_array(&mut self, _user: &AsynUser, buf: &mut [i32]) -> AsynResult<usize> {
1533 impl_read_array!(
1534 self, buf, I32,
1535 ccast: [I8, U8, I16, U16, U32, I64, U64],
1536 convert: [F32, F64]
1537 )
1538 }
1539
1540 fn read_int64_array(&mut self, _user: &AsynUser, buf: &mut [i64]) -> AsynResult<usize> {
1541 impl_read_array!(
1542 self, buf, I64,
1543 ccast: [I8, U8, I16, U16, I32, U32, U64],
1544 convert: [F32, F64]
1545 )
1546 }
1547
1548 fn read_float32_array(&mut self, _user: &AsynUser, buf: &mut [f32]) -> AsynResult<usize> {
1549 impl_read_array!(
1550 self, buf, F32,
1551 ccast: [],
1552 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F64]
1553 )
1554 }
1555
1556 fn read_float64_array(&mut self, _user: &AsynUser, buf: &mut [f64]) -> AsynResult<usize> {
1557 impl_read_array!(
1558 self, buf, F64,
1559 ccast: [],
1560 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F32]
1561 )
1562 }
1563}
1564
1565#[derive(Clone)]
1567pub struct PluginRuntimeHandle {
1568 port_runtime: PortRuntimeHandle,
1569 array_sender: NDArraySender,
1570 array_output: Arc<parking_lot::Mutex<NDArrayOutput>>,
1571 port_name: String,
1572 param_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1573 pub ndarray_params: NDArrayDriverParams,
1574 pub plugin_params: PluginBaseParams,
1575}
1576
1577impl PluginRuntimeHandle {
1578 pub fn port_runtime(&self) -> &PortRuntimeHandle {
1579 &self.port_runtime
1580 }
1581
1582 pub fn array_sender(&self) -> &NDArraySender {
1583 &self.array_sender
1584 }
1585
1586 pub fn array_output(&self) -> &Arc<parking_lot::Mutex<NDArrayOutput>> {
1587 &self.array_output
1588 }
1589
1590 pub fn wait_params_applied(&self, timeout: std::time::Duration) -> bool {
1608 let (ack_tx, ack_rx) = std::sync::mpsc::sync_channel(1);
1609 if self.param_tx.send(PluginParamMsg::Barrier(ack_tx)).is_err() {
1610 return false;
1611 }
1612 ack_rx.recv_timeout(timeout).is_ok()
1613 }
1614
1615 pub fn port_name(&self) -> &str {
1616 &self.port_name
1617 }
1618}
1619
1620pub fn create_plugin_runtime<P: NDPluginProcess>(
1627 port_name: &str,
1628 processor: P,
1629 pool: Arc<NDArrayPool>,
1630 queue_size: usize,
1631 ndarray_port: &str,
1632 wiring: Arc<WiringRegistry>,
1633) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1634 create_plugin_runtime_multi_addr(
1635 port_name,
1636 processor,
1637 pool,
1638 queue_size,
1639 ndarray_port,
1640 wiring,
1641 1,
1642 )
1643}
1644
1645pub fn create_plugin_runtime_multi_addr<P: NDPluginProcess>(
1649 port_name: &str,
1650 mut processor: P,
1651 pool: Arc<NDArrayPool>,
1652 queue_size: usize,
1653 ndarray_port: &str,
1654 wiring: Arc<WiringRegistry>,
1655 max_addr: usize,
1656) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1657 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
1662 let handle_param_tx = param_tx.clone();
1663
1664 let plugin_type_name = processor.plugin_type().to_string();
1666 let compression_aware = processor.compression_aware();
1667 let does_array_callbacks = processor.does_array_callbacks();
1668 let array_data = processor.array_data_handle();
1669
1670 let driver = PluginPortDriver::new(
1672 port_name,
1673 &plugin_type_name,
1674 queue_size,
1675 ndarray_port,
1676 max_addr,
1677 param_tx,
1678 &mut processor,
1679 array_data,
1680 )
1681 .expect("failed to create plugin port driver");
1682
1683 let ndarray_params = driver.ndarray_params;
1684 let plugin_params = driver.plugin_params;
1685 let std_array_data_param = driver.std_array_data_param;
1686
1687 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default())
1698 .unwrap_or_else(|e| port_runtime_unavailable(port_name, &e));
1699
1700 let port_handle = port_runtime.port_handle().clone();
1702
1703 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
1705
1706 let enabled = Arc::new(AtomicBool::new(false));
1708 let blocking_mode = Arc::new(AtomicBool::new(false));
1709
1710 let array_output = Arc::new(parking_lot::Mutex::new(NDArrayOutput::new()));
1712 let array_output_for_handle = array_output.clone();
1713 wiring.register_output_addrs(port_name, max_addr, array_output.clone());
1719 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
1722 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
1723 processor,
1724 output: array_output,
1725 pool,
1726 ndarray_params,
1727 plugin_params,
1728 port_handle,
1729 array_counter: 0,
1730 std_array_data_param,
1731 array_callbacks: does_array_callbacks,
1734 min_callback_time: 0.0,
1735 last_process_time: None,
1736 sort_mode: 0,
1737 sort_time: 0.0,
1738 sort_size: 10,
1739 sort_buffer: SortBuffer::new(),
1740 dropped_arrays: dropped_arrays_counter,
1741 compression_aware,
1742 max_byte_rate: 0.0,
1743 throttler: super::throttler::Throttler::new(0.0),
1744 prev_input_array: None,
1745 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
1746 nd_array_addr: 0,
1747 max_threads: 1,
1748 num_threads: 1,
1749 }));
1750
1751 let data_enabled = enabled.clone();
1752 let data_blocking = blocking_mode.clone();
1753
1754 let mut array_sender = array_sender;
1755 array_sender.set_mode_flags(enabled, blocking_mode);
1756
1757 let sender_port_name = port_name.to_string();
1759 let initial_upstream = ndarray_port.to_string();
1760
1761 let data_jh = MandatoryThread::new(
1763 format!("plugin-data-{port_name}"),
1764 ThreadPriority::Medium,
1768 StackSizeClass::Medium,
1771 )
1772 .spawn(move || {
1773 plugin_data_loop(
1774 shared,
1775 array_rx,
1776 param_rx,
1777 plugin_params,
1778 ndarray_params.array_counter,
1779 data_enabled,
1780 data_blocking,
1781 sender_port_name,
1782 initial_upstream,
1783 wiring,
1784 );
1785 });
1786
1787 let handle = PluginRuntimeHandle {
1788 port_runtime,
1789 array_sender,
1790 array_output: array_output_for_handle,
1791 port_name: port_name.to_string(),
1792 param_tx: handle_param_tx,
1793 ndarray_params,
1794 plugin_params,
1795 };
1796
1797 (handle, data_jh)
1798}
1799
1800fn queue_status_batch(
1806 plugin_params: &PluginBaseParams,
1807 max_capacity: usize,
1808 free: i32,
1809) -> ParamBatch {
1810 use asyn_rs::request::ParamSetValue;
1811 ParamBatch {
1812 addr0: vec![
1813 ParamSetValue::new(
1814 plugin_params.queue_size,
1815 0,
1816 ParamValue::Int32(max_capacity as i32),
1817 ),
1818 ParamSetValue::new(plugin_params.queue_use, 0, ParamValue::Int32(free)),
1819 ],
1820 extra: std::collections::HashMap::new(),
1821 }
1822}
1823
1824async fn clamp_writeback(port: &PortHandle, reason: usize, value: i32) {
1827 use asyn_rs::request::ParamSetValue;
1828 let _ = port
1829 .set_params_and_notify(
1830 0,
1831 vec![ParamSetValue::new(
1832 reason,
1833 0,
1834 asyn_rs::param::ParamValue::Int32(value),
1835 )],
1836 )
1837 .await;
1838}
1839
1840fn plugin_data_loop<P: NDPluginProcess>(
1841 shared: Arc<parking_lot::Mutex<SharedProcessorInner<P>>>,
1842 mut array_rx: NDArrayReceiver,
1843 mut param_rx: tokio::sync::mpsc::UnboundedReceiver<PluginParamMsg>,
1844 plugin_params: PluginBaseParams,
1845 array_counter_reason: usize,
1846 enabled: Arc<AtomicBool>,
1847 blocking_mode: Arc<AtomicBool>,
1848 sender_port_name: String,
1849 initial_upstream: String,
1850 wiring: Arc<WiringRegistry>,
1851) {
1852 let enable_callbacks_reason = plugin_params.enable_callbacks;
1853 let blocking_callbacks_reason = plugin_params.blocking_callbacks;
1854 let min_callback_time_reason = plugin_params.min_callback_time;
1855 let sort_mode_reason = plugin_params.sort_mode;
1856 let sort_time_reason = plugin_params.sort_time;
1857 let sort_size_reason = plugin_params.sort_size;
1858 let nd_array_port_reason = plugin_params.nd_array_port;
1859 let nd_array_addr_reason = plugin_params.nd_array_addr;
1860 let process_plugin_reason = plugin_params.process_plugin;
1861 let max_byte_rate_reason = plugin_params.max_byte_rate;
1862 let num_threads_reason = plugin_params.num_threads;
1863 let max_threads_reason = plugin_params.max_threads;
1864 let array_callbacks_reason = shared.lock().ndarray_params.array_callbacks;
1865 let mut current_upstream = initial_upstream;
1869 let mut current_addr: i32 = 0;
1870 let rt = tokio::runtime::Builder::new_current_thread()
1871 .enable_all()
1872 .build()
1873 .unwrap();
1874 rt.block_on(async {
1875 let mut sort_flush_interval = tokio::time::interval(std::time::Duration::from_secs(3600));
1878 let mut sort_flush_active = false;
1879 let mut last_queue_free: Option<i32> = None;
1882 let mut scatter_cursor: usize = 0;
1887 let mut held_barriers: Vec<std::sync::mpsc::SyncSender<()>> = Vec::new();
1893
1894 loop {
1895 if !held_barriers.is_empty() && array_rx.pending() == 0 {
1899 for ack in held_barriers.drain(..) {
1900 let _ = ack.try_send(());
1901 }
1902 }
1903 tokio::select! {
1904 msg = array_rx.recv_msg() => {
1905 match msg {
1906 Some(msg) => {
1907 if !enabled.load(Ordering::Acquire) {
1912 continue;
1913 }
1914 let (process_output, senders, port) = {
1916 let mut guard = shared.lock();
1917 let compressed = msg.array.codec.is_some();
1921 let output = if compressed && !guard.compression_aware {
1922 guard
1923 .dropped_arrays
1924 .fetch_add(1, Ordering::AcqRel);
1925 Some(guard.dropped_arrays_only_batch())
1926 } else {
1927 guard.process_and_publish(&msg.array)
1932 };
1933 let senders = guard.output.lock().senders_clone();
1934 let port = guard.port_handle.clone();
1935 (output, senders, port)
1936 };
1937 let max_cap = array_rx.max_capacity();
1942 let free = max_cap.saturating_sub(array_rx.pending()) as i32;
1943 let queue_batch = if last_queue_free != Some(free) {
1944 last_queue_free = Some(free);
1945 Some(queue_status_batch(&plugin_params, max_cap, free))
1946 } else {
1947 None
1948 };
1949 if let Some(po) = process_output {
1952 po.publish_arrays(&senders, &mut scatter_cursor).await;
1953 po.batch.flush(&port).await;
1954 }
1955 if let Some(qb) = queue_batch {
1956 qb.flush(&port).await;
1957 }
1958 }
1959 None => break,
1960 }
1961 }
1962 param = param_rx.recv() => {
1963 match param {
1964 Some(PluginParamMsg::Barrier(ack)) => {
1970 held_barriers.push(ack);
1971 }
1972 Some(PluginParamMsg::Change(reason, addr, value)) => {
1973 if reason == enable_callbacks_reason {
1974 let on = value.as_i32() != 0;
1975 enabled.store(on, Ordering::Release);
1976 if !on {
1979 shared.lock().prev_input_array = None;
1980 }
1981 }
1982 if reason == blocking_callbacks_reason {
1983 blocking_mode.store(value.as_i32() != 0, Ordering::Release);
1984 }
1985 if reason == array_callbacks_reason {
1990 shared.lock().array_callbacks = value.as_i32() != 0;
1991 }
1992 if reason == min_callback_time_reason {
1994 shared.lock().min_callback_time = value.as_f64();
1995 }
1996 if reason == max_byte_rate_reason {
1999 let rate = value.as_f64();
2000 let mut guard = shared.lock();
2001 guard.max_byte_rate = rate;
2002 guard.throttler.reset(rate);
2003 }
2004 if reason == max_threads_reason {
2011 let (port, clamped, mt) = {
2013 let mut guard = shared.lock();
2014 guard.max_threads = value.as_i32().max(1);
2015 let clamped =
2016 guard.num_threads.clamp(1, guard.max_threads);
2017 guard.num_threads = clamped;
2018 (guard.port_handle.clone(), clamped, guard.max_threads)
2019 };
2020 clamp_writeback(&port, num_threads_reason, clamped).await;
2021 clamp_writeback(&port, max_threads_reason, mt).await;
2022 }
2023 if reason == num_threads_reason {
2024 let (port, clamped) = {
2025 let mut guard = shared.lock();
2026 let clamped =
2027 value.as_i32().clamp(1, guard.max_threads.max(1));
2028 guard.num_threads = clamped;
2029 (guard.port_handle.clone(), clamped)
2030 };
2031 clamp_writeback(&port, num_threads_reason, clamped).await;
2032 }
2033 if reason == nd_array_addr_reason {
2037 let new_addr = value.as_i32();
2038 if new_addr != current_addr {
2039 let old_key = upstream_key(¤t_upstream, current_addr);
2040 let new_key = upstream_key(¤t_upstream, new_addr);
2041 shared.lock().nd_array_addr = new_addr;
2042 match wiring.rewire_by_name(
2043 &sender_port_name,
2044 &old_key,
2045 &new_key,
2046 ) {
2047 Ok(()) => current_addr = new_addr,
2048 Err(e) => {
2049 eprintln!("NDArrayAddr reconnect failed: {e}");
2050 shared.lock().nd_array_addr = current_addr;
2051 }
2052 }
2053 }
2054 }
2055 if reason == process_plugin_reason && value.as_i32() != 0 {
2058 let (process_output, senders, port) = {
2059 let mut guard = shared.lock();
2060 let output = guard.process_plugin();
2061 let senders = guard.output.lock().senders_clone();
2062 let port = guard.port_handle.clone();
2063 (output, senders, port)
2064 };
2065 if let Some(po) = process_output {
2066 po.publish_arrays(&senders, &mut scatter_cursor).await;
2067 po.batch.flush(&port).await;
2068 } else {
2069 #[cfg(feature = "ioc")]
2083 if let Some(entry) =
2084 asyn_rs::asyn_record::get_port(&sender_port_name)
2085 {
2086 asyn_rs::asyn_trace!(
2087 entry.trace,
2088 sender_port_name.as_str(),
2089 asyn_rs::trace::TraceMask::WARNING,
2090 "plugin {sender_port_name}: ProcessPlugin \
2091 requested but no input array cached"
2092 );
2093 }
2094 }
2095 }
2096 if reason == array_counter_reason {
2100 shared.lock().array_counter = value.as_i32();
2101 }
2102 if reason == sort_mode_reason {
2104 let mode = value.as_i32();
2105 let flush_work = {
2108 let mut guard = shared.lock();
2109 guard.sort_mode = mode;
2110 if mode == 0 {
2111 let output = guard.flush_sort_buffer();
2112 let senders = guard.output.lock().senders_clone();
2113 let port = guard.port_handle.clone();
2114 sort_flush_active = false;
2115 Some((output, senders, port))
2116 } else {
2117 sort_flush_active = guard.sort_time > 0.0;
2118 if sort_flush_active {
2119 let dur = std::time::Duration::from_secs_f64(guard.sort_time);
2120 sort_flush_interval = tokio::time::interval(dur);
2121 }
2122 None
2123 }
2124 };
2125 if let Some((output, senders, port)) = flush_work {
2126 output.publish_arrays(&senders, &mut scatter_cursor).await;
2127 output.batch.flush(&port).await;
2128 }
2129 }
2130 if reason == sort_time_reason {
2131 let t = value.as_f64();
2132 let mut guard = shared.lock();
2133 guard.sort_time = t;
2134 if guard.sort_mode != 0 && t > 0.0 {
2135 sort_flush_active = true;
2136 let dur = std::time::Duration::from_secs_f64(t);
2137 sort_flush_interval = tokio::time::interval(dur);
2138 } else {
2139 sort_flush_active = false;
2140 }
2141 drop(guard);
2142 }
2143 if reason == sort_size_reason {
2144 shared.lock().sort_size = value.as_i32();
2145 }
2146 if reason == nd_array_port_reason {
2148 if let Some(new_port) = value.as_string() {
2149 if new_port != current_upstream {
2150 let old_key =
2151 upstream_key(¤t_upstream, current_addr);
2152 let new_key = upstream_key(new_port, current_addr);
2153 match wiring.rewire_by_name(
2154 &sender_port_name,
2155 &old_key,
2156 &new_key,
2157 ) {
2158 Ok(()) => current_upstream = new_port.to_string(),
2159 Err(e) => {
2160 eprintln!("NDArrayPort rewire failed: {e}")
2161 }
2162 }
2163 }
2164 }
2165 }
2166 let snapshot = PluginParamSnapshot {
2167 enable_callbacks: enabled.load(Ordering::Acquire),
2168 reason,
2169 addr,
2170 value,
2171 };
2172 let (process_output, senders, port) = {
2173 let mut guard = shared.lock();
2174 let t0 = std::time::Instant::now();
2175 let result = guard.processor.on_param_change(reason, &snapshot);
2176 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
2177 let output = if !result.output_arrays.is_empty() || !result.param_updates.is_empty() {
2178 let deliver = guard.array_callbacks;
2179 Some(guard.build_publish_batch(result.output_arrays, result.param_updates, false, None, elapsed_ms, deliver, true))
2180 } else {
2181 None
2182 };
2183 let senders = guard.output.lock().senders_clone();
2184 (output, senders, guard.port_handle.clone())
2185 };
2186 if let Some(po) = process_output {
2187 po.publish_arrays(&senders, &mut scatter_cursor).await;
2188 po.batch.flush(&port).await;
2189 }
2190 }
2191 None => break,
2192 }
2193 }
2194 _ = sort_flush_interval.tick(), if sort_flush_active => {
2195 let (output, senders, port) = {
2198 let mut guard = shared.lock();
2199 let output = guard.tick_sort_buffer();
2200 let senders = guard.output.lock().senders_clone();
2201 let port = guard.port_handle.clone();
2202 (output, senders, port)
2203 };
2204 output.publish_arrays(&senders, &mut scatter_cursor).await;
2205 output.batch.flush(&port).await;
2206 }
2207 }
2208 }
2209 });
2210}
2211
2212pub fn wire_downstream(upstream: &PluginRuntimeHandle, downstream_sender: NDArraySender) {
2219 upstream.array_output().lock().add(downstream_sender);
2220}
2221
2222pub fn create_plugin_runtime_with_output<P: NDPluginProcess>(
2224 port_name: &str,
2225 mut processor: P,
2226 pool: Arc<NDArrayPool>,
2227 queue_size: usize,
2228 output: NDArrayOutput,
2229 ndarray_port: &str,
2230 wiring: Arc<WiringRegistry>,
2231) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
2232 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
2236 let handle_param_tx = param_tx.clone();
2237
2238 let plugin_type_name = processor.plugin_type().to_string();
2239 let compression_aware = processor.compression_aware();
2240 let does_array_callbacks = processor.does_array_callbacks();
2241 let array_data = processor.array_data_handle();
2242 let driver = PluginPortDriver::new(
2243 port_name,
2244 &plugin_type_name,
2245 queue_size,
2246 ndarray_port,
2247 1,
2248 param_tx,
2249 &mut processor,
2250 array_data,
2251 )
2252 .expect("failed to create plugin port driver");
2253
2254 let ndarray_params = driver.ndarray_params;
2255 let plugin_params = driver.plugin_params;
2256 let std_array_data_param = driver.std_array_data_param;
2257
2258 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default())
2262 .unwrap_or_else(|e| port_runtime_unavailable(port_name, &e));
2263
2264 let port_handle = port_runtime.port_handle().clone();
2265
2266 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
2267
2268 let enabled = Arc::new(AtomicBool::new(false));
2269 let blocking_mode = Arc::new(AtomicBool::new(false));
2270
2271 let array_output = Arc::new(parking_lot::Mutex::new(output));
2272 let array_output_for_handle = array_output.clone();
2273 wiring.register_output(port_name, array_output.clone());
2277 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
2279 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
2280 processor,
2281 output: array_output,
2282 pool,
2283 ndarray_params,
2284 plugin_params,
2285 port_handle,
2286 array_counter: 0,
2287 std_array_data_param,
2288 array_callbacks: does_array_callbacks,
2291 min_callback_time: 0.0,
2292 last_process_time: None,
2293 sort_mode: 0,
2294 sort_time: 0.0,
2295 sort_size: 10,
2296 sort_buffer: SortBuffer::new(),
2297 dropped_arrays: dropped_arrays_counter,
2298 compression_aware,
2299 max_byte_rate: 0.0,
2300 throttler: super::throttler::Throttler::new(0.0),
2301 prev_input_array: None,
2302 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
2303 nd_array_addr: 0,
2304 max_threads: 1,
2305 num_threads: 1,
2306 }));
2307
2308 let data_enabled = enabled.clone();
2309 let data_blocking = blocking_mode.clone();
2310
2311 let mut array_sender = array_sender;
2312 array_sender.set_mode_flags(enabled, blocking_mode);
2313
2314 let sender_port_name = port_name.to_string();
2316 let initial_upstream = ndarray_port.to_string();
2317
2318 let data_jh = MandatoryThread::new(
2319 format!("plugin-data-{port_name}"),
2320 ThreadPriority::Medium,
2324 StackSizeClass::Medium,
2327 )
2328 .spawn(move || {
2329 plugin_data_loop(
2330 shared,
2331 array_rx,
2332 param_rx,
2333 plugin_params,
2334 ndarray_params.array_counter,
2335 data_enabled,
2336 data_blocking,
2337 sender_port_name,
2338 initial_upstream,
2339 wiring,
2340 );
2341 });
2342
2343 let handle = PluginRuntimeHandle {
2344 port_runtime,
2345 array_sender,
2346 array_output: array_output_for_handle,
2347 port_name: port_name.to_string(),
2348 param_tx: handle_param_tx,
2349 ndarray_params,
2350 plugin_params,
2351 };
2352
2353 (handle, data_jh)
2354}
2355
2356#[cfg(test)]
2357mod tests {
2358 use super::*;
2359 use crate::ndarray::{NDDataType, NDDimension};
2360 use crate::plugin::channel::ndarray_channel;
2361
2362 #[test]
2393 fn plugin_data_threads_are_mandatory() {
2394 let prod = match include_str!("runtime.rs").find("\n#[cfg(test)]") {
2395 Some(i) => &include_str!("runtime.rs")[..i],
2396 None => include_str!("runtime.rs"),
2397 };
2398 assert_eq!(
2399 prod.matches("MandatoryThread::new(").count(),
2400 2,
2401 "`create_plugin_runtime_multi_addr` and `create_plugin_runtime_with_output`"
2402 );
2403 let bare = concat!("thread", "::Builder::new()");
2404 let strays: Vec<&str> = prod
2405 .lines()
2406 .map(str::trim)
2407 .filter(|l| !l.starts_with("//"))
2408 .filter(|l| l.contains(bare) || l.contains(concat!("thread", "::spawn(")))
2409 .collect();
2410 assert!(
2411 strays.is_empty(),
2412 "a plugin data thread created outside `MandatoryThread` resolves its \
2413 own spawn failure locally: {strays:?}"
2414 );
2415 }
2416
2417 struct PassthroughProcessor;
2419
2420 impl NDPluginProcess for PassthroughProcessor {
2421 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2422 ProcessResult::arrays(vec![Arc::new(array.clone())])
2423 }
2424 fn plugin_type(&self) -> &str {
2425 "Passthrough"
2426 }
2427 }
2428
2429 struct SinkProcessor {
2431 count: usize,
2432 }
2433
2434 impl NDPluginProcess for SinkProcessor {
2435 fn process_array(&mut self, _array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2436 self.count += 1;
2437 ProcessResult::empty()
2438 }
2439 fn plugin_type(&self) -> &str {
2440 "Sink"
2441 }
2442 }
2443
2444 fn make_test_array(id: i32) -> Arc<NDArray> {
2445 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2446 arr.unique_id = id;
2447 Arc::new(arr)
2448 }
2449
2450 fn test_wiring() -> Arc<WiringRegistry> {
2451 Arc::new(WiringRegistry::new())
2452 }
2453
2454 fn params_applied(handle: &PluginRuntimeHandle) {
2459 assert!(
2460 handle.wait_params_applied(std::time::Duration::from_secs(10)),
2461 "data thread did not apply queued param changes"
2462 );
2463 }
2464
2465 fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
2469 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2470 while !cond() {
2471 assert!(
2472 std::time::Instant::now() < deadline,
2473 "timed out waiting for {what}"
2474 );
2475 std::thread::sleep(std::time::Duration::from_millis(2));
2476 }
2477 }
2478
2479 fn enable_callbacks(handle: &PluginRuntimeHandle) {
2482 handle
2483 .port_runtime()
2484 .port_handle()
2485 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2486 .unwrap();
2487 params_applied(handle);
2488 }
2489
2490 fn send_array(sender: &NDArraySender, array: Arc<NDArray>) {
2494 let sender = sender.clone();
2495 let jh = std::thread::spawn(move || {
2496 let rt = tokio::runtime::Builder::new_current_thread()
2497 .enable_all()
2498 .build()
2499 .unwrap();
2500 rt.block_on(sender.publish(array));
2501 });
2502 jh.join().unwrap();
2503 }
2504
2505 #[test]
2506 fn test_passthrough_runtime() {
2507 let pool = Arc::new(NDArrayPool::new(1_000_000));
2508
2509 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2511 let mut output = NDArrayOutput::new();
2512 output.add(downstream_sender);
2513
2514 let (handle, _data_jh) = create_plugin_runtime_with_output(
2515 "PASS1",
2516 PassthroughProcessor,
2517 pool,
2518 10,
2519 output,
2520 "",
2521 test_wiring(),
2522 );
2523 enable_callbacks(&handle);
2524
2525 send_array(handle.array_sender(), make_test_array(42));
2527
2528 let received = downstream_rx.blocking_recv().unwrap();
2530 assert_eq!(received.unique_id, 42);
2531 }
2532
2533 #[test]
2534 fn test_sink_runtime() {
2535 let pool = Arc::new(NDArrayPool::new(1_000_000));
2536
2537 let (handle, _data_jh) = create_plugin_runtime(
2538 "SINK1",
2539 SinkProcessor { count: 0 },
2540 pool,
2541 10,
2542 "",
2543 test_wiring(),
2544 );
2545 enable_callbacks(&handle);
2546
2547 send_array(handle.array_sender(), make_test_array(1));
2549 send_array(handle.array_sender(), make_test_array(2));
2550
2551 let port = handle.port_runtime().port_handle().clone();
2553 let counter = handle.ndarray_params.array_counter;
2554 wait_until("sink to process both arrays", || {
2555 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 2)
2556 });
2557 assert_eq!(handle.port_name(), "SINK1");
2558 }
2559
2560 #[test]
2561 fn test_plugin_type_param() {
2562 let pool = Arc::new(NDArrayPool::new(1_000_000));
2563
2564 let (handle, _data_jh) = create_plugin_runtime(
2565 "TYPE_TEST",
2566 PassthroughProcessor,
2567 pool,
2568 10,
2569 "",
2570 test_wiring(),
2571 );
2572
2573 assert_eq!(handle.port_name(), "TYPE_TEST");
2575 assert_eq!(handle.port_runtime().port_name(), "TYPE_TEST");
2576 }
2577
2578 #[test]
2579 fn test_ndtimestamp_param_is_the_standalone_double() {
2580 let pool = Arc::new(NDArrayPool::new(1_000_000));
2587 let (ds, _rx) = ndarray_channel("DS_TS", 10);
2588 let mut output = NDArrayOutput::new();
2589 output.add(ds);
2590 let (handle, _jh) = create_plugin_runtime_with_output(
2591 "TS_PARAM",
2592 PassthroughProcessor,
2593 pool,
2594 10,
2595 output,
2596 "",
2597 test_wiring(),
2598 );
2599 enable_callbacks(&handle);
2600 let port = handle.port_runtime().port_handle().clone();
2601
2602 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2603 arr.timestamp = crate::timestamp::EpicsTimestamp {
2604 sec: 1234,
2605 nsec: 5678,
2606 };
2607 arr.time_stamp = 100.5; send_array(handle.array_sender(), Arc::new(arr));
2609 std::thread::sleep(std::time::Duration::from_millis(50));
2610
2611 assert_eq!(
2612 port.read_float64_blocking(handle.ndarray_params.timestamp_rbv, 0)
2613 .unwrap(),
2614 100.5,
2615 "NDTimeStamp publishes pArray->timeStamp"
2616 );
2617 assert_eq!(
2618 port.read_int32_blocking(handle.ndarray_params.epics_ts_sec, 0)
2619 .unwrap(),
2620 1234
2621 );
2622 assert_eq!(
2623 port.read_int32_blocking(handle.ndarray_params.epics_ts_nsec, 0)
2624 .unwrap(),
2625 5678
2626 );
2627 }
2628
2629 #[test]
2630 fn test_shutdown_on_handle_drop() {
2631 let pool = Arc::new(NDArrayPool::new(1_000_000));
2632
2633 let (handle, data_jh) = create_plugin_runtime(
2634 "SHUTDOWN_TEST",
2635 PassthroughProcessor,
2636 pool,
2637 10,
2638 "",
2639 test_wiring(),
2640 );
2641
2642 let sender = handle.array_sender().clone();
2644 drop(handle);
2645 drop(sender);
2646
2647 let result = data_jh.join();
2649 assert!(result.is_ok());
2650 }
2651
2652 #[test]
2653 fn test_wire_to_nonzero_ndarray_addr() {
2654 use crate::plugin::wiring::upstream_key;
2660 let pool = Arc::new(NDArrayPool::new(1_000_000));
2661 let wiring = test_wiring();
2662
2663 let (up_handle, _up_jh) = create_plugin_runtime_multi_addr(
2665 "UP_MULTI",
2666 PassthroughProcessor,
2667 pool,
2668 10,
2669 "",
2670 wiring.clone(),
2671 2,
2672 );
2673 enable_callbacks(&up_handle);
2674
2675 let addr0 = wiring.lookup_output("UP_MULTI");
2677 let addr1 = wiring.lookup_output(&upstream_key("UP_MULTI", 1));
2678 assert!(addr0.is_some(), "addr 0 output must be registered");
2679 assert!(
2680 addr1.is_some(),
2681 "addr 1 output must be registered for a max_addr=2 port"
2682 );
2683
2684 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWN_ADDR1", 10);
2686 wiring
2687 .rewire(&downstream_sender, "", &upstream_key("UP_MULTI", 1))
2688 .expect("wiring a consumer to NDArrayAddr=1 must succeed");
2689
2690 send_array(up_handle.array_sender(), make_test_array(99));
2692 let received = downstream_rx.blocking_recv().unwrap();
2693 assert_eq!(
2694 received.unique_id, 99,
2695 "consumer wired to NDArrayAddr=1 must receive upstream arrays"
2696 );
2697 }
2698
2699 #[test]
2700 fn test_nonblocking_passthrough() {
2701 let pool = Arc::new(NDArrayPool::new(1_000_000));
2702 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2703 let mut output = NDArrayOutput::new();
2704 output.add(downstream_sender);
2705
2706 let (handle, _data_jh) = create_plugin_runtime_with_output(
2707 "NB_TEST",
2708 PassthroughProcessor,
2709 pool,
2710 10,
2711 output,
2712 "",
2713 test_wiring(),
2714 );
2715 enable_callbacks(&handle);
2716
2717 send_array(handle.array_sender(), make_test_array(42));
2718
2719 let received = downstream_rx.blocking_recv().unwrap();
2720 assert_eq!(received.unique_id, 42);
2721 }
2722
2723 #[test]
2724 fn test_blocking_to_nonblocking_switch() {
2725 let pool = Arc::new(NDArrayPool::new(1_000_000));
2726 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2727 let mut output = NDArrayOutput::new();
2728 output.add(downstream_sender);
2729
2730 let (handle, _data_jh) = create_plugin_runtime_with_output(
2731 "SWITCH_TEST",
2732 PassthroughProcessor,
2733 pool,
2734 10,
2735 output,
2736 "",
2737 test_wiring(),
2738 );
2739 enable_callbacks(&handle);
2740
2741 handle
2743 .port_runtime()
2744 .port_handle()
2745 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 1)
2746 .unwrap();
2747 params_applied(&handle);
2748
2749 send_array(handle.array_sender(), make_test_array(1));
2750 let received = downstream_rx.blocking_recv().unwrap();
2751 assert_eq!(received.unique_id, 1);
2752
2753 handle
2755 .port_runtime()
2756 .port_handle()
2757 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 0)
2758 .unwrap();
2759 params_applied(&handle);
2760
2761 send_array(handle.array_sender(), make_test_array(2));
2763 let received = downstream_rx.blocking_recv().unwrap();
2764 assert_eq!(received.unique_id, 2);
2765 }
2766
2767 #[test]
2768 fn test_enable_callbacks_disables_processing() {
2769 let pool = Arc::new(NDArrayPool::new(1_000_000));
2770 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2771 let mut output = NDArrayOutput::new();
2772 output.add(downstream_sender);
2773
2774 let (handle, _data_jh) = create_plugin_runtime_with_output(
2775 "ENABLE_TEST",
2776 PassthroughProcessor,
2777 pool,
2778 10,
2779 output,
2780 "",
2781 test_wiring(),
2782 );
2783
2784 handle
2786 .port_runtime()
2787 .port_handle()
2788 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 0)
2789 .unwrap();
2790 params_applied(&handle);
2791
2792 send_array(handle.array_sender(), make_test_array(99));
2794
2795 let rt = tokio::runtime::Builder::new_current_thread()
2797 .enable_all()
2798 .build()
2799 .unwrap();
2800 let result = rt.block_on(async {
2801 tokio::time::timeout(std::time::Duration::from_millis(100), downstream_rx.recv()).await
2802 });
2803 assert!(
2804 result.is_err(),
2805 "should not receive array when callbacks disabled"
2806 );
2807 }
2808
2809 #[test]
2810 fn test_downstream_receives_multiple() {
2811 let pool = Arc::new(NDArrayPool::new(1_000_000));
2812
2813 let (ds1, mut rx1) = ndarray_channel("DS1", 10);
2814 let (ds2, mut rx2) = ndarray_channel("DS2", 10);
2815 let mut output = NDArrayOutput::new();
2816 output.add(ds1);
2817 output.add(ds2);
2818
2819 let (handle, _data_jh) = create_plugin_runtime_with_output(
2820 "DS_TEST",
2821 PassthroughProcessor,
2822 pool,
2823 10,
2824 output,
2825 "",
2826 test_wiring(),
2827 );
2828 enable_callbacks(&handle);
2829
2830 send_array(handle.array_sender(), make_test_array(77));
2831
2832 let r1 = rx1.blocking_recv().unwrap();
2834 let r2 = rx2.blocking_recv().unwrap();
2835 assert_eq!(r1.unique_id, 77);
2836 assert_eq!(r2.unique_id, 77);
2837 }
2838
2839 #[test]
2840 fn test_param_updates_after_send() {
2841 let pool = Arc::new(NDArrayPool::new(1_000_000));
2842
2843 struct ParamTracker;
2844 impl NDPluginProcess for ParamTracker {
2845 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2846 ProcessResult::arrays(vec![Arc::new(array.clone())])
2847 }
2848 fn plugin_type(&self) -> &str {
2849 "ParamTracker"
2850 }
2851 }
2852
2853 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2854 let mut output = NDArrayOutput::new();
2855 output.add(downstream_sender);
2856
2857 let (handle, _data_jh) = create_plugin_runtime_with_output(
2858 "PARAM_TEST",
2859 ParamTracker,
2860 pool,
2861 10,
2862 output,
2863 "",
2864 test_wiring(),
2865 );
2866 enable_callbacks(&handle);
2867
2868 send_array(handle.array_sender(), make_test_array(1));
2870 let received = downstream_rx.blocking_recv().unwrap();
2871 assert_eq!(received.unique_id, 1);
2872
2873 handle
2875 .port_runtime()
2876 .port_handle()
2877 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2878 .unwrap();
2879 params_applied(&handle);
2880
2881 send_array(handle.array_sender(), make_test_array(2));
2883 let received = downstream_rx.blocking_recv().unwrap();
2884 assert_eq!(received.unique_id, 2);
2885 }
2886
2887 #[test]
2888 fn test_sort_buffer_reorders_by_unique_id() {
2889 let mut buf = SortBuffer::new();
2890
2891 buf.insert(3, vec![make_test_array(3)], 10);
2893 buf.insert(1, vec![make_test_array(1)], 10);
2894 buf.insert(2, vec![make_test_array(2)], 10);
2895
2896 assert_eq!(buf.len(), 3);
2897
2898 let drained = buf.drain_all();
2899 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2900 assert_eq!(ids, vec![1, 2, 3], "should drain in sorted uniqueId order");
2901 assert_eq!(buf.len(), 0);
2902 assert_eq!(buf.prev_unique_id, 3);
2903 }
2904
2905 #[test]
2906 fn test_sort_buffer_drain_ready_contiguous() {
2907 let mut buf = SortBuffer::new();
2910 buf.note_emitted(0);
2913 buf.insert(1, vec![make_test_array(1)], 10);
2914 buf.insert(2, vec![make_test_array(2)], 10);
2915 buf.insert(5, vec![make_test_array(5)], 10); let drained = buf.drain_ready(100.0);
2919 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2920 assert_eq!(ids, vec![1, 2], "contiguous run released; id=5 held by gap");
2921 assert_eq!(buf.len(), 1);
2922 }
2923
2924 #[test]
2925 fn test_sort_buffer_drain_ready_deadline() {
2926 let mut buf = SortBuffer::new();
2928 buf.note_emitted(1); buf.insert(5, vec![make_test_array(5)], 10); std::thread::sleep(std::time::Duration::from_millis(30));
2931 let drained = buf.drain_ready(0.01);
2933 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2934 assert_eq!(ids, vec![5], "stale head released via deadline");
2935 }
2936
2937 #[test]
2938 fn test_sort_buffer_detects_disordered_on_emit() {
2939 let mut buf = SortBuffer::new();
2941 buf.note_emitted(5); buf.note_emitted(3); assert_eq!(buf.disordered_arrays, 1);
2944 buf.note_emitted(4); assert_eq!(buf.disordered_arrays, 1);
2946 }
2947
2948 #[test]
2949 fn test_sort_buffer_drops_when_full() {
2950 let mut buf = SortBuffer::new();
2951
2952 assert!(buf.insert(1, vec![make_test_array(1)], 2));
2954 assert!(buf.insert(2, vec![make_test_array(2)], 2));
2955 assert!(!buf.insert(3, vec![make_test_array(3)], 2));
2956
2957 assert_eq!(buf.len(), 2);
2958 assert_eq!(buf.dropped_output_arrays, 1);
2959 }
2960
2961 #[test]
2962 fn test_sort_mode_runtime_integration() {
2963 let pool = Arc::new(NDArrayPool::new(1_000_000));
2964 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2965 let mut output = NDArrayOutput::new();
2966 output.add(downstream_sender);
2967
2968 let (handle, _data_jh) = create_plugin_runtime_with_output(
2969 "SORT_TEST",
2970 PassthroughProcessor,
2971 pool,
2972 10,
2973 output,
2974 "",
2975 test_wiring(),
2976 );
2977 enable_callbacks(&handle);
2978
2979 handle
2981 .port_runtime()
2982 .port_handle()
2983 .write_int32_blocking(handle.plugin_params.sort_size, 0, 10)
2984 .unwrap();
2985 handle
2986 .port_runtime()
2987 .port_handle()
2988 .write_float64_blocking(handle.plugin_params.sort_time, 0, 0.1)
2989 .unwrap();
2990 handle
2991 .port_runtime()
2992 .port_handle()
2993 .write_int32_blocking(handle.plugin_params.sort_mode, 0, 1)
2994 .unwrap();
2995 params_applied(&handle);
2996
2997 send_array(handle.array_sender(), make_test_array(1));
3000 send_array(handle.array_sender(), make_test_array(2));
3001 send_array(handle.array_sender(), make_test_array(3));
3002
3003 let rt = tokio::runtime::Builder::new_current_thread()
3004 .enable_all()
3005 .build()
3006 .unwrap();
3007 let fast = rt.block_on(async {
3008 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3009 });
3010 assert!(
3011 fast.is_ok(),
3012 "in-order arrays must be emitted immediately, not buffered"
3013 );
3014 assert_eq!(fast.unwrap().unwrap().unique_id, 1);
3015 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3016 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 3);
3017
3018 send_array(handle.array_sender(), make_test_array(5));
3022 send_array(handle.array_sender(), make_test_array(4));
3023 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 4);
3026 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 5);
3027 }
3028
3029 #[test]
3030 fn test_throttle_drops_output_arrays() {
3031 let pool = Arc::new(NDArrayPool::new(1_000_000));
3034 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3035 let mut output = NDArrayOutput::new();
3036 output.add(downstream_sender);
3037
3038 let (handle, _data_jh) = create_plugin_runtime_with_output(
3039 "THROTTLE_TEST",
3040 PassthroughProcessor,
3041 pool,
3042 10,
3043 output,
3044 "",
3045 test_wiring(),
3046 );
3047 enable_callbacks(&handle);
3048
3049 handle
3052 .port_runtime()
3053 .port_handle()
3054 .write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 8.0)
3055 .unwrap();
3056 params_applied(&handle);
3057
3058 for id in 1..=5 {
3059 send_array(handle.array_sender(), make_test_array(id));
3060 }
3061 let port = handle.port_runtime().port_handle().clone();
3066 let counter = handle.ndarray_params.array_counter;
3067 wait_until("all 5 frames to be processed", || {
3068 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 5)
3069 });
3070
3071 let rt = tokio::runtime::Builder::new_current_thread()
3073 .enable_all()
3074 .build()
3075 .unwrap();
3076 let mut received = 0;
3077 while rt
3078 .block_on(async {
3079 tokio::time::timeout(std::time::Duration::from_millis(20), downstream_rx.recv())
3080 .await
3081 })
3082 .map(|o| o.is_some())
3083 .unwrap_or(false)
3084 {
3085 received += 1;
3086 }
3087 assert!(
3088 received < 5,
3089 "throttle must drop some arrays (got {received})"
3090 );
3091 assert!(received >= 1, "first array within budget must pass");
3092 }
3093
3094 #[test]
3095 fn test_process_plugin_reprocesses_last_input() {
3096 let pool = Arc::new(NDArrayPool::new(1_000_000));
3098 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3099 let mut output = NDArrayOutput::new();
3100 output.add(downstream_sender);
3101
3102 let (handle, _data_jh) = create_plugin_runtime_with_output(
3103 "PROCESS_PLUGIN_TEST",
3104 PassthroughProcessor,
3105 pool,
3106 10,
3107 output,
3108 "",
3109 test_wiring(),
3110 );
3111 enable_callbacks(&handle);
3112
3113 send_array(handle.array_sender(), make_test_array(7));
3114 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 7);
3115
3116 handle
3118 .port_runtime()
3119 .port_handle()
3120 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3121 .unwrap();
3122 let reprocessed = downstream_rx.blocking_recv().unwrap();
3123 assert_eq!(
3124 reprocessed.unique_id, 7,
3125 "ProcessPlugin re-emits last input"
3126 );
3127 }
3128
3129 #[test]
3130 fn test_min_callback_time_throttle_not_counted() {
3131 let pool = Arc::new(NDArrayPool::new(1_000_000));
3139 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3140 let mut output = NDArrayOutput::new();
3141 output.add(downstream_sender);
3142
3143 let (handle, _data_jh) = create_plugin_runtime_with_output(
3144 "MIN_CB_TEST",
3145 PassthroughProcessor,
3146 pool,
3147 10,
3148 output,
3149 "",
3150 test_wiring(),
3151 );
3152 enable_callbacks(&handle);
3153 let dropped = handle.array_sender().dropped_arrays_counter().clone();
3154
3155 handle
3157 .port_runtime()
3158 .port_handle()
3159 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3160 .unwrap();
3161 params_applied(&handle);
3162
3163 send_array(handle.array_sender(), make_test_array(1));
3164 send_array(handle.array_sender(), make_test_array(2));
3165
3166 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3167 params_applied(&handle);
3171 let rt = tokio::runtime::Builder::new_current_thread()
3172 .enable_all()
3173 .build()
3174 .unwrap();
3175 let second = rt.block_on(async {
3176 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3177 });
3178 assert!(
3179 second.is_err(),
3180 "second array throttled out by MinCallbackTime"
3181 );
3182 assert_eq!(
3183 dropped.load(Ordering::Acquire),
3184 0,
3185 "a MinCallbackTime-throttled frame must NOT increment DroppedArrays"
3186 );
3187 }
3188
3189 #[test]
3190 fn test_array_callbacks_zero_withholds_downstream_delivery() {
3191 let pool = Arc::new(NDArrayPool::new(1_000_000));
3197 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3198 let mut output = NDArrayOutput::new();
3199 output.add(downstream_sender);
3200
3201 let (handle, _data_jh) = create_plugin_runtime_with_output(
3202 "ARRAY_CB_TEST",
3203 PassthroughProcessor,
3204 pool,
3205 10,
3206 output,
3207 "",
3208 test_wiring(),
3209 );
3210 enable_callbacks(&handle);
3211 let port = handle.port_runtime().port_handle().clone();
3212
3213 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
3215 .unwrap();
3216 params_applied(&handle);
3217
3218 send_array(handle.array_sender(), make_test_array(1));
3219 wait_until("frame 1 to be processed", || {
3222 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3223 .is_ok_and(|v| v == 1)
3224 });
3225
3226 let rt = tokio::runtime::Builder::new_current_thread()
3228 .enable_all()
3229 .build()
3230 .unwrap();
3231 let got = rt.block_on(async {
3232 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3233 });
3234 assert!(
3235 got.is_err(),
3236 "NDArrayCallbacks=0 must withhold downstream delivery"
3237 );
3238 assert_eq!(
3240 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3241 .unwrap(),
3242 1,
3243 "processing (and metadata params) must continue while delivery is off"
3244 );
3245
3246 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 1)
3248 .unwrap();
3249 params_applied(&handle);
3250 send_array(handle.array_sender(), make_test_array(2));
3251 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3252 }
3253
3254 #[test]
3255 fn test_plugin_output_publishes_compressed_size() {
3256 struct CompressProcessor;
3262 impl NDPluginProcess for CompressProcessor {
3263 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3264 let mut out = array.clone();
3265 out.codec = Some(crate::codec::Codec {
3266 name: crate::codec::CodecName::JPEG,
3267 compressed_size: 7,
3268 level: 0,
3269 shuffle: 0,
3270 compressor: 0,
3271 original_data_type: NDDataType::UInt8,
3272 });
3273 ProcessResult::arrays(vec![Arc::new(out)])
3274 }
3275 fn plugin_type(&self) -> &str {
3276 "Compress"
3277 }
3278 }
3279
3280 {
3282 let pool = Arc::new(NDArrayPool::new(1_000_000));
3283 let (ds, _rx) = ndarray_channel("DS_RAW", 10);
3284 let mut output = NDArrayOutput::new();
3285 output.add(ds);
3286 let (handle, _jh) = create_plugin_runtime_with_output(
3287 "CODEC_RAW",
3288 PassthroughProcessor,
3289 pool,
3290 10,
3291 output,
3292 "",
3293 test_wiring(),
3294 );
3295 enable_callbacks(&handle);
3296 let port = handle.port_runtime().port_handle().clone();
3297 send_array(handle.array_sender(), make_test_array(1));
3298 wait_until(
3301 "uncompressed output to publish CompressedSize == raw byte count",
3302 || {
3303 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3304 .is_ok_and(|v| v == 4)
3305 },
3306 );
3307 }
3308
3309 {
3311 let pool = Arc::new(NDArrayPool::new(1_000_000));
3312 let (ds, _rx) = ndarray_channel("DS_CMP", 10);
3313 let mut output = NDArrayOutput::new();
3314 output.add(ds);
3315 let (handle, _jh) = create_plugin_runtime_with_output(
3316 "CODEC_CMP",
3317 CompressProcessor,
3318 pool,
3319 10,
3320 output,
3321 "",
3322 test_wiring(),
3323 );
3324 enable_callbacks(&handle);
3325 let port = handle.port_runtime().port_handle().clone();
3326 send_array(handle.array_sender(), make_test_array(1));
3327 wait_until(
3328 "compressed output to publish CompressedSize == codec.compressed_size",
3329 || {
3330 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3331 .is_ok_and(|v| v == 7)
3332 },
3333 );
3334 }
3335 }
3336
3337 #[test]
3338 fn test_process_plugin_skips_throttled_input() {
3339 let pool = Arc::new(NDArrayPool::new(1_000_000));
3344 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3345 let mut output = NDArrayOutput::new();
3346 output.add(downstream_sender);
3347
3348 let (handle, _data_jh) = create_plugin_runtime_with_output(
3349 "PROCESS_THROTTLE_TEST",
3350 PassthroughProcessor,
3351 pool,
3352 10,
3353 output,
3354 "",
3355 test_wiring(),
3356 );
3357 enable_callbacks(&handle);
3358
3359 handle
3361 .port_runtime()
3362 .port_handle()
3363 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3364 .unwrap();
3365 params_applied(&handle);
3366
3367 send_array(handle.array_sender(), make_test_array(1));
3368 send_array(handle.array_sender(), make_test_array(2));
3369
3370 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3372 params_applied(&handle);
3376
3377 handle
3382 .port_runtime()
3383 .port_handle()
3384 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 0.0)
3385 .unwrap();
3386 handle
3390 .port_runtime()
3391 .port_handle()
3392 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3393 .unwrap();
3394 let reprocessed = downstream_rx.blocking_recv().unwrap();
3395 assert_eq!(
3396 reprocessed.unique_id, 1,
3397 "ProcessPlugin must re-inject the last processed array (1), not the throttled array (2)"
3398 );
3399 }
3400
3401 #[test]
3402 fn test_g3_compressed_array_dropped_on_non_aware_plugin() {
3403 let pool = Arc::new(NDArrayPool::new(1_000_000));
3405 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3406 let mut output = NDArrayOutput::new();
3407 output.add(downstream_sender);
3408
3409 let (handle, _data_jh) = create_plugin_runtime_with_output(
3410 "G3_TEST",
3411 PassthroughProcessor, pool,
3413 10,
3414 output,
3415 "",
3416 test_wiring(),
3417 );
3418 enable_callbacks(&handle);
3419
3420 let mut compressed = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
3422 compressed.unique_id = 1;
3423 compressed.codec = Some(crate::codec::Codec {
3424 name: crate::codec::CodecName::JPEG,
3425 compressed_size: 16,
3426 level: 0,
3427 shuffle: 0,
3428 compressor: 0,
3429 original_data_type: NDDataType::UInt8,
3430 });
3431 send_array(handle.array_sender(), Arc::new(compressed));
3432
3433 send_array(handle.array_sender(), make_test_array(2));
3435
3436 let r = downstream_rx.blocking_recv().unwrap();
3437 assert_eq!(
3438 r.unique_id, 2,
3439 "compressed array dropped; only the raw array reaches downstream"
3440 );
3441 }
3442
3443 #[test]
3444 fn test_drop_on_full_increments_dropped_counter() {
3445 struct SlowProcessor;
3449 impl NDPluginProcess for SlowProcessor {
3450 fn process_array(&mut self, _a: &NDArray, _p: &NDArrayPool) -> ProcessResult {
3451 std::thread::sleep(std::time::Duration::from_millis(200));
3452 ProcessResult::empty()
3453 }
3454 fn plugin_type(&self) -> &str {
3455 "Slow"
3456 }
3457 }
3458 let pool = Arc::new(NDArrayPool::new(1_000_000));
3459
3460 let (downstream_handle, _ds_jh) =
3462 create_plugin_runtime("B1_DOWNSTREAM", SlowProcessor, pool, 1, "", test_wiring());
3463 enable_callbacks(&downstream_handle);
3464 let ds_sender = downstream_handle.array_sender().clone();
3465 let dropped = ds_sender.dropped_arrays_counter().clone();
3466
3467 send_array(&ds_sender, make_test_array(1));
3470 send_array(&ds_sender, make_test_array(2));
3471 send_array(&ds_sender, make_test_array(3));
3472 send_array(&ds_sender, make_test_array(4));
3473
3474 assert!(
3475 dropped.load(Ordering::Acquire) >= 1,
3476 "arrays dropped on a full queue must be counted (got {})",
3477 dropped.load(Ordering::Acquire)
3478 );
3479 }
3480
3481 #[test]
3482 fn test_cross_width_narrowing_array_read_truncates() {
3483 let mut out = [0i8; 1];
3492 let n = copy_ccast(&[300u16], &mut out);
3493 assert_eq!(n, 1);
3494 assert_eq!(out[0], 44, "(epicsInt8)(epicsUInt16)300 == 44 (low 8 bits)");
3495 let mut sat = [0i8; 1];
3497 copy_convert(&[300u16], &mut sat);
3498 assert_eq!(sat[0], 127, "f64 round-trip saturates — the wrong behavior");
3499
3500 let mut out2 = [0i8; 1];
3502 copy_ccast(&[0x1234_5678i32], &mut out2);
3503 assert_eq!(out2[0], 0x78);
3504
3505 let mut out3 = [0i8; 1];
3507 copy_ccast(&[-1i32], &mut out3);
3508 assert_eq!(out3[0], -1);
3509
3510 let mut out4 = [0i8; 1];
3512 copy_ccast(&[255u16], &mut out4);
3513 assert_eq!(out4[0], -1);
3514
3515 let mut out5 = [0i32; 1];
3517 copy_ccast(&[0x0000_0001_0000_002Ai64], &mut out5);
3518 assert_eq!(out5[0], 42);
3519
3520 let mut out6 = [0i16; 1];
3522 copy_ccast(&[70000u32], &mut out6);
3523 assert_eq!(out6[0], 4464);
3524
3525 let mut out7 = [0i8; 1];
3528 copy_ccast(&[255u8], &mut out7);
3529 assert_eq!(out7[0], -1);
3530
3531 let mut fout = [0i32; 1];
3537 copy_convert(&[42.9f64], &mut fout);
3538 assert_eq!(fout[0], 42, "f64 -> i32 truncates toward zero");
3539 }
3540
3541 fn block<F: std::future::Future>(f: F) -> F::Output {
3545 tokio::runtime::Builder::new_current_thread()
3546 .enable_all()
3547 .build()
3548 .unwrap()
3549 .block_on(f)
3550 }
3551
3552 #[test]
3553 fn test_scatter_reroutes_past_full_consumer() {
3554 let (sa, mut ra) = ndarray_channel("A", 1);
3559 let (sb, mut rb) = ndarray_channel("B", 1);
3560 let (sc, _rc) = ndarray_channel("C", 1);
3561 block(async {
3562 assert_eq!(
3563 sa.publish(make_test_array(99)).await,
3564 PublishOutcome::Delivered
3565 );
3566 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3567 let mut cursor = 0usize;
3568 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3569 assert_eq!(cursor, 2);
3571 assert_eq!(rb.recv().await.unwrap().unique_id, 1);
3572 assert_eq!(ra.recv().await.unwrap().unique_id, 99);
3574 assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3575 });
3576 }
3577
3578 #[test]
3579 fn test_scatter_drops_on_last_when_all_full_counts_once() {
3580 let (sa, mut ra) = ndarray_channel("A", 1);
3584 let (sb, mut rb) = ndarray_channel("B", 1);
3585 block(async {
3586 sa.publish(make_test_array(91)).await;
3587 sb.publish(make_test_array(92)).await;
3588 let senders = vec![sa.clone(), sb.clone()];
3589 let mut cursor = 0usize;
3590 ProcessOutput::scatter_publish(&make_test_array(7), &senders, &mut cursor).await;
3591 assert_eq!(cursor, 2); assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3594 assert_eq!(sb.dropped_arrays_counter().load(Ordering::Acquire), 1);
3595 assert_eq!(ra.recv().await.unwrap().unique_id, 91);
3597 assert_eq!(rb.recv().await.unwrap().unique_id, 92);
3598 });
3599 }
3600
3601 #[test]
3602 fn test_scatter_cursor_advances_per_attempt_across_frames() {
3603 let (sa, _ra) = ndarray_channel("A", 1);
3608 let (sb, mut rb) = ndarray_channel("B", 10);
3609 let (sc, mut rc) = ndarray_channel("C", 10);
3610 block(async {
3611 sa.publish(make_test_array(90)).await; let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3613 let mut cursor = 0usize;
3614 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3615 assert_eq!(cursor, 2); assert_eq!(rb.recv().await.unwrap().unique_id, 0);
3617 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3618 assert_eq!(cursor, 3); assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3620 });
3621 }
3622
3623 #[test]
3624 fn test_scatter_skips_disabled_consumer() {
3625 let (sa, mut ra) = ndarray_channel("A", 10);
3628 let (mut sb, _rb) = ndarray_channel("B", 10);
3629 let (sc, mut rc) = ndarray_channel("C", 10);
3630 sb.set_mode_flags(
3631 Arc::new(AtomicBool::new(false)),
3632 Arc::new(AtomicBool::new(false)),
3633 );
3634 block(async {
3635 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3636 let mut cursor = 0usize;
3637 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3639 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3640 assert_eq!(ra.recv().await.unwrap().unique_id, 0);
3641 assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3642 });
3643 }
3644}