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
1172const PLUGIN_NDARRAY_ADDR: i32 = 0;
1182const PLUGIN_MAX_THREADS: i32 = 1;
1183const PLUGIN_NUM_THREADS: i32 = 1;
1184
1185#[allow(dead_code)]
1187pub struct PluginPortDriver {
1188 base: PortDriverBase,
1189 ndarray_params: NDArrayDriverParams,
1190 plugin_params: PluginBaseParams,
1191 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1192 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1194 std_array_data_param: Option<usize>,
1196}
1197
1198impl PluginPortDriver {
1199 fn new<P: NDPluginProcess>(
1200 port_name: &str,
1201 plugin_type_name: &str,
1202 queue_size: usize,
1203 ndarray_port: &str,
1204 max_addr: usize,
1205 param_change_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1206 processor: &mut P,
1207 array_data: Option<Arc<parking_lot::Mutex<Option<Arc<NDArray>>>>>,
1208 pool: &NDArrayPool,
1209 ) -> AsynResult<Self> {
1210 let mut base = PortDriverBase::new(
1211 port_name,
1212 max_addr,
1213 PortFlags {
1214 can_block: true,
1215 ..Default::default()
1216 },
1217 );
1218
1219 let ndarray_params = NDArrayDriverParams::create(&mut base)?;
1220 let plugin_params = PluginBaseParams::create(&mut base)?;
1221
1222 base.set_string_param(plugin_params.nd_array_port, 0, ndarray_port.into())?;
1232 base.set_int32_param(plugin_params.nd_array_addr, 0, PLUGIN_NDARRAY_ADDR)?;
1233 base.set_int32_param(plugin_params.dropped_arrays, 0, 0)?;
1234 base.set_int32_param(plugin_params.dropped_output_arrays, 0, 0)?;
1235 base.set_int32_param(plugin_params.queue_size, 0, queue_size as i32)?;
1236 base.set_int32_param(plugin_params.queue_use, 0, queue_size as i32)?;
1240 base.set_int32_param(plugin_params.max_threads, 0, PLUGIN_MAX_THREADS)?;
1241 base.set_int32_param(plugin_params.num_threads, 0, PLUGIN_NUM_THREADS)?;
1242 base.set_int32_param(plugin_params.blocking_callbacks, 0, 0)?;
1245
1246 base.set_int32_param(plugin_params.enable_callbacks, 0, 0)?;
1248 base.set_string_param(plugin_params.plugin_type, 0, plugin_type_name.into())?;
1249
1250 crate::driver::ndarray_driver::init_read_only_params(
1254 &mut base,
1255 &ndarray_params,
1256 port_name,
1257 )?;
1258 crate::driver::ndarray_driver::refresh_pool_stats(&mut base, &ndarray_params, pool)?;
1259 base.set_int32_param(
1263 ndarray_params.array_callbacks,
1264 0,
1265 processor.does_array_callbacks() as i32,
1266 )?;
1267 base.set_string_param(ndarray_params.full_file_name, 0, "".into())?;
1270 let std_array_data_param = if array_data.is_some() {
1272 Some(base.create_param("STD_ARRAY_DATA", asyn_rs::param::ParamType::GenericPointer)?)
1273 } else {
1274 None
1275 };
1276
1277 processor.register_params(&mut base)?;
1279
1280 Ok(Self {
1281 base,
1282 ndarray_params,
1283 plugin_params,
1284 param_change_tx,
1285 array_data,
1286 std_array_data_param,
1287 })
1288 }
1289}
1290
1291fn copy_direct<T: Copy>(src: &[T], dst: &mut [T]) -> usize {
1293 let n = src.len().min(dst.len());
1294 dst[..n].copy_from_slice(&src[..n]);
1295 n
1296}
1297
1298fn copy_convert<S, D>(src: &[S], dst: &mut [D]) -> usize
1300where
1301 S: CastToF64 + Copy,
1302 D: CastFromF64 + Copy,
1303{
1304 let n = src.len().min(dst.len());
1305 for i in 0..n {
1306 dst[i] = D::cast_from_f64(src[i].cast_to_f64());
1307 }
1308 n
1309}
1310
1311trait CCastTo<D> {
1327 fn ccast(self) -> D;
1328}
1329macro_rules! impl_ccast {
1330 ( $src:ty => $( $dst:ty ),+ ) => {
1331 $(
1332 impl CCastTo<$dst> for $src {
1333 #[inline]
1334 fn ccast(self) -> $dst {
1335 self as $dst
1336 }
1337 }
1338 )+
1339 };
1340}
1341impl_ccast!(i8 => i16, i32, i64);
1342impl_ccast!(u8 => i8, i16, i32, i64);
1343impl_ccast!(i16 => i8, i32, i64);
1344impl_ccast!(u16 => i8, i16, i32, i64);
1345impl_ccast!(i32 => i8, i16, i64);
1346impl_ccast!(u32 => i8, i16, i32, i64);
1347impl_ccast!(i64 => i8, i16, i32);
1348impl_ccast!(u64 => i8, i16, i32, i64);
1349
1350fn copy_ccast<S, D>(src: &[S], dst: &mut [D]) -> usize
1354where
1355 S: CCastTo<D> + Copy,
1356 D: Copy,
1357{
1358 let n = src.len().min(dst.len());
1359 for i in 0..n {
1360 dst[i] = src[i].ccast();
1361 }
1362 n
1363}
1364
1365trait CastToF64 {
1367 fn cast_to_f64(self) -> f64;
1368}
1369
1370impl CastToF64 for i8 {
1371 fn cast_to_f64(self) -> f64 {
1372 self as f64
1373 }
1374}
1375impl CastToF64 for u8 {
1376 fn cast_to_f64(self) -> f64 {
1377 self as f64
1378 }
1379}
1380impl CastToF64 for i16 {
1381 fn cast_to_f64(self) -> f64 {
1382 self as f64
1383 }
1384}
1385impl CastToF64 for u16 {
1386 fn cast_to_f64(self) -> f64 {
1387 self as f64
1388 }
1389}
1390impl CastToF64 for i32 {
1391 fn cast_to_f64(self) -> f64 {
1392 self as f64
1393 }
1394}
1395impl CastToF64 for u32 {
1396 fn cast_to_f64(self) -> f64 {
1397 self as f64
1398 }
1399}
1400impl CastToF64 for i64 {
1401 fn cast_to_f64(self) -> f64 {
1402 self as f64
1403 }
1404}
1405impl CastToF64 for u64 {
1406 fn cast_to_f64(self) -> f64 {
1407 self as f64
1408 }
1409}
1410impl CastToF64 for f32 {
1411 fn cast_to_f64(self) -> f64 {
1412 self as f64
1413 }
1414}
1415impl CastToF64 for f64 {
1416 fn cast_to_f64(self) -> f64 {
1417 self
1418 }
1419}
1420
1421trait CastFromF64 {
1423 fn cast_from_f64(v: f64) -> Self;
1424}
1425
1426impl CastFromF64 for i8 {
1427 fn cast_from_f64(v: f64) -> Self {
1428 v as i8
1429 }
1430}
1431impl CastFromF64 for i16 {
1432 fn cast_from_f64(v: f64) -> Self {
1433 v as i16
1434 }
1435}
1436impl CastFromF64 for i32 {
1437 fn cast_from_f64(v: f64) -> Self {
1438 v as i32
1439 }
1440}
1441impl CastFromF64 for i64 {
1442 fn cast_from_f64(v: f64) -> Self {
1443 v as i64
1444 }
1445}
1446impl CastFromF64 for f32 {
1447 fn cast_from_f64(v: f64) -> Self {
1448 v as f32
1449 }
1450}
1451impl CastFromF64 for f64 {
1452 fn cast_from_f64(v: f64) -> Self {
1453 v
1454 }
1455}
1456
1457macro_rules! impl_read_array {
1460 (
1461 $self:expr, $buf:expr, $direct_variant:ident,
1462 ccast: [ $( $ccast_variant:ident ),* ],
1463 convert: [ $( $variant:ident ),* ]
1464 ) => {{
1465 use crate::ndarray::NDDataBuffer;
1466 let handle = match &$self.array_data {
1467 Some(h) => h,
1468 None => return Ok(0),
1469 };
1470 let guard = handle.lock();
1471 let array = match &*guard {
1472 Some(a) => a,
1473 None => return Ok(0),
1474 };
1475 let n = match &array.data {
1476 NDDataBuffer::$direct_variant(v) => copy_direct(v, $buf),
1477 $( NDDataBuffer::$ccast_variant(v) => copy_ccast(v, $buf), )*
1478 $( NDDataBuffer::$variant(v) => copy_convert(v, $buf), )*
1479 };
1480 Ok(n)
1481 }};
1482}
1483
1484impl PortDriver for PluginPortDriver {
1485 fn base(&self) -> &PortDriverBase {
1486 &self.base
1487 }
1488
1489 fn base_mut(&mut self) -> &mut PortDriverBase {
1490 &mut self.base
1491 }
1492
1493 fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1494 let reason = user.reason;
1495 let addr = user.addr;
1496 self.base.set_int32_param(reason, addr, value)?;
1497 self.base.call_param_callbacks(addr)?;
1498 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1500 reason,
1501 addr,
1502 ParamChangeValue::Int32(value),
1503 ));
1504 Ok(())
1505 }
1506
1507 fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1508 let reason = user.reason;
1509 let addr = user.addr;
1510 self.base.set_float64_param(reason, addr, value)?;
1511 self.base.call_param_callbacks(addr)?;
1512 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1513 reason,
1514 addr,
1515 ParamChangeValue::Float64(value),
1516 ));
1517 Ok(())
1518 }
1519
1520 fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1521 let reason = user.reason;
1522 let addr = user.addr;
1523 let s = String::from_utf8_lossy(data).into_owned();
1524 self.base.set_string_param(reason, addr, s.clone())?;
1525 self.base.call_param_callbacks(addr)?;
1526 let _ = self.param_change_tx.send(PluginParamMsg::Change(
1527 reason,
1528 addr,
1529 ParamChangeValue::Octet(s),
1530 ));
1531 Ok(data.len())
1532 }
1533
1534 fn read_int8_array(&mut self, _user: &AsynUser, buf: &mut [i8]) -> AsynResult<usize> {
1535 impl_read_array!(
1538 self, buf, I8,
1539 ccast: [U8, I16, U16, I32, U32, I64, U64],
1540 convert: [F32, F64]
1541 )
1542 }
1543
1544 fn read_int16_array(&mut self, _user: &AsynUser, buf: &mut [i16]) -> AsynResult<usize> {
1545 impl_read_array!(
1546 self, buf, I16,
1547 ccast: [I8, U8, U16, I32, U32, I64, U64],
1548 convert: [F32, F64]
1549 )
1550 }
1551
1552 fn read_int32_array(&mut self, _user: &AsynUser, buf: &mut [i32]) -> AsynResult<usize> {
1553 impl_read_array!(
1554 self, buf, I32,
1555 ccast: [I8, U8, I16, U16, U32, I64, U64],
1556 convert: [F32, F64]
1557 )
1558 }
1559
1560 fn read_int64_array(&mut self, _user: &AsynUser, buf: &mut [i64]) -> AsynResult<usize> {
1561 impl_read_array!(
1562 self, buf, I64,
1563 ccast: [I8, U8, I16, U16, I32, U32, U64],
1564 convert: [F32, F64]
1565 )
1566 }
1567
1568 fn read_float32_array(&mut self, _user: &AsynUser, buf: &mut [f32]) -> AsynResult<usize> {
1569 impl_read_array!(
1570 self, buf, F32,
1571 ccast: [],
1572 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F64]
1573 )
1574 }
1575
1576 fn read_float64_array(&mut self, _user: &AsynUser, buf: &mut [f64]) -> AsynResult<usize> {
1577 impl_read_array!(
1578 self, buf, F64,
1579 ccast: [],
1580 convert: [I8, U8, I16, U16, I32, U32, I64, U64, F32]
1581 )
1582 }
1583}
1584
1585#[derive(Clone)]
1587pub struct PluginRuntimeHandle {
1588 port_runtime: PortRuntimeHandle,
1589 array_sender: NDArraySender,
1590 array_output: Arc<parking_lot::Mutex<NDArrayOutput>>,
1591 port_name: String,
1592 param_tx: tokio::sync::mpsc::UnboundedSender<PluginParamMsg>,
1593 pub ndarray_params: NDArrayDriverParams,
1594 pub plugin_params: PluginBaseParams,
1595}
1596
1597impl PluginRuntimeHandle {
1598 pub fn port_runtime(&self) -> &PortRuntimeHandle {
1599 &self.port_runtime
1600 }
1601
1602 pub fn array_sender(&self) -> &NDArraySender {
1603 &self.array_sender
1604 }
1605
1606 pub fn array_output(&self) -> &Arc<parking_lot::Mutex<NDArrayOutput>> {
1607 &self.array_output
1608 }
1609
1610 pub fn wait_params_applied(&self, timeout: std::time::Duration) -> bool {
1628 let (ack_tx, ack_rx) = std::sync::mpsc::sync_channel(1);
1629 if self.param_tx.send(PluginParamMsg::Barrier(ack_tx)).is_err() {
1630 return false;
1631 }
1632 ack_rx.recv_timeout(timeout).is_ok()
1633 }
1634
1635 pub fn port_name(&self) -> &str {
1636 &self.port_name
1637 }
1638}
1639
1640pub fn create_plugin_runtime<P: NDPluginProcess>(
1647 port_name: &str,
1648 processor: P,
1649 pool: Arc<NDArrayPool>,
1650 queue_size: usize,
1651 ndarray_port: &str,
1652 wiring: Arc<WiringRegistry>,
1653) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1654 create_plugin_runtime_multi_addr(
1655 port_name,
1656 processor,
1657 pool,
1658 queue_size,
1659 ndarray_port,
1660 wiring,
1661 1,
1662 )
1663}
1664
1665pub fn create_plugin_runtime_multi_addr<P: NDPluginProcess>(
1669 port_name: &str,
1670 mut processor: P,
1671 pool: Arc<NDArrayPool>,
1672 queue_size: usize,
1673 ndarray_port: &str,
1674 wiring: Arc<WiringRegistry>,
1675 max_addr: usize,
1676) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
1677 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
1682 let handle_param_tx = param_tx.clone();
1683
1684 let plugin_type_name = processor.plugin_type().to_string();
1686 let compression_aware = processor.compression_aware();
1687 let does_array_callbacks = processor.does_array_callbacks();
1688 let array_data = processor.array_data_handle();
1689
1690 let driver = PluginPortDriver::new(
1692 port_name,
1693 &plugin_type_name,
1694 queue_size,
1695 ndarray_port,
1696 max_addr,
1697 param_tx,
1698 &mut processor,
1699 array_data,
1700 &pool,
1701 )
1702 .expect("failed to create plugin port driver");
1703
1704 let ndarray_params = driver.ndarray_params;
1705 let plugin_params = driver.plugin_params;
1706 let std_array_data_param = driver.std_array_data_param;
1707
1708 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default())
1719 .unwrap_or_else(|e| port_runtime_unavailable(port_name, &e));
1720
1721 let port_handle = port_runtime.port_handle().clone();
1723
1724 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
1726
1727 let enabled = Arc::new(AtomicBool::new(false));
1729 let blocking_mode = Arc::new(AtomicBool::new(false));
1730
1731 let array_output = Arc::new(parking_lot::Mutex::new(NDArrayOutput::new()));
1733 let array_output_for_handle = array_output.clone();
1734 wiring.register_output_addrs(port_name, max_addr, array_output.clone());
1740 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
1743 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
1744 processor,
1745 output: array_output,
1746 pool,
1747 ndarray_params,
1748 plugin_params,
1749 port_handle,
1750 array_counter: 0,
1751 std_array_data_param,
1752 array_callbacks: does_array_callbacks,
1755 min_callback_time: 0.0,
1756 last_process_time: None,
1757 sort_mode: 0,
1758 sort_time: 0.0,
1759 sort_size: 10,
1760 sort_buffer: SortBuffer::new(),
1761 dropped_arrays: dropped_arrays_counter,
1762 compression_aware,
1763 max_byte_rate: 0.0,
1764 throttler: super::throttler::Throttler::new(0.0),
1765 prev_input_array: None,
1766 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
1767 nd_array_addr: PLUGIN_NDARRAY_ADDR,
1768 max_threads: PLUGIN_MAX_THREADS,
1769 num_threads: PLUGIN_NUM_THREADS,
1770 }));
1771
1772 let data_enabled = enabled.clone();
1773 let data_blocking = blocking_mode.clone();
1774
1775 let mut array_sender = array_sender;
1776 array_sender.set_mode_flags(enabled, blocking_mode);
1777
1778 let sender_port_name = port_name.to_string();
1780 let initial_upstream = ndarray_port.to_string();
1781
1782 let data_jh = MandatoryThread::new(
1784 format!("plugin-data-{port_name}"),
1785 ThreadPriority::Medium,
1789 StackSizeClass::Medium,
1792 )
1793 .spawn(move || {
1794 plugin_data_loop(
1795 shared,
1796 array_rx,
1797 param_rx,
1798 plugin_params,
1799 ndarray_params.array_counter,
1800 data_enabled,
1801 data_blocking,
1802 sender_port_name,
1803 initial_upstream,
1804 wiring,
1805 );
1806 });
1807
1808 let handle = PluginRuntimeHandle {
1809 port_runtime,
1810 array_sender,
1811 array_output: array_output_for_handle,
1812 port_name: port_name.to_string(),
1813 param_tx: handle_param_tx,
1814 ndarray_params,
1815 plugin_params,
1816 };
1817
1818 (handle, data_jh)
1819}
1820
1821fn queue_status_batch(
1827 plugin_params: &PluginBaseParams,
1828 max_capacity: usize,
1829 free: i32,
1830) -> ParamBatch {
1831 use asyn_rs::request::ParamSetValue;
1832 ParamBatch {
1833 addr0: vec![
1834 ParamSetValue::new(
1835 plugin_params.queue_size,
1836 0,
1837 ParamValue::Int32(max_capacity as i32),
1838 ),
1839 ParamSetValue::new(plugin_params.queue_use, 0, ParamValue::Int32(free)),
1840 ],
1841 extra: std::collections::HashMap::new(),
1842 }
1843}
1844
1845async fn clamp_writeback(port: &PortHandle, reason: usize, value: i32) {
1848 use asyn_rs::request::ParamSetValue;
1849 let _ = port
1850 .set_params_and_notify(
1851 0,
1852 vec![ParamSetValue::new(
1853 reason,
1854 0,
1855 asyn_rs::param::ParamValue::Int32(value),
1856 )],
1857 )
1858 .await;
1859}
1860
1861fn plugin_data_loop<P: NDPluginProcess>(
1862 shared: Arc<parking_lot::Mutex<SharedProcessorInner<P>>>,
1863 mut array_rx: NDArrayReceiver,
1864 mut param_rx: tokio::sync::mpsc::UnboundedReceiver<PluginParamMsg>,
1865 plugin_params: PluginBaseParams,
1866 array_counter_reason: usize,
1867 enabled: Arc<AtomicBool>,
1868 blocking_mode: Arc<AtomicBool>,
1869 sender_port_name: String,
1870 initial_upstream: String,
1871 wiring: Arc<WiringRegistry>,
1872) {
1873 let enable_callbacks_reason = plugin_params.enable_callbacks;
1874 let blocking_callbacks_reason = plugin_params.blocking_callbacks;
1875 let min_callback_time_reason = plugin_params.min_callback_time;
1876 let sort_mode_reason = plugin_params.sort_mode;
1877 let sort_time_reason = plugin_params.sort_time;
1878 let sort_size_reason = plugin_params.sort_size;
1879 let nd_array_port_reason = plugin_params.nd_array_port;
1880 let nd_array_addr_reason = plugin_params.nd_array_addr;
1881 let process_plugin_reason = plugin_params.process_plugin;
1882 let max_byte_rate_reason = plugin_params.max_byte_rate;
1883 let num_threads_reason = plugin_params.num_threads;
1884 let max_threads_reason = plugin_params.max_threads;
1885 let array_callbacks_reason = shared.lock().ndarray_params.array_callbacks;
1886 let mut current_upstream = initial_upstream;
1890 let mut current_addr: i32 = 0;
1891 let rt = tokio::runtime::Builder::new_current_thread()
1892 .enable_all()
1893 .build()
1894 .unwrap();
1895 rt.block_on(async {
1896 let mut sort_flush_interval = tokio::time::interval(std::time::Duration::from_secs(3600));
1899 let mut sort_flush_active = false;
1900 let mut last_queue_free: Option<i32> = None;
1903 let mut scatter_cursor: usize = 0;
1908 let mut held_barriers: Vec<std::sync::mpsc::SyncSender<()>> = Vec::new();
1914
1915 loop {
1916 if !held_barriers.is_empty() && array_rx.pending() == 0 {
1920 for ack in held_barriers.drain(..) {
1921 let _ = ack.try_send(());
1922 }
1923 }
1924 tokio::select! {
1925 msg = array_rx.recv_msg() => {
1926 match msg {
1927 Some(msg) => {
1928 if !enabled.load(Ordering::Acquire) {
1933 continue;
1934 }
1935 let (process_output, senders, port) = {
1937 let mut guard = shared.lock();
1938 let compressed = msg.array.codec.is_some();
1942 let output = if compressed && !guard.compression_aware {
1943 guard
1944 .dropped_arrays
1945 .fetch_add(1, Ordering::AcqRel);
1946 Some(guard.dropped_arrays_only_batch())
1947 } else {
1948 guard.process_and_publish(&msg.array)
1953 };
1954 let senders = guard.output.lock().senders_clone();
1955 let port = guard.port_handle.clone();
1956 (output, senders, port)
1957 };
1958 let max_cap = array_rx.max_capacity();
1963 let free = max_cap.saturating_sub(array_rx.pending()) as i32;
1964 let queue_batch = if last_queue_free != Some(free) {
1965 last_queue_free = Some(free);
1966 Some(queue_status_batch(&plugin_params, max_cap, free))
1967 } else {
1968 None
1969 };
1970 if let Some(po) = process_output {
1973 po.publish_arrays(&senders, &mut scatter_cursor).await;
1974 po.batch.flush(&port).await;
1975 }
1976 if let Some(qb) = queue_batch {
1977 qb.flush(&port).await;
1978 }
1979 }
1980 None => break,
1981 }
1982 }
1983 param = param_rx.recv() => {
1984 match param {
1985 Some(PluginParamMsg::Barrier(ack)) => {
1991 held_barriers.push(ack);
1992 }
1993 Some(PluginParamMsg::Change(reason, addr, value)) => {
1994 if reason == enable_callbacks_reason {
1995 let on = value.as_i32() != 0;
1996 enabled.store(on, Ordering::Release);
1997 if !on {
2000 shared.lock().prev_input_array = None;
2001 }
2002 }
2003 if reason == blocking_callbacks_reason {
2004 blocking_mode.store(value.as_i32() != 0, Ordering::Release);
2005 }
2006 if reason == array_callbacks_reason {
2011 shared.lock().array_callbacks = value.as_i32() != 0;
2012 }
2013 if reason == min_callback_time_reason {
2015 shared.lock().min_callback_time = value.as_f64();
2016 }
2017 if reason == max_byte_rate_reason {
2020 let rate = value.as_f64();
2021 let mut guard = shared.lock();
2022 guard.max_byte_rate = rate;
2023 guard.throttler.reset(rate);
2024 }
2025 if reason == max_threads_reason {
2032 let (port, clamped, mt) = {
2034 let mut guard = shared.lock();
2035 guard.max_threads = value.as_i32().max(1);
2036 let clamped =
2037 guard.num_threads.clamp(1, guard.max_threads);
2038 guard.num_threads = clamped;
2039 (guard.port_handle.clone(), clamped, guard.max_threads)
2040 };
2041 clamp_writeback(&port, num_threads_reason, clamped).await;
2042 clamp_writeback(&port, max_threads_reason, mt).await;
2043 }
2044 if reason == num_threads_reason {
2045 let (port, clamped) = {
2046 let mut guard = shared.lock();
2047 let clamped =
2048 value.as_i32().clamp(1, guard.max_threads.max(1));
2049 guard.num_threads = clamped;
2050 (guard.port_handle.clone(), clamped)
2051 };
2052 clamp_writeback(&port, num_threads_reason, clamped).await;
2053 }
2054 if reason == nd_array_addr_reason {
2058 let new_addr = value.as_i32();
2059 if new_addr != current_addr {
2060 let old_key = upstream_key(¤t_upstream, current_addr);
2061 let new_key = upstream_key(¤t_upstream, new_addr);
2062 shared.lock().nd_array_addr = new_addr;
2063 match wiring.rewire_by_name(
2064 &sender_port_name,
2065 &old_key,
2066 &new_key,
2067 ) {
2068 Ok(()) => current_addr = new_addr,
2069 Err(e) => {
2070 eprintln!("NDArrayAddr reconnect failed: {e}");
2071 shared.lock().nd_array_addr = current_addr;
2072 }
2073 }
2074 }
2075 }
2076 if reason == process_plugin_reason && value.as_i32() != 0 {
2079 let (process_output, senders, port) = {
2080 let mut guard = shared.lock();
2081 let output = guard.process_plugin();
2082 let senders = guard.output.lock().senders_clone();
2083 let port = guard.port_handle.clone();
2084 (output, senders, port)
2085 };
2086 if let Some(po) = process_output {
2087 po.publish_arrays(&senders, &mut scatter_cursor).await;
2088 po.batch.flush(&port).await;
2089 } else {
2090 #[cfg(feature = "ioc")]
2104 if let Some(entry) =
2105 asyn_rs::asyn_record::get_port(&sender_port_name)
2106 {
2107 asyn_rs::asyn_trace!(
2108 entry.trace,
2109 sender_port_name.as_str(),
2110 asyn_rs::trace::TraceMask::WARNING,
2111 "plugin {sender_port_name}: ProcessPlugin \
2112 requested but no input array cached"
2113 );
2114 }
2115 }
2116 }
2117 if reason == array_counter_reason {
2121 shared.lock().array_counter = value.as_i32();
2122 }
2123 if reason == sort_mode_reason {
2125 let mode = value.as_i32();
2126 let flush_work = {
2129 let mut guard = shared.lock();
2130 guard.sort_mode = mode;
2131 if mode == 0 {
2132 let output = guard.flush_sort_buffer();
2133 let senders = guard.output.lock().senders_clone();
2134 let port = guard.port_handle.clone();
2135 sort_flush_active = false;
2136 Some((output, senders, port))
2137 } else {
2138 sort_flush_active = guard.sort_time > 0.0;
2139 if sort_flush_active {
2140 let dur = epics_libcom_rs::runtime::time::duration_from_secs(
2141 guard.sort_time,
2142 );
2143 sort_flush_interval = tokio::time::interval(dur);
2144 }
2145 None
2146 }
2147 };
2148 if let Some((output, senders, port)) = flush_work {
2149 output.publish_arrays(&senders, &mut scatter_cursor).await;
2150 output.batch.flush(&port).await;
2151 }
2152 }
2153 if reason == sort_time_reason {
2154 let t = value.as_f64();
2155 let mut guard = shared.lock();
2156 guard.sort_time = t;
2157 if guard.sort_mode != 0 && t > 0.0 {
2158 sort_flush_active = true;
2159 let dur = epics_libcom_rs::runtime::time::duration_from_secs(t);
2160 sort_flush_interval = tokio::time::interval(dur);
2161 } else {
2162 sort_flush_active = false;
2163 }
2164 drop(guard);
2165 }
2166 if reason == sort_size_reason {
2167 shared.lock().sort_size = value.as_i32();
2168 }
2169 if reason == nd_array_port_reason {
2171 if let Some(new_port) = value.as_string() {
2172 if new_port != current_upstream {
2173 let old_key =
2174 upstream_key(¤t_upstream, current_addr);
2175 let new_key = upstream_key(new_port, current_addr);
2176 match wiring.rewire_by_name(
2177 &sender_port_name,
2178 &old_key,
2179 &new_key,
2180 ) {
2181 Ok(()) => current_upstream = new_port.to_string(),
2182 Err(e) => {
2183 eprintln!("NDArrayPort rewire failed: {e}")
2184 }
2185 }
2186 }
2187 }
2188 }
2189 let snapshot = PluginParamSnapshot {
2190 enable_callbacks: enabled.load(Ordering::Acquire),
2191 reason,
2192 addr,
2193 value,
2194 };
2195 let (process_output, senders, port) = {
2196 let mut guard = shared.lock();
2197 let t0 = std::time::Instant::now();
2198 let result = guard.processor.on_param_change(reason, &snapshot);
2199 let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
2200 let output = if !result.output_arrays.is_empty() || !result.param_updates.is_empty() {
2201 let deliver = guard.array_callbacks;
2202 Some(guard.build_publish_batch(result.output_arrays, result.param_updates, false, None, elapsed_ms, deliver, true))
2203 } else {
2204 None
2205 };
2206 let senders = guard.output.lock().senders_clone();
2207 (output, senders, guard.port_handle.clone())
2208 };
2209 if let Some(po) = process_output {
2210 po.publish_arrays(&senders, &mut scatter_cursor).await;
2211 po.batch.flush(&port).await;
2212 }
2213 }
2214 None => break,
2215 }
2216 }
2217 _ = sort_flush_interval.tick(), if sort_flush_active => {
2218 let (output, senders, port) = {
2221 let mut guard = shared.lock();
2222 let output = guard.tick_sort_buffer();
2223 let senders = guard.output.lock().senders_clone();
2224 let port = guard.port_handle.clone();
2225 (output, senders, port)
2226 };
2227 output.publish_arrays(&senders, &mut scatter_cursor).await;
2228 output.batch.flush(&port).await;
2229 }
2230 }
2231 }
2232 });
2233}
2234
2235pub fn wire_downstream(upstream: &PluginRuntimeHandle, downstream_sender: NDArraySender) {
2242 upstream.array_output().lock().add(downstream_sender);
2243}
2244
2245pub fn create_plugin_runtime_with_output<P: NDPluginProcess>(
2247 port_name: &str,
2248 mut processor: P,
2249 pool: Arc<NDArrayPool>,
2250 queue_size: usize,
2251 output: NDArrayOutput,
2252 ndarray_port: &str,
2253 wiring: Arc<WiringRegistry>,
2254) -> (PluginRuntimeHandle, thread::JoinHandle<()>) {
2255 let (param_tx, param_rx) = tokio::sync::mpsc::unbounded_channel::<PluginParamMsg>();
2259 let handle_param_tx = param_tx.clone();
2260
2261 let plugin_type_name = processor.plugin_type().to_string();
2262 let compression_aware = processor.compression_aware();
2263 let does_array_callbacks = processor.does_array_callbacks();
2264 let array_data = processor.array_data_handle();
2265 let driver = PluginPortDriver::new(
2266 port_name,
2267 &plugin_type_name,
2268 queue_size,
2269 ndarray_port,
2270 1,
2271 param_tx,
2272 &mut processor,
2273 array_data,
2274 &pool,
2275 )
2276 .expect("failed to create plugin port driver");
2277
2278 let ndarray_params = driver.ndarray_params;
2279 let plugin_params = driver.plugin_params;
2280 let std_array_data_param = driver.std_array_data_param;
2281
2282 let (port_runtime, _actor_jh) = create_port_runtime(driver, RuntimeConfig::default())
2286 .unwrap_or_else(|e| port_runtime_unavailable(port_name, &e));
2287
2288 let port_handle = port_runtime.port_handle().clone();
2289
2290 let (array_sender, array_rx) = ndarray_channel(port_name, queue_size);
2291
2292 let enabled = Arc::new(AtomicBool::new(false));
2293 let blocking_mode = Arc::new(AtomicBool::new(false));
2294
2295 let array_output = Arc::new(parking_lot::Mutex::new(output));
2296 let array_output_for_handle = array_output.clone();
2297 wiring.register_output(port_name, array_output.clone());
2301 let dropped_arrays_counter = array_sender.dropped_arrays_counter().clone();
2303 let shared = Arc::new(parking_lot::Mutex::new(SharedProcessorInner {
2304 processor,
2305 output: array_output,
2306 pool,
2307 ndarray_params,
2308 plugin_params,
2309 port_handle,
2310 array_counter: 0,
2311 std_array_data_param,
2312 array_callbacks: does_array_callbacks,
2315 min_callback_time: 0.0,
2316 last_process_time: None,
2317 sort_mode: 0,
2318 sort_time: 0.0,
2319 sort_size: 10,
2320 sort_buffer: SortBuffer::new(),
2321 dropped_arrays: dropped_arrays_counter,
2322 compression_aware,
2323 max_byte_rate: 0.0,
2324 throttler: super::throttler::Throttler::new(0.0),
2325 prev_input_array: None,
2326 dims_prev: vec![0i32; crate::ndarray::ND_ARRAY_MAX_DIMS],
2327 nd_array_addr: PLUGIN_NDARRAY_ADDR,
2328 max_threads: PLUGIN_MAX_THREADS,
2329 num_threads: PLUGIN_NUM_THREADS,
2330 }));
2331
2332 let data_enabled = enabled.clone();
2333 let data_blocking = blocking_mode.clone();
2334
2335 let mut array_sender = array_sender;
2336 array_sender.set_mode_flags(enabled, blocking_mode);
2337
2338 let sender_port_name = port_name.to_string();
2340 let initial_upstream = ndarray_port.to_string();
2341
2342 let data_jh = MandatoryThread::new(
2343 format!("plugin-data-{port_name}"),
2344 ThreadPriority::Medium,
2348 StackSizeClass::Medium,
2351 )
2352 .spawn(move || {
2353 plugin_data_loop(
2354 shared,
2355 array_rx,
2356 param_rx,
2357 plugin_params,
2358 ndarray_params.array_counter,
2359 data_enabled,
2360 data_blocking,
2361 sender_port_name,
2362 initial_upstream,
2363 wiring,
2364 );
2365 });
2366
2367 let handle = PluginRuntimeHandle {
2368 port_runtime,
2369 array_sender,
2370 array_output: array_output_for_handle,
2371 port_name: port_name.to_string(),
2372 param_tx: handle_param_tx,
2373 ndarray_params,
2374 plugin_params,
2375 };
2376
2377 (handle, data_jh)
2378}
2379
2380#[cfg(test)]
2381mod tests {
2382 use super::*;
2383 use crate::ndarray::{NDDataType, NDDimension};
2384 use crate::plugin::channel::ndarray_channel;
2385
2386 #[test]
2417 fn plugin_data_threads_are_mandatory() {
2418 let prod = match include_str!("runtime.rs").find("\n#[cfg(test)]") {
2419 Some(i) => &include_str!("runtime.rs")[..i],
2420 None => include_str!("runtime.rs"),
2421 };
2422 assert_eq!(
2423 prod.matches("MandatoryThread::new(").count(),
2424 2,
2425 "`create_plugin_runtime_multi_addr` and `create_plugin_runtime_with_output`"
2426 );
2427 let bare = concat!("thread", "::Builder::new()");
2428 let strays: Vec<&str> = prod
2429 .lines()
2430 .map(str::trim)
2431 .filter(|l| !l.starts_with("//"))
2432 .filter(|l| l.contains(bare) || l.contains(concat!("thread", "::spawn(")))
2433 .collect();
2434 assert!(
2435 strays.is_empty(),
2436 "a plugin data thread created outside `MandatoryThread` resolves its \
2437 own spawn failure locally: {strays:?}"
2438 );
2439 }
2440
2441 struct PassthroughProcessor;
2443
2444 impl NDPluginProcess for PassthroughProcessor {
2445 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2446 ProcessResult::arrays(vec![Arc::new(array.clone())])
2447 }
2448 fn plugin_type(&self) -> &str {
2449 "Passthrough"
2450 }
2451 }
2452
2453 struct SinkProcessor {
2455 count: usize,
2456 }
2457
2458 impl NDPluginProcess for SinkProcessor {
2459 fn process_array(&mut self, _array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2460 self.count += 1;
2461 ProcessResult::empty()
2462 }
2463 fn plugin_type(&self) -> &str {
2464 "Sink"
2465 }
2466 }
2467
2468 fn make_test_array(id: i32) -> Arc<NDArray> {
2469 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2470 arr.unique_id = id;
2471 Arc::new(arr)
2472 }
2473
2474 fn test_wiring() -> Arc<WiringRegistry> {
2475 Arc::new(WiringRegistry::new())
2476 }
2477
2478 fn params_applied(handle: &PluginRuntimeHandle) {
2483 assert!(
2484 handle.wait_params_applied(std::time::Duration::from_secs(10)),
2485 "data thread did not apply queued param changes"
2486 );
2487 }
2488
2489 fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
2493 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2494 while !cond() {
2495 assert!(
2496 std::time::Instant::now() < deadline,
2497 "timed out waiting for {what}"
2498 );
2499 std::thread::sleep(std::time::Duration::from_millis(2));
2500 }
2501 }
2502
2503 fn enable_callbacks(handle: &PluginRuntimeHandle) {
2506 handle
2507 .port_runtime()
2508 .port_handle()
2509 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2510 .unwrap();
2511 params_applied(handle);
2512 }
2513
2514 fn send_array(sender: &NDArraySender, array: Arc<NDArray>) {
2518 let sender = sender.clone();
2519 let jh = std::thread::spawn(move || {
2520 let rt = tokio::runtime::Builder::new_current_thread()
2521 .enable_all()
2522 .build()
2523 .unwrap();
2524 rt.block_on(sender.publish(array));
2525 });
2526 jh.join().unwrap();
2527 }
2528
2529 #[test]
2530 fn test_passthrough_runtime() {
2531 let pool = Arc::new(NDArrayPool::new(1_000_000));
2532
2533 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2535 let mut output = NDArrayOutput::new();
2536 output.add(downstream_sender);
2537
2538 let (handle, _data_jh) = create_plugin_runtime_with_output(
2539 "PASS1",
2540 PassthroughProcessor,
2541 pool,
2542 10,
2543 output,
2544 "",
2545 test_wiring(),
2546 );
2547 enable_callbacks(&handle);
2548
2549 send_array(handle.array_sender(), make_test_array(42));
2551
2552 let received = downstream_rx.blocking_recv().unwrap();
2554 assert_eq!(received.unique_id, 42);
2555 }
2556
2557 #[test]
2558 fn test_sink_runtime() {
2559 let pool = Arc::new(NDArrayPool::new(1_000_000));
2560
2561 let (handle, _data_jh) = create_plugin_runtime(
2562 "SINK1",
2563 SinkProcessor { count: 0 },
2564 pool,
2565 10,
2566 "",
2567 test_wiring(),
2568 );
2569 enable_callbacks(&handle);
2570
2571 send_array(handle.array_sender(), make_test_array(1));
2573 send_array(handle.array_sender(), make_test_array(2));
2574
2575 let port = handle.port_runtime().port_handle().clone();
2577 let counter = handle.ndarray_params.array_counter;
2578 wait_until("sink to process both arrays", || {
2579 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 2)
2580 });
2581 assert_eq!(handle.port_name(), "SINK1");
2582 }
2583
2584 #[test]
2585 fn test_plugin_type_param() {
2586 let pool = Arc::new(NDArrayPool::new(1_000_000));
2587
2588 let (handle, _data_jh) = create_plugin_runtime(
2589 "TYPE_TEST",
2590 PassthroughProcessor,
2591 pool,
2592 10,
2593 "",
2594 test_wiring(),
2595 );
2596
2597 assert_eq!(handle.port_name(), "TYPE_TEST");
2599 assert_eq!(handle.port_runtime().port_name(), "TYPE_TEST");
2600 }
2601
2602 #[test]
2603 fn test_ndtimestamp_param_is_the_standalone_double() {
2604 let pool = Arc::new(NDArrayPool::new(1_000_000));
2611 let (ds, _rx) = ndarray_channel("DS_TS", 10);
2612 let mut output = NDArrayOutput::new();
2613 output.add(ds);
2614 let (handle, _jh) = create_plugin_runtime_with_output(
2615 "TS_PARAM",
2616 PassthroughProcessor,
2617 pool,
2618 10,
2619 output,
2620 "",
2621 test_wiring(),
2622 );
2623 enable_callbacks(&handle);
2624 let port = handle.port_runtime().port_handle().clone();
2625
2626 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
2627 arr.timestamp = crate::timestamp::EpicsTimestamp {
2628 sec: 1234,
2629 nsec: 5678,
2630 };
2631 arr.time_stamp = 100.5; send_array(handle.array_sender(), Arc::new(arr));
2633 std::thread::sleep(std::time::Duration::from_millis(50));
2634
2635 assert_eq!(
2636 port.read_float64_blocking(handle.ndarray_params.timestamp_rbv, 0)
2637 .unwrap(),
2638 100.5,
2639 "NDTimeStamp publishes pArray->timeStamp"
2640 );
2641 assert_eq!(
2642 port.read_int32_blocking(handle.ndarray_params.epics_ts_sec, 0)
2643 .unwrap(),
2644 1234
2645 );
2646 assert_eq!(
2647 port.read_int32_blocking(handle.ndarray_params.epics_ts_nsec, 0)
2648 .unwrap(),
2649 5678
2650 );
2651 }
2652
2653 #[test]
2654 fn test_shutdown_on_handle_drop() {
2655 let pool = Arc::new(NDArrayPool::new(1_000_000));
2656
2657 let (handle, data_jh) = create_plugin_runtime(
2658 "SHUTDOWN_TEST",
2659 PassthroughProcessor,
2660 pool,
2661 10,
2662 "",
2663 test_wiring(),
2664 );
2665
2666 let sender = handle.array_sender().clone();
2668 drop(handle);
2669 drop(sender);
2670
2671 let result = data_jh.join();
2673 assert!(result.is_ok());
2674 }
2675
2676 #[test]
2677 fn test_wire_to_nonzero_ndarray_addr() {
2678 use crate::plugin::wiring::upstream_key;
2684 let pool = Arc::new(NDArrayPool::new(1_000_000));
2685 let wiring = test_wiring();
2686
2687 let (up_handle, _up_jh) = create_plugin_runtime_multi_addr(
2689 "UP_MULTI",
2690 PassthroughProcessor,
2691 pool,
2692 10,
2693 "",
2694 wiring.clone(),
2695 2,
2696 );
2697 enable_callbacks(&up_handle);
2698
2699 let addr0 = wiring.lookup_output("UP_MULTI");
2701 let addr1 = wiring.lookup_output(&upstream_key("UP_MULTI", 1));
2702 assert!(addr0.is_some(), "addr 0 output must be registered");
2703 assert!(
2704 addr1.is_some(),
2705 "addr 1 output must be registered for a max_addr=2 port"
2706 );
2707
2708 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWN_ADDR1", 10);
2710 wiring
2711 .rewire(&downstream_sender, "", &upstream_key("UP_MULTI", 1))
2712 .expect("wiring a consumer to NDArrayAddr=1 must succeed");
2713
2714 send_array(up_handle.array_sender(), make_test_array(99));
2716 let received = downstream_rx.blocking_recv().unwrap();
2717 assert_eq!(
2718 received.unique_id, 99,
2719 "consumer wired to NDArrayAddr=1 must receive upstream arrays"
2720 );
2721 }
2722
2723 #[test]
2724 fn test_nonblocking_passthrough() {
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 "NB_TEST",
2732 PassthroughProcessor,
2733 pool,
2734 10,
2735 output,
2736 "",
2737 test_wiring(),
2738 );
2739 enable_callbacks(&handle);
2740
2741 send_array(handle.array_sender(), make_test_array(42));
2742
2743 let received = downstream_rx.blocking_recv().unwrap();
2744 assert_eq!(received.unique_id, 42);
2745 }
2746
2747 #[test]
2748 fn test_blocking_to_nonblocking_switch() {
2749 let pool = Arc::new(NDArrayPool::new(1_000_000));
2750 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2751 let mut output = NDArrayOutput::new();
2752 output.add(downstream_sender);
2753
2754 let (handle, _data_jh) = create_plugin_runtime_with_output(
2755 "SWITCH_TEST",
2756 PassthroughProcessor,
2757 pool,
2758 10,
2759 output,
2760 "",
2761 test_wiring(),
2762 );
2763 enable_callbacks(&handle);
2764
2765 handle
2767 .port_runtime()
2768 .port_handle()
2769 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 1)
2770 .unwrap();
2771 params_applied(&handle);
2772
2773 send_array(handle.array_sender(), make_test_array(1));
2774 let received = downstream_rx.blocking_recv().unwrap();
2775 assert_eq!(received.unique_id, 1);
2776
2777 handle
2779 .port_runtime()
2780 .port_handle()
2781 .write_int32_blocking(handle.plugin_params.blocking_callbacks, 0, 0)
2782 .unwrap();
2783 params_applied(&handle);
2784
2785 send_array(handle.array_sender(), make_test_array(2));
2787 let received = downstream_rx.blocking_recv().unwrap();
2788 assert_eq!(received.unique_id, 2);
2789 }
2790
2791 #[test]
2792 fn test_enable_callbacks_disables_processing() {
2793 let pool = Arc::new(NDArrayPool::new(1_000_000));
2794 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2795 let mut output = NDArrayOutput::new();
2796 output.add(downstream_sender);
2797
2798 let (handle, _data_jh) = create_plugin_runtime_with_output(
2799 "ENABLE_TEST",
2800 PassthroughProcessor,
2801 pool,
2802 10,
2803 output,
2804 "",
2805 test_wiring(),
2806 );
2807
2808 handle
2810 .port_runtime()
2811 .port_handle()
2812 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 0)
2813 .unwrap();
2814 params_applied(&handle);
2815
2816 send_array(handle.array_sender(), make_test_array(99));
2818
2819 let rt = tokio::runtime::Builder::new_current_thread()
2821 .enable_all()
2822 .build()
2823 .unwrap();
2824 let result = rt.block_on(async {
2825 tokio::time::timeout(std::time::Duration::from_millis(100), downstream_rx.recv()).await
2826 });
2827 assert!(
2828 result.is_err(),
2829 "should not receive array when callbacks disabled"
2830 );
2831 }
2832
2833 #[test]
2834 fn test_downstream_receives_multiple() {
2835 let pool = Arc::new(NDArrayPool::new(1_000_000));
2836
2837 let (ds1, mut rx1) = ndarray_channel("DS1", 10);
2838 let (ds2, mut rx2) = ndarray_channel("DS2", 10);
2839 let mut output = NDArrayOutput::new();
2840 output.add(ds1);
2841 output.add(ds2);
2842
2843 let (handle, _data_jh) = create_plugin_runtime_with_output(
2844 "DS_TEST",
2845 PassthroughProcessor,
2846 pool,
2847 10,
2848 output,
2849 "",
2850 test_wiring(),
2851 );
2852 enable_callbacks(&handle);
2853
2854 send_array(handle.array_sender(), make_test_array(77));
2855
2856 let r1 = rx1.blocking_recv().unwrap();
2858 let r2 = rx2.blocking_recv().unwrap();
2859 assert_eq!(r1.unique_id, 77);
2860 assert_eq!(r2.unique_id, 77);
2861 }
2862
2863 #[test]
2864 fn test_param_updates_after_send() {
2865 let pool = Arc::new(NDArrayPool::new(1_000_000));
2866
2867 struct ParamTracker;
2868 impl NDPluginProcess for ParamTracker {
2869 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
2870 ProcessResult::arrays(vec![Arc::new(array.clone())])
2871 }
2872 fn plugin_type(&self) -> &str {
2873 "ParamTracker"
2874 }
2875 }
2876
2877 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
2878 let mut output = NDArrayOutput::new();
2879 output.add(downstream_sender);
2880
2881 let (handle, _data_jh) = create_plugin_runtime_with_output(
2882 "PARAM_TEST",
2883 ParamTracker,
2884 pool,
2885 10,
2886 output,
2887 "",
2888 test_wiring(),
2889 );
2890 enable_callbacks(&handle);
2891
2892 send_array(handle.array_sender(), make_test_array(1));
2894 let received = downstream_rx.blocking_recv().unwrap();
2895 assert_eq!(received.unique_id, 1);
2896
2897 handle
2899 .port_runtime()
2900 .port_handle()
2901 .write_int32_blocking(handle.plugin_params.enable_callbacks, 0, 1)
2902 .unwrap();
2903 params_applied(&handle);
2904
2905 send_array(handle.array_sender(), make_test_array(2));
2907 let received = downstream_rx.blocking_recv().unwrap();
2908 assert_eq!(received.unique_id, 2);
2909 }
2910
2911 #[test]
2912 fn test_sort_buffer_reorders_by_unique_id() {
2913 let mut buf = SortBuffer::new();
2914
2915 buf.insert(3, vec![make_test_array(3)], 10);
2917 buf.insert(1, vec![make_test_array(1)], 10);
2918 buf.insert(2, vec![make_test_array(2)], 10);
2919
2920 assert_eq!(buf.len(), 3);
2921
2922 let drained = buf.drain_all();
2923 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2924 assert_eq!(ids, vec![1, 2, 3], "should drain in sorted uniqueId order");
2925 assert_eq!(buf.len(), 0);
2926 assert_eq!(buf.prev_unique_id, 3);
2927 }
2928
2929 #[test]
2930 fn test_sort_buffer_drain_ready_contiguous() {
2931 let mut buf = SortBuffer::new();
2934 buf.note_emitted(0);
2937 buf.insert(1, vec![make_test_array(1)], 10);
2938 buf.insert(2, vec![make_test_array(2)], 10);
2939 buf.insert(5, vec![make_test_array(5)], 10); let drained = buf.drain_ready(100.0);
2943 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2944 assert_eq!(ids, vec![1, 2], "contiguous run released; id=5 held by gap");
2945 assert_eq!(buf.len(), 1);
2946 }
2947
2948 #[test]
2949 fn test_sort_buffer_drain_ready_deadline() {
2950 let mut buf = SortBuffer::new();
2952 buf.note_emitted(1); buf.insert(5, vec![make_test_array(5)], 10); std::thread::sleep(std::time::Duration::from_millis(30));
2955 let drained = buf.drain_ready(0.01);
2957 let ids: Vec<i32> = drained.iter().map(|(id, _)| *id).collect();
2958 assert_eq!(ids, vec![5], "stale head released via deadline");
2959 }
2960
2961 #[test]
2962 fn test_sort_buffer_detects_disordered_on_emit() {
2963 let mut buf = SortBuffer::new();
2965 buf.note_emitted(5); buf.note_emitted(3); assert_eq!(buf.disordered_arrays, 1);
2968 buf.note_emitted(4); assert_eq!(buf.disordered_arrays, 1);
2970 }
2971
2972 #[test]
2973 fn test_sort_buffer_drops_when_full() {
2974 let mut buf = SortBuffer::new();
2975
2976 assert!(buf.insert(1, vec![make_test_array(1)], 2));
2978 assert!(buf.insert(2, vec![make_test_array(2)], 2));
2979 assert!(!buf.insert(3, vec![make_test_array(3)], 2));
2980
2981 assert_eq!(buf.len(), 2);
2982 assert_eq!(buf.dropped_output_arrays, 1);
2983 }
2984
2985 #[test]
2986 fn test_constructor_initialises_c_read_only_params() {
2987 let pool = Arc::new(NDArrayPool::new(1_000_000));
2994 let (handle, _data_jh) = create_plugin_runtime_with_output(
2995 "CTOR_TEST",
2996 PassthroughProcessor,
2997 pool,
2998 20,
2999 NDArrayOutput::new(),
3000 "",
3001 test_wiring(),
3002 );
3003 let port = handle.port_runtime().port_handle();
3004 let read = |reason: usize| port.read_int32_blocking(reason, 0);
3005
3006 assert_eq!(read(handle.plugin_params.queue_size).unwrap(), 20);
3008 assert_eq!(read(handle.plugin_params.queue_use).unwrap(), 20);
3009 assert_eq!(
3013 read(handle.plugin_params.nd_array_addr).unwrap(),
3014 PLUGIN_NDARRAY_ADDR
3015 );
3016 assert_eq!(
3017 read(handle.plugin_params.max_threads).unwrap(),
3018 PLUGIN_MAX_THREADS
3019 );
3020 assert_eq!(
3021 read(handle.plugin_params.num_threads).unwrap(),
3022 PLUGIN_NUM_THREADS
3023 );
3024 assert_eq!(read(handle.plugin_params.dropped_arrays).unwrap(), 0);
3026 assert_eq!(read(handle.plugin_params.dropped_output_arrays).unwrap(), 0);
3027 }
3028
3029 #[test]
3030 fn test_constructor_initialises_the_ndarray_read_only_block() {
3031 let pool = Arc::new(NDArrayPool::new(2_097_152));
3036 let (handle, _data_jh) = create_plugin_runtime_with_output(
3037 "NDCTOR_TEST",
3038 PassthroughProcessor,
3039 pool,
3040 20,
3041 NDArrayOutput::new(),
3042 "",
3043 test_wiring(),
3044 );
3045 let port = handle.port_runtime().port_handle();
3046 let p = &handle.ndarray_params;
3047 for (name, reason, want) in [
3048 ("ARRAY_SIZE_X", p.array_size_x, 0),
3049 ("ARRAY_SIZE_Y", p.array_size_y, 0),
3050 ("ARRAY_SIZE_Z", p.array_size_z, 0),
3051 ("ARRAY_SIZE", p.array_size, 0),
3052 ("ND_DIMENSIONS", p.n_dimensions, 0),
3053 (
3054 "COLOR_MODE",
3055 p.color_mode,
3056 crate::color::NDColorMode::Mono as i32,
3057 ),
3058 ("UNIQUE_ID", p.unique_id, 0),
3059 ("EPICS_TS_SEC", p.epics_ts_sec, 0),
3060 ("EPICS_TS_NSEC", p.epics_ts_nsec, 0),
3061 ("BAYER_PATTERN", p.bayer_pattern, 0),
3062 ("ARRAY_COUNTER", p.array_counter, 0),
3063 ("NUM_CAPTURED", p.num_captured, 0),
3064 ("FREE_CAPTURE", p.free_capture, 0),
3065 (
3066 "ND_ATTRIBUTES_STATUS",
3067 p.attributes_status,
3068 crate::driver::ndarray_driver::ATTR_STATUS_FILE_NOT_FOUND,
3069 ),
3070 ("NUM_QUEUED_ARRAYS", p.num_queued_arrays, 0),
3071 ("POOL_ALLOC_BUFFERS", p.pool_alloc_buffers, 0),
3072 ("POOL_FREE_BUFFERS", p.pool_free_buffers, 0),
3073 ] {
3074 assert_eq!(
3075 port.read_int32_blocking(reason, 0)
3076 .unwrap_or_else(|e| panic!("{name} unset after construction: {e:?}")),
3077 want,
3078 "{name}"
3079 );
3080 }
3081 assert_eq!(
3082 port.read_float64_blocking(p.pool_max_memory, 0)
3083 .expect("POOL_MAX_MEMORY unset after construction"),
3084 2.0
3085 );
3086 assert_eq!(
3087 port.read_float64_blocking(p.pool_used_memory, 0)
3088 .expect("POOL_USED_MEMORY unset after construction"),
3089 0.0
3090 );
3091 assert_eq!(
3092 port.read_float64_blocking(p.timestamp_rbv, 0)
3093 .expect("TIME_STAMP unset after construction"),
3094 0.0
3095 );
3096 }
3097
3098 #[test]
3099 fn test_sort_mode_runtime_integration() {
3100 let pool = Arc::new(NDArrayPool::new(1_000_000));
3101 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3102 let mut output = NDArrayOutput::new();
3103 output.add(downstream_sender);
3104
3105 let (handle, _data_jh) = create_plugin_runtime_with_output(
3106 "SORT_TEST",
3107 PassthroughProcessor,
3108 pool,
3109 10,
3110 output,
3111 "",
3112 test_wiring(),
3113 );
3114 enable_callbacks(&handle);
3115
3116 handle
3118 .port_runtime()
3119 .port_handle()
3120 .write_int32_blocking(handle.plugin_params.sort_size, 0, 10)
3121 .unwrap();
3122 handle
3123 .port_runtime()
3124 .port_handle()
3125 .write_float64_blocking(handle.plugin_params.sort_time, 0, 0.1)
3126 .unwrap();
3127 handle
3128 .port_runtime()
3129 .port_handle()
3130 .write_int32_blocking(handle.plugin_params.sort_mode, 0, 1)
3131 .unwrap();
3132 params_applied(&handle);
3133
3134 send_array(handle.array_sender(), make_test_array(1));
3137 send_array(handle.array_sender(), make_test_array(2));
3138 send_array(handle.array_sender(), make_test_array(3));
3139
3140 let rt = tokio::runtime::Builder::new_current_thread()
3141 .enable_all()
3142 .build()
3143 .unwrap();
3144 let fast = rt.block_on(async {
3145 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3146 });
3147 assert!(
3148 fast.is_ok(),
3149 "in-order arrays must be emitted immediately, not buffered"
3150 );
3151 assert_eq!(fast.unwrap().unwrap().unique_id, 1);
3152 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3153 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 3);
3154
3155 send_array(handle.array_sender(), make_test_array(5));
3159 send_array(handle.array_sender(), make_test_array(4));
3160 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 4);
3163 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 5);
3164 }
3165
3166 #[test]
3167 fn test_throttle_drops_output_arrays() {
3168 let pool = Arc::new(NDArrayPool::new(1_000_000));
3171 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3172 let mut output = NDArrayOutput::new();
3173 output.add(downstream_sender);
3174
3175 let (handle, _data_jh) = create_plugin_runtime_with_output(
3176 "THROTTLE_TEST",
3177 PassthroughProcessor,
3178 pool,
3179 10,
3180 output,
3181 "",
3182 test_wiring(),
3183 );
3184 enable_callbacks(&handle);
3185
3186 handle
3189 .port_runtime()
3190 .port_handle()
3191 .write_float64_blocking(handle.plugin_params.max_byte_rate, 0, 8.0)
3192 .unwrap();
3193 params_applied(&handle);
3194
3195 for id in 1..=5 {
3196 send_array(handle.array_sender(), make_test_array(id));
3197 }
3198 let port = handle.port_runtime().port_handle().clone();
3203 let counter = handle.ndarray_params.array_counter;
3204 wait_until("all 5 frames to be processed", || {
3205 port.read_int32_blocking(counter, 0).is_ok_and(|v| v == 5)
3206 });
3207
3208 let rt = tokio::runtime::Builder::new_current_thread()
3210 .enable_all()
3211 .build()
3212 .unwrap();
3213 let mut received = 0;
3214 while rt
3215 .block_on(async {
3216 tokio::time::timeout(std::time::Duration::from_millis(20), downstream_rx.recv())
3217 .await
3218 })
3219 .map(|o| o.is_some())
3220 .unwrap_or(false)
3221 {
3222 received += 1;
3223 }
3224 assert!(
3225 received < 5,
3226 "throttle must drop some arrays (got {received})"
3227 );
3228 assert!(received >= 1, "first array within budget must pass");
3229 }
3230
3231 #[test]
3232 fn test_process_plugin_reprocesses_last_input() {
3233 let pool = Arc::new(NDArrayPool::new(1_000_000));
3235 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3236 let mut output = NDArrayOutput::new();
3237 output.add(downstream_sender);
3238
3239 let (handle, _data_jh) = create_plugin_runtime_with_output(
3240 "PROCESS_PLUGIN_TEST",
3241 PassthroughProcessor,
3242 pool,
3243 10,
3244 output,
3245 "",
3246 test_wiring(),
3247 );
3248 enable_callbacks(&handle);
3249
3250 send_array(handle.array_sender(), make_test_array(7));
3251 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 7);
3252
3253 handle
3255 .port_runtime()
3256 .port_handle()
3257 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3258 .unwrap();
3259 let reprocessed = downstream_rx.blocking_recv().unwrap();
3260 assert_eq!(
3261 reprocessed.unique_id, 7,
3262 "ProcessPlugin re-emits last input"
3263 );
3264 }
3265
3266 #[test]
3267 fn test_min_callback_time_throttle_not_counted() {
3268 let pool = Arc::new(NDArrayPool::new(1_000_000));
3276 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3277 let mut output = NDArrayOutput::new();
3278 output.add(downstream_sender);
3279
3280 let (handle, _data_jh) = create_plugin_runtime_with_output(
3281 "MIN_CB_TEST",
3282 PassthroughProcessor,
3283 pool,
3284 10,
3285 output,
3286 "",
3287 test_wiring(),
3288 );
3289 enable_callbacks(&handle);
3290 let dropped = handle.array_sender().dropped_arrays_counter().clone();
3291
3292 handle
3294 .port_runtime()
3295 .port_handle()
3296 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3297 .unwrap();
3298 params_applied(&handle);
3299
3300 send_array(handle.array_sender(), make_test_array(1));
3301 send_array(handle.array_sender(), make_test_array(2));
3302
3303 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3304 params_applied(&handle);
3308 let rt = tokio::runtime::Builder::new_current_thread()
3309 .enable_all()
3310 .build()
3311 .unwrap();
3312 let second = rt.block_on(async {
3313 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3314 });
3315 assert!(
3316 second.is_err(),
3317 "second array throttled out by MinCallbackTime"
3318 );
3319 assert_eq!(
3320 dropped.load(Ordering::Acquire),
3321 0,
3322 "a MinCallbackTime-throttled frame must NOT increment DroppedArrays"
3323 );
3324 }
3325
3326 #[test]
3327 fn test_array_callbacks_zero_withholds_downstream_delivery() {
3328 let pool = Arc::new(NDArrayPool::new(1_000_000));
3334 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3335 let mut output = NDArrayOutput::new();
3336 output.add(downstream_sender);
3337
3338 let (handle, _data_jh) = create_plugin_runtime_with_output(
3339 "ARRAY_CB_TEST",
3340 PassthroughProcessor,
3341 pool,
3342 10,
3343 output,
3344 "",
3345 test_wiring(),
3346 );
3347 enable_callbacks(&handle);
3348 let port = handle.port_runtime().port_handle().clone();
3349
3350 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 0)
3352 .unwrap();
3353 params_applied(&handle);
3354
3355 send_array(handle.array_sender(), make_test_array(1));
3356 wait_until("frame 1 to be processed", || {
3359 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3360 .is_ok_and(|v| v == 1)
3361 });
3362
3363 let rt = tokio::runtime::Builder::new_current_thread()
3365 .enable_all()
3366 .build()
3367 .unwrap();
3368 let got = rt.block_on(async {
3369 tokio::time::timeout(std::time::Duration::from_millis(50), downstream_rx.recv()).await
3370 });
3371 assert!(
3372 got.is_err(),
3373 "NDArrayCallbacks=0 must withhold downstream delivery"
3374 );
3375 assert_eq!(
3377 port.read_int32_blocking(handle.ndarray_params.array_counter, 0)
3378 .unwrap(),
3379 1,
3380 "processing (and metadata params) must continue while delivery is off"
3381 );
3382
3383 port.write_int32_blocking(handle.ndarray_params.array_callbacks, 0, 1)
3385 .unwrap();
3386 params_applied(&handle);
3387 send_array(handle.array_sender(), make_test_array(2));
3388 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 2);
3389 }
3390
3391 #[test]
3392 fn test_plugin_output_publishes_compressed_size() {
3393 struct CompressProcessor;
3399 impl NDPluginProcess for CompressProcessor {
3400 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3401 let mut out = array.clone();
3402 out.codec = Some(crate::codec::Codec {
3403 name: crate::codec::CodecName::JPEG,
3404 compressed_size: 7,
3405 level: 0,
3406 shuffle: 0,
3407 compressor: 0,
3408 original_data_type: NDDataType::UInt8,
3409 });
3410 ProcessResult::arrays(vec![Arc::new(out)])
3411 }
3412 fn plugin_type(&self) -> &str {
3413 "Compress"
3414 }
3415 }
3416
3417 {
3419 let pool = Arc::new(NDArrayPool::new(1_000_000));
3420 let (ds, _rx) = ndarray_channel("DS_RAW", 10);
3421 let mut output = NDArrayOutput::new();
3422 output.add(ds);
3423 let (handle, _jh) = create_plugin_runtime_with_output(
3424 "CODEC_RAW",
3425 PassthroughProcessor,
3426 pool,
3427 10,
3428 output,
3429 "",
3430 test_wiring(),
3431 );
3432 enable_callbacks(&handle);
3433 let port = handle.port_runtime().port_handle().clone();
3434 send_array(handle.array_sender(), make_test_array(1));
3435 wait_until(
3438 "uncompressed output to publish CompressedSize == raw byte count",
3439 || {
3440 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3441 .is_ok_and(|v| v == 4)
3442 },
3443 );
3444 }
3445
3446 {
3448 let pool = Arc::new(NDArrayPool::new(1_000_000));
3449 let (ds, _rx) = ndarray_channel("DS_CMP", 10);
3450 let mut output = NDArrayOutput::new();
3451 output.add(ds);
3452 let (handle, _jh) = create_plugin_runtime_with_output(
3453 "CODEC_CMP",
3454 CompressProcessor,
3455 pool,
3456 10,
3457 output,
3458 "",
3459 test_wiring(),
3460 );
3461 enable_callbacks(&handle);
3462 let port = handle.port_runtime().port_handle().clone();
3463 send_array(handle.array_sender(), make_test_array(1));
3464 wait_until(
3465 "compressed output to publish CompressedSize == codec.compressed_size",
3466 || {
3467 port.read_int32_blocking(handle.ndarray_params.compressed_size, 0)
3468 .is_ok_and(|v| v == 7)
3469 },
3470 );
3471 }
3472 }
3473
3474 #[test]
3475 fn test_process_plugin_skips_throttled_input() {
3476 let pool = Arc::new(NDArrayPool::new(1_000_000));
3481 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3482 let mut output = NDArrayOutput::new();
3483 output.add(downstream_sender);
3484
3485 let (handle, _data_jh) = create_plugin_runtime_with_output(
3486 "PROCESS_THROTTLE_TEST",
3487 PassthroughProcessor,
3488 pool,
3489 10,
3490 output,
3491 "",
3492 test_wiring(),
3493 );
3494 enable_callbacks(&handle);
3495
3496 handle
3498 .port_runtime()
3499 .port_handle()
3500 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 10.0)
3501 .unwrap();
3502 params_applied(&handle);
3503
3504 send_array(handle.array_sender(), make_test_array(1));
3505 send_array(handle.array_sender(), make_test_array(2));
3506
3507 assert_eq!(downstream_rx.blocking_recv().unwrap().unique_id, 1);
3509 params_applied(&handle);
3513
3514 handle
3519 .port_runtime()
3520 .port_handle()
3521 .write_float64_blocking(handle.plugin_params.min_callback_time, 0, 0.0)
3522 .unwrap();
3523 handle
3527 .port_runtime()
3528 .port_handle()
3529 .write_int32_blocking(handle.plugin_params.process_plugin, 0, 1)
3530 .unwrap();
3531 let reprocessed = downstream_rx.blocking_recv().unwrap();
3532 assert_eq!(
3533 reprocessed.unique_id, 1,
3534 "ProcessPlugin must re-inject the last processed array (1), not the throttled array (2)"
3535 );
3536 }
3537
3538 #[test]
3539 fn test_g3_compressed_array_dropped_on_non_aware_plugin() {
3540 let pool = Arc::new(NDArrayPool::new(1_000_000));
3542 let (downstream_sender, mut downstream_rx) = ndarray_channel("DOWNSTREAM", 10);
3543 let mut output = NDArrayOutput::new();
3544 output.add(downstream_sender);
3545
3546 let (handle, _data_jh) = create_plugin_runtime_with_output(
3547 "G3_TEST",
3548 PassthroughProcessor, pool,
3550 10,
3551 output,
3552 "",
3553 test_wiring(),
3554 );
3555 enable_callbacks(&handle);
3556
3557 let mut compressed = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
3559 compressed.unique_id = 1;
3560 compressed.codec = Some(crate::codec::Codec {
3561 name: crate::codec::CodecName::JPEG,
3562 compressed_size: 16,
3563 level: 0,
3564 shuffle: 0,
3565 compressor: 0,
3566 original_data_type: NDDataType::UInt8,
3567 });
3568 send_array(handle.array_sender(), Arc::new(compressed));
3569
3570 send_array(handle.array_sender(), make_test_array(2));
3572
3573 let r = downstream_rx.blocking_recv().unwrap();
3574 assert_eq!(
3575 r.unique_id, 2,
3576 "compressed array dropped; only the raw array reaches downstream"
3577 );
3578 }
3579
3580 #[test]
3581 fn test_drop_on_full_increments_dropped_counter() {
3582 struct SlowProcessor;
3586 impl NDPluginProcess for SlowProcessor {
3587 fn process_array(&mut self, _a: &NDArray, _p: &NDArrayPool) -> ProcessResult {
3588 std::thread::sleep(std::time::Duration::from_millis(200));
3589 ProcessResult::empty()
3590 }
3591 fn plugin_type(&self) -> &str {
3592 "Slow"
3593 }
3594 }
3595 let pool = Arc::new(NDArrayPool::new(1_000_000));
3596
3597 let (downstream_handle, _ds_jh) =
3599 create_plugin_runtime("B1_DOWNSTREAM", SlowProcessor, pool, 1, "", test_wiring());
3600 enable_callbacks(&downstream_handle);
3601 let ds_sender = downstream_handle.array_sender().clone();
3602 let dropped = ds_sender.dropped_arrays_counter().clone();
3603
3604 send_array(&ds_sender, make_test_array(1));
3607 send_array(&ds_sender, make_test_array(2));
3608 send_array(&ds_sender, make_test_array(3));
3609 send_array(&ds_sender, make_test_array(4));
3610
3611 assert!(
3612 dropped.load(Ordering::Acquire) >= 1,
3613 "arrays dropped on a full queue must be counted (got {})",
3614 dropped.load(Ordering::Acquire)
3615 );
3616 }
3617
3618 #[test]
3619 fn test_cross_width_narrowing_array_read_truncates() {
3620 let mut out = [0i8; 1];
3629 let n = copy_ccast(&[300u16], &mut out);
3630 assert_eq!(n, 1);
3631 assert_eq!(out[0], 44, "(epicsInt8)(epicsUInt16)300 == 44 (low 8 bits)");
3632 let mut sat = [0i8; 1];
3634 copy_convert(&[300u16], &mut sat);
3635 assert_eq!(sat[0], 127, "f64 round-trip saturates — the wrong behavior");
3636
3637 let mut out2 = [0i8; 1];
3639 copy_ccast(&[0x1234_5678i32], &mut out2);
3640 assert_eq!(out2[0], 0x78);
3641
3642 let mut out3 = [0i8; 1];
3644 copy_ccast(&[-1i32], &mut out3);
3645 assert_eq!(out3[0], -1);
3646
3647 let mut out4 = [0i8; 1];
3649 copy_ccast(&[255u16], &mut out4);
3650 assert_eq!(out4[0], -1);
3651
3652 let mut out5 = [0i32; 1];
3654 copy_ccast(&[0x0000_0001_0000_002Ai64], &mut out5);
3655 assert_eq!(out5[0], 42);
3656
3657 let mut out6 = [0i16; 1];
3659 copy_ccast(&[70000u32], &mut out6);
3660 assert_eq!(out6[0], 4464);
3661
3662 let mut out7 = [0i8; 1];
3665 copy_ccast(&[255u8], &mut out7);
3666 assert_eq!(out7[0], -1);
3667
3668 let mut fout = [0i32; 1];
3674 copy_convert(&[42.9f64], &mut fout);
3675 assert_eq!(fout[0], 42, "f64 -> i32 truncates toward zero");
3676 }
3677
3678 fn block<F: std::future::Future>(f: F) -> F::Output {
3682 tokio::runtime::Builder::new_current_thread()
3683 .enable_all()
3684 .build()
3685 .unwrap()
3686 .block_on(f)
3687 }
3688
3689 #[test]
3690 fn test_scatter_reroutes_past_full_consumer() {
3691 let (sa, mut ra) = ndarray_channel("A", 1);
3696 let (sb, mut rb) = ndarray_channel("B", 1);
3697 let (sc, _rc) = ndarray_channel("C", 1);
3698 block(async {
3699 assert_eq!(
3700 sa.publish(make_test_array(99)).await,
3701 PublishOutcome::Delivered
3702 );
3703 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3704 let mut cursor = 0usize;
3705 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3706 assert_eq!(cursor, 2);
3708 assert_eq!(rb.recv().await.unwrap().unique_id, 1);
3709 assert_eq!(ra.recv().await.unwrap().unique_id, 99);
3711 assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3712 });
3713 }
3714
3715 #[test]
3716 fn test_scatter_drops_on_last_when_all_full_counts_once() {
3717 let (sa, mut ra) = ndarray_channel("A", 1);
3721 let (sb, mut rb) = ndarray_channel("B", 1);
3722 block(async {
3723 sa.publish(make_test_array(91)).await;
3724 sb.publish(make_test_array(92)).await;
3725 let senders = vec![sa.clone(), sb.clone()];
3726 let mut cursor = 0usize;
3727 ProcessOutput::scatter_publish(&make_test_array(7), &senders, &mut cursor).await;
3728 assert_eq!(cursor, 2); assert_eq!(sa.dropped_arrays_counter().load(Ordering::Acquire), 0);
3731 assert_eq!(sb.dropped_arrays_counter().load(Ordering::Acquire), 1);
3732 assert_eq!(ra.recv().await.unwrap().unique_id, 91);
3734 assert_eq!(rb.recv().await.unwrap().unique_id, 92);
3735 });
3736 }
3737
3738 #[test]
3739 fn test_scatter_cursor_advances_per_attempt_across_frames() {
3740 let (sa, _ra) = ndarray_channel("A", 1);
3745 let (sb, mut rb) = ndarray_channel("B", 10);
3746 let (sc, mut rc) = ndarray_channel("C", 10);
3747 block(async {
3748 sa.publish(make_test_array(90)).await; let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3750 let mut cursor = 0usize;
3751 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3752 assert_eq!(cursor, 2); assert_eq!(rb.recv().await.unwrap().unique_id, 0);
3754 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3755 assert_eq!(cursor, 3); assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3757 });
3758 }
3759
3760 #[test]
3761 fn test_scatter_skips_disabled_consumer() {
3762 let (sa, mut ra) = ndarray_channel("A", 10);
3765 let (mut sb, _rb) = ndarray_channel("B", 10);
3766 let (sc, mut rc) = ndarray_channel("C", 10);
3767 sb.set_mode_flags(
3768 Arc::new(AtomicBool::new(false)),
3769 Arc::new(AtomicBool::new(false)),
3770 );
3771 block(async {
3772 let senders = vec![sa.clone(), sb.clone(), sc.clone()];
3773 let mut cursor = 0usize;
3774 ProcessOutput::scatter_publish(&make_test_array(0), &senders, &mut cursor).await;
3776 ProcessOutput::scatter_publish(&make_test_array(1), &senders, &mut cursor).await;
3777 assert_eq!(ra.recv().await.unwrap().unique_id, 0);
3778 assert_eq!(rc.recv().await.unwrap().unique_id, 1);
3779 });
3780 }
3781}