1#![cfg(not(target_arch = "wasm32"))]
19
20use crate::wal::WriteAheadLog;
21use crate::NQuin;
22use core_affinity::CoreId;
23use crossbeam_channel::{bounded, Receiver, Sender};
24use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
25use std::sync::{Arc, Mutex};
26use std::thread;
27use std::time::{Duration, Instant};
28
29const WORKER_MEMORY_BOUNDARY: usize = 512 * 1024 * 1024;
31
32const JOB_QUEUE_CAPACITY: usize = 4096;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum JobStatus {
38 Pending,
39 InProgress,
40 Completed,
41 Paused,
42 Failed,
43}
44
45#[derive(Debug, Clone)]
47pub struct Job {
48 pub quin: NQuin,
49 pub status: JobStatus,
50 pub total_bytes: u64,
51 pub dispatched_at: Option<Instant>,
52 pub completed_at: Option<Instant>,
53 pub compute_target: ComputeTarget,
54}
55
56impl Job {
57 pub fn new(quin: NQuin, total_bytes: u64) -> Self {
58 Self {
59 quin,
60 status: JobStatus::Pending,
61 total_bytes,
62 dispatched_at: None,
63 completed_at: None,
64 compute_target: ComputeTarget::Cpu, }
66 }
67
68 pub fn with_target(quin: NQuin, total_bytes: u64, compute_target: ComputeTarget) -> Self {
69 Self {
70 quin,
71 status: JobStatus::Pending,
72 total_bytes,
73 dispatched_at: None,
74 completed_at: None,
75 compute_target,
76 }
77 }
78
79 pub fn progress(&self) -> f64 {
81 if self.total_bytes == 0 {
82 return 0.0;
83 }
84 let current_offset = self.quin.object;
85 (current_offset as f64 / self.total_bytes as f64) * 100.0
86 }
87
88 pub fn velocity(&self) -> f64 {
90 match (self.dispatched_at, self.completed_at) {
91 (Some(dispatched), Some(completed)) => {
92 let duration = completed.duration_since(dispatched).as_secs_f64();
93 if duration > 0.0 {
94 self.quin.object as f64 / duration
95 } else {
96 0.0
97 }
98 }
99 _ => 0.0,
100 }
101 }
102}
103
104pub struct WorkerCell {
106 pub cell_id: usize,
107 pub core_id: CoreId,
108 pub memory_boundary: usize,
109 pub job_receiver: Receiver<Job>,
110 pub result_sender: Sender<Job>,
111 pub shutdown_signal: Arc<AtomicBool>,
112 pub pause_signal: Arc<AtomicBool>,
113}
114
115impl WorkerCell {
116 pub fn new(
117 cell_id: usize,
118 core_id: CoreId,
119 job_receiver: Receiver<Job>,
120 result_sender: Sender<Job>,
121 shutdown_signal: Arc<AtomicBool>,
122 pause_signal: Arc<AtomicBool>,
123 ) -> Self {
124 Self {
125 cell_id,
126 core_id,
127 memory_boundary: WORKER_MEMORY_BOUNDARY,
128 job_receiver,
129 result_sender,
130 shutdown_signal,
131 pause_signal,
132 }
133 }
134
135 pub fn run(self) {
137 let pinned = core_affinity::set_for_current(self.core_id);
139 if !pinned {
140 log::error!(
141 "Failed to pin worker {} to core {:?}",
142 self.cell_id,
143 self.core_id
144 );
145 } else {
146 log::info!("Worker {} pinned to core {:?}", self.cell_id, self.core_id);
147 }
148
149 log::info!("Worker {} starting execution loop", self.cell_id);
150
151 while !self.shutdown_signal.load(Ordering::Relaxed) {
152 if self.pause_signal.load(Ordering::Relaxed) {
154 log::info!("Worker {} paused (environmental yielding)", self.cell_id);
155 std::thread::sleep(Duration::from_millis(100));
156 continue;
157 }
158
159 match self.job_receiver.recv_timeout(Duration::from_millis(100)) {
161 Ok(mut job) => {
162 log::debug!(
163 "Worker {} received job at offset {}",
164 self.cell_id,
165 job.quin.object
166 );
167
168 job.status = JobStatus::InProgress;
169 job.dispatched_at = Some(Instant::now());
170
171 self.process_job(&mut job);
173
174 job.status = JobStatus::Completed;
175 job.completed_at = Some(Instant::now());
176
177 if let Err(e) = self.result_sender.send(job) {
179 log::error!("Worker {} failed to send result: {}", self.cell_id, e);
180 }
181 }
182 Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
183 continue;
185 }
186 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
187 log::info!(
188 "Worker {} job channel disconnected, shutting down",
189 self.cell_id
190 );
191 break;
192 }
193 }
194 }
195
196 log::info!("Worker {} shutting down", self.cell_id);
197 }
198
199 fn process_job(&self, job: &mut Job) {
201 let chunk_size = 4096; job.quin.object = job.quin.object.saturating_add(chunk_size);
205
206 std::thread::sleep(Duration::from_millis(10));
208
209 log::trace!(
210 "Worker {} processed chunk to offset {}",
211 self.cell_id,
212 job.quin.object
213 );
214 }
215}
216
217pub struct ProductionQueue {
219 num_workers: usize,
220 job_sender: Sender<Job>,
221 result_receiver: Receiver<Job>,
222 shutdown_signal: Arc<AtomicBool>,
223 pause_signal: Arc<AtomicBool>,
224 active_jobs: Arc<AtomicU64>,
225 completed_jobs: Arc<AtomicU64>,
226 wal: Arc<Mutex<Option<WriteAheadLog>>>,
227 wal_path: String,
228 estimator: Arc<Mutex<JobEstimator>>,
229}
230
231impl ProductionQueue {
232 pub fn new() -> Self {
234 Self::with_wal_path("jobs.wal")
235 }
236
237 pub fn with_wal_path(wal_path: &str) -> Self {
239 let num_logical_cores = num_cpus::get();
240 log::info!(
241 "Initializing production queue with {} workers",
242 num_logical_cores
243 );
244
245 let (job_sender, job_receiver) = bounded(JOB_QUEUE_CAPACITY);
246 let (result_sender, result_receiver) = bounded(JOB_QUEUE_CAPACITY);
247 let shutdown_signal = Arc::new(AtomicBool::new(false));
248 let pause_signal = Arc::new(AtomicBool::new(false));
249 let active_jobs = Arc::new(AtomicU64::new(0));
250 let completed_jobs = Arc::new(AtomicU64::new(0));
251
252 let wal = Arc::new(Mutex::new(WriteAheadLog::open(wal_path).ok()));
254 if wal.lock().unwrap().is_some() {
255 log::info!("Production queue WAL initialized at {}", wal_path);
256 } else {
257 log::warn!(
258 "Failed to initialize WAL at {}, job persistence disabled",
259 wal_path
260 );
261 }
262
263 let estimator = Arc::new(Mutex::new(JobEstimator::new()));
265 log::info!("Job estimator initialized");
266
267 let mut worker_senders = Vec::new();
268 let mut worker_receivers = Vec::new();
269 for _ in 0..num_logical_cores {
270 let (worker_job_sender, worker_job_receiver) = bounded(16);
271 worker_senders.push(worker_job_sender);
272 worker_receivers.push(worker_job_receiver);
273 }
274
275 let dispatch_targets = worker_senders.clone();
277 let dispatcher_active = active_jobs.clone();
278 let dispatcher_shutdown = shutdown_signal.clone();
279 thread::spawn(move || {
280 while !dispatcher_shutdown.load(Ordering::Relaxed) {
281 match job_receiver.recv_timeout(Duration::from_millis(100)) {
282 Ok(job) => {
283 let worker_id = dispatcher_active.fetch_add(1, Ordering::Relaxed) as usize
284 % num_logical_cores;
285 if dispatch_targets[worker_id].send(job).is_err() {
286 dispatcher_active.fetch_sub(1, Ordering::Relaxed);
287 }
288 }
289 Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
290 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
291 }
292 }
293 });
294
295 for (worker_id, worker_job_receiver) in worker_receivers.into_iter().enumerate() {
296 let core_id = CoreId {
297 id: worker_id % num_logical_cores,
298 };
299 let worker_result_sender = result_sender.clone();
300 let worker_shutdown = shutdown_signal.clone();
301 let worker_pause = pause_signal.clone();
302
303 let worker = WorkerCell::new(
304 worker_id,
305 core_id,
306 worker_job_receiver,
307 worker_result_sender,
308 worker_shutdown,
309 worker_pause,
310 );
311
312 thread::spawn(move || {
313 worker.run();
314 });
315 }
316
317 Self {
318 num_workers: num_logical_cores,
319 job_sender,
320 result_receiver,
321 shutdown_signal,
322 pause_signal,
323 active_jobs,
324 completed_jobs,
325 wal,
326 wal_path: wal_path.to_string(),
327 estimator,
328 }
329 }
330
331 pub fn submit_job(&self, job: Job) -> Result<(), String> {
333 if let Ok(mut wal_guard) = self.wal.lock() {
335 if let Some(ref mut wal) = wal_guard.as_mut() {
336 if let Err(e) = wal.append_mutation(&job.quin) {
337 log::warn!("Failed to persist job to WAL: {}", e);
338 }
339 }
340 }
341
342 if let Err(e) = self.job_sender.send(job) {
343 return Err(format!("Failed to enqueue job: {}", e));
344 }
345
346 log::debug!("Job enqueued for dispatcher");
347 Ok(())
348 }
349
350 pub fn wal_path(&self) -> &str {
352 &self.wal_path
353 }
354
355 pub fn recover_jobs(&mut self) -> Result<Vec<Job>, String> {
357 if let Ok(mut wal_guard) = self.wal.lock() {
358 if let Some(ref mut wal) = wal_guard.as_mut() {
359 match wal.recover() {
360 Ok(quins) => {
361 let jobs: Vec<Job> = quins
362 .into_iter()
363 .map(|quin| Job::new(quin, 10000)) .collect();
365 log::info!(
366 "Recovered {} jobs from WAL at {}",
367 jobs.len(),
368 self.wal_path()
369 );
370 return Ok(jobs);
371 }
372 Err(e) => return Err(format!("Failed to recover jobs from WAL: {}", e)),
373 }
374 }
375 }
376 Ok(Vec::new())
377 }
378
379 pub fn persist_job_completion(&self, job: &Job) -> Result<(), String> {
381 if let Ok(mut wal_guard) = self.wal.lock() {
382 if let Some(ref mut wal) = wal_guard.as_mut() {
383 if let Err(e) = wal.append_mutation(&job.quin) {
384 return Err(format!("Failed to persist job completion to WAL: {}", e));
385 }
386 }
387 }
388 Ok(())
389 }
390
391 pub fn collect_results(&self) -> Vec<Job> {
393 let mut results = Vec::new();
394
395 while let Ok(job) = self.result_receiver.try_recv() {
396 self.completed_jobs.fetch_add(1, Ordering::Relaxed);
397 self.active_jobs.fetch_sub(1, Ordering::Relaxed);
398
399 if let Err(e) = self.persist_job_completion(&job) {
401 log::warn!("Failed to persist job completion: {}", e);
402 }
403
404 results.push(job);
405 }
406
407 results
408 }
409
410 pub fn stats(&self) -> QueueStats {
412 QueueStats {
413 num_workers: self.num_workers,
414 active_jobs: self.active_jobs.load(Ordering::Relaxed),
415 completed_jobs: self.completed_jobs.load(Ordering::Relaxed),
416 }
417 }
418
419 pub fn shutdown(&self) {
421 log::info!("Shutting down production queue");
422 self.shutdown_signal.store(true, Ordering::Relaxed);
423
424 std::thread::sleep(Duration::from_secs(2));
426
427 log::info!("Production queue shutdown complete");
428 }
429
430 pub fn pause(&self) {
432 log::info!("Pausing production queue (environmental yielding)");
433 self.pause_signal.store(true, Ordering::Relaxed);
434 }
435
436 pub fn resume(&self) {
438 log::info!("Resuming production queue");
439 self.pause_signal.store(false, Ordering::Relaxed);
440 }
441
442 pub fn is_paused(&self) -> bool {
444 self.pause_signal.load(Ordering::Relaxed)
445 }
446
447 pub fn estimate_job(&self, params: &JobEstimateParams) -> JobEstimate {
449 if let Ok(estimator) = self.estimator.lock() {
450 estimator.estimate_job_duration(params)
451 } else {
452 JobEstimate {
454 estimated_duration: Duration::from_secs(0),
455 confidence: 0.0,
456 compute_target: params.compute_target,
457 workload_bytes: params.workload_bytes,
458 }
459 }
460 }
461
462 pub fn record_performance(
464 &self,
465 target: ComputeTarget,
466 bytes_processed: u64,
467 duration: Duration,
468 ) {
469 if let Ok(mut estimator) = self.estimator.lock() {
470 estimator.record_performance(target, bytes_processed, duration);
471 }
472 }
473
474 pub fn set_power_throttle_factor(&self, factor: f64) {
476 if let Ok(mut estimator) = self.estimator.lock() {
477 estimator.set_power_throttle_factor(factor);
478 log::info!("Power throttle factor set to {}", factor);
479 }
480 }
481
482 pub fn get_power_throttle_factor(&self) -> f64 {
484 if let Ok(estimator) = self.estimator.lock() {
485 estimator.get_power_throttle_factor()
486 } else {
487 1.0
488 }
489 }
490
491 pub fn calculate_telemetry(&self, total_job_bytes: u64) -> QueueTelemetry {
493 let active = self.active_jobs.load(Ordering::Relaxed);
494 let completed = self.completed_jobs.load(Ordering::Relaxed);
495 let total = active + completed;
496
497 let overall_progress = if total > 0 {
499 (completed as f64 / total as f64) * 100.0
500 } else {
501 0.0
502 };
503
504 let results = self.collect_results();
506 let total_velocity: f64 = results.iter().map(|j| j.velocity()).sum();
507 let avg_velocity = if !results.is_empty() {
508 total_velocity / results.len() as f64
509 } else {
510 0.0
511 };
512
513 let estimated_time_remaining = if avg_velocity > 0.0 && total_job_bytes > 0 {
515 let remaining_bytes =
516 total_job_bytes.saturating_sub(results.iter().map(|j| j.quin.object).sum::<u64>());
517 Some(Duration::from_secs_f64(
518 remaining_bytes as f64 / avg_velocity,
519 ))
520 } else {
521 None
522 };
523
524 QueueTelemetry {
525 total_jobs: total,
526 active_jobs: active,
527 completed_jobs: completed,
528 overall_progress,
529 estimated_time_remaining,
530 bytes_per_second: avg_velocity,
531 }
532 }
533}
534
535impl Drop for ProductionQueue {
536 fn drop(&mut self) {
537 self.shutdown();
538 }
539}
540
541#[derive(Debug, Clone, Copy)]
543pub struct QueueStats {
544 pub num_workers: usize,
545 pub active_jobs: u64,
546 pub completed_jobs: u64,
547}
548
549#[derive(Debug, Clone)]
551pub struct QueueTelemetry {
552 pub total_jobs: u64,
553 pub active_jobs: u64,
554 pub completed_jobs: u64,
555 pub overall_progress: f64, pub estimated_time_remaining: Option<Duration>,
557 pub bytes_per_second: f64,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum ComputeTarget {
563 Cpu,
564 WebGpu,
565 DirectMl,
566}
567
568#[derive(Debug, Clone)]
570pub struct HardwareVelocity {
571 pub target: ComputeTarget,
572 pub bytes_per_second: f64,
573 pub sample_count: u64,
574 pub last_updated: Option<Instant>,
575}
576
577impl HardwareVelocity {
578 pub fn new(target: ComputeTarget) -> Self {
579 Self {
580 target,
581 bytes_per_second: 0.0,
582 sample_count: 0,
583 last_updated: None,
584 }
585 }
586
587 pub fn update(&mut self, bytes_processed: u64, duration: Duration) {
589 let duration_secs = duration.as_secs_f64();
590 if duration_secs > 0.0 {
591 let new_velocity = bytes_processed as f64 / duration_secs;
592
593 let alpha = 0.2;
595 if self.sample_count == 0 {
596 self.bytes_per_second = new_velocity;
597 } else {
598 self.bytes_per_second =
599 alpha * new_velocity + (1.0 - alpha) * self.bytes_per_second;
600 }
601
602 self.sample_count += 1;
603 self.last_updated = Some(Instant::now());
604 }
605 }
606
607 pub fn get_velocity(&self) -> f64 {
609 if self.sample_count == 0 {
610 match self.target {
612 ComputeTarget::Cpu => 10_000_000.0, ComputeTarget::WebGpu => 100_000_000.0, ComputeTarget::DirectMl => 500_000_000.0, }
616 } else {
617 self.bytes_per_second
618 }
619 }
620}
621
622#[derive(Debug, Clone)]
624pub struct JobEstimateParams {
625 pub workload_bytes: u64,
626 pub step_size: f32,
627 pub compute_target: ComputeTarget,
628 pub contention_factor: f64, }
630
631#[derive(Debug, Clone)]
633pub struct JobEstimate {
634 pub estimated_duration: Duration,
635 pub confidence: f64, pub compute_target: ComputeTarget,
637 pub workload_bytes: u64,
638}
639
640pub struct JobEstimator {
642 cpu_velocity: HardwareVelocity,
643 webgpu_velocity: HardwareVelocity,
644 directml_velocity: HardwareVelocity,
645 power_throttle_factor: f64, }
647
648impl JobEstimator {
649 pub fn new() -> Self {
650 Self {
651 cpu_velocity: HardwareVelocity::new(ComputeTarget::Cpu),
652 webgpu_velocity: HardwareVelocity::new(ComputeTarget::WebGpu),
653 directml_velocity: HardwareVelocity::new(ComputeTarget::DirectMl),
654 power_throttle_factor: 1.0, }
656 }
657
658 pub fn estimate_job_duration(&self, params: &JobEstimateParams) -> JobEstimate {
660 let velocity = match params.compute_target {
662 ComputeTarget::Cpu => self.cpu_velocity.get_velocity(),
663 ComputeTarget::WebGpu => self.webgpu_velocity.get_velocity(),
664 ComputeTarget::DirectMl => self.directml_velocity.get_velocity(),
665 };
666
667 let adjusted_velocity = velocity / self.power_throttle_factor;
669
670 let effective_velocity = adjusted_velocity / params.contention_factor;
672
673 let workload_points = if params.step_size > 0.0 {
675 params.workload_bytes as f64 / params.step_size as f64
676 } else {
677 params.workload_bytes as f64
678 };
679
680 let overhead_secs = match params.compute_target {
682 ComputeTarget::Cpu => 0.005, ComputeTarget::WebGpu => 0.250, ComputeTarget::DirectMl => 0.100, };
686
687 let duration_secs = (workload_points / effective_velocity) + overhead_secs;
689 let estimated_duration = Duration::from_secs_f64(duration_secs.max(0.0));
690
691 let sample_count = match params.compute_target {
693 ComputeTarget::Cpu => self.cpu_velocity.sample_count,
694 ComputeTarget::WebGpu => self.webgpu_velocity.sample_count,
695 ComputeTarget::DirectMl => self.directml_velocity.sample_count,
696 };
697
698 let confidence = (sample_count as f64 / (sample_count as f64 + 10.0)).min(0.95);
700
701 JobEstimate {
702 estimated_duration,
703 confidence,
704 compute_target: params.compute_target,
705 workload_bytes: params.workload_bytes,
706 }
707 }
708
709 pub fn record_performance(
711 &mut self,
712 target: ComputeTarget,
713 bytes_processed: u64,
714 duration: Duration,
715 ) {
716 match target {
717 ComputeTarget::Cpu => self.cpu_velocity.update(bytes_processed, duration),
718 ComputeTarget::WebGpu => self.webgpu_velocity.update(bytes_processed, duration),
719 ComputeTarget::DirectMl => self.directml_velocity.update(bytes_processed, duration),
720 }
721 }
722
723 pub fn set_power_throttle_factor(&mut self, factor: f64) {
725 self.power_throttle_factor = factor.max(0.5).min(2.0); }
727
728 pub fn get_power_throttle_factor(&self) -> f64 {
730 self.power_throttle_factor
731 }
732
733 pub fn get_velocity(&self, target: ComputeTarget) -> f64 {
735 match target {
736 ComputeTarget::Cpu => self.cpu_velocity.get_velocity(),
737 ComputeTarget::WebGpu => self.webgpu_velocity.get_velocity(),
738 ComputeTarget::DirectMl => self.directml_velocity.get_velocity(),
739 }
740 }
741}
742
743impl Default for JobEstimator {
744 fn default() -> Self {
745 Self::new()
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn test_job_progress_calculation() {
755 let mut quin = NQuin::default();
756 quin.object = 5000; let job = Job::new(quin, 10000); assert_eq!(job.progress(), 50.0);
761 assert_eq!(job.compute_target, ComputeTarget::Cpu);
762 }
763
764 #[test]
765 fn test_job_velocity_calculation() {
766 let mut quin = NQuin::default();
767 quin.object = 1000;
768
769 let mut job = Job::new(quin, 10000);
770 job.dispatched_at = Some(Instant::now() - Duration::from_secs(1));
771 job.completed_at = Some(Instant::now());
772
773 assert!((job.velocity() - 1000.0).abs() < 10.0);
775 }
776
777 #[test]
778 fn test_production_queue_creation() {
779 let queue = ProductionQueue::new();
780 let stats = queue.stats();
781
782 assert!(stats.num_workers > 0);
783 assert_eq!(stats.active_jobs, 0);
784 assert_eq!(stats.completed_jobs, 0);
785
786 queue.shutdown();
787 }
788
789 #[test]
790 fn test_worker_thread_job_processing() {
791 let queue = ProductionQueue::new();
792 let num_jobs = 10;
793
794 for i in 0..num_jobs {
796 let mut quin = NQuin::default();
797 quin.object = i * 1000; let job = Job::new(quin, 10000);
799 queue.submit_job(job).unwrap();
800 }
801
802 std::thread::sleep(Duration::from_secs(2));
804
805 let results = queue.collect_results();
807 assert!(results.len() > 0, "At least one job should be completed");
808
809 for result in &results {
811 assert!(result.quin.object > 0, "Job should have progressed");
812 assert_eq!(result.status, JobStatus::Completed);
813 }
814
815 queue.shutdown();
816 }
817
818 #[test]
819 fn test_environmental_yielding() {
820 let queue = ProductionQueue::new();
821
822 let mut quin = NQuin::default();
824 quin.object = 1000;
825 let job = Job::new(quin, 10000);
826 queue.submit_job(job).unwrap();
827
828 std::thread::sleep(Duration::from_millis(100));
830
831 queue.pause();
833 assert!(queue.is_paused(), "Queue should be paused");
834
835 std::thread::sleep(Duration::from_millis(200));
837
838 queue.resume();
840 assert!(!queue.is_paused(), "Queue should not be paused");
841
842 std::thread::sleep(Duration::from_secs(1));
844
845 queue.shutdown();
846 }
847
848 #[test]
849 fn test_telemetry_calculation() {
850 let queue = ProductionQueue::new();
851 let total_bytes = 100000;
852
853 for i in 0..5 {
855 let mut quin = NQuin::default();
856 quin.object = i * 1000;
857 let job = Job::new(quin, 20000);
858 queue.submit_job(job).unwrap();
859 }
860
861 std::thread::sleep(Duration::from_secs(1));
863
864 let telemetry = queue.calculate_telemetry(total_bytes);
866
867 assert!(telemetry.total_jobs > 0);
868 assert!(telemetry.overall_progress >= 0.0 && telemetry.overall_progress <= 100.0);
869
870 queue.shutdown();
871 }
872
873 #[test]
874 fn test_hardware_velocity_tracking() {
875 let mut velocity = HardwareVelocity::new(ComputeTarget::Cpu);
876
877 velocity.update(1_000_000, Duration::from_millis(100)); velocity.update(2_000_000, Duration::from_millis(200)); assert_eq!(velocity.sample_count, 2);
882 assert!(velocity.get_velocity() > 0.0);
883 }
884
885 #[test]
886 fn test_job_estimator() {
887 let estimator = JobEstimator::new();
888
889 let params = JobEstimateParams {
890 workload_bytes: 100_000_000, step_size: 0.01,
892 compute_target: ComputeTarget::Cpu,
893 contention_factor: 1.0,
894 };
895
896 let estimate = estimator.estimate_job_duration(¶ms);
897
898 assert!(estimate.estimated_duration.as_secs() > 0);
899 assert!(estimate.confidence >= 0.0 && estimate.confidence <= 1.0);
900 assert_eq!(estimate.compute_target, ComputeTarget::Cpu);
901 }
902
903 #[test]
904 fn test_job_estimator_with_throttling() {
905 let mut estimator = JobEstimator::new();
906
907 estimator.set_power_throttle_factor(1.5); let params = JobEstimateParams {
911 workload_bytes: 100_000_000,
912 step_size: 0.01,
913 compute_target: ComputeTarget::Cpu,
914 contention_factor: 1.0,
915 };
916
917 let _estimate = estimator.estimate_job_duration(¶ms);
918
919 assert_eq!(estimator.get_power_throttle_factor(), 1.5);
921 }
922
923 #[test]
924 fn test_job_estimator_velocity_learning() {
925 let mut estimator = JobEstimator::new();
926
927 estimator.record_performance(ComputeTarget::Cpu, 10_000_000, Duration::from_secs(1));
929 estimator.record_performance(ComputeTarget::Cpu, 20_000_000, Duration::from_secs(2));
930
931 let params = JobEstimateParams {
933 workload_bytes: 100_000_000,
934 step_size: 0.01,
935 compute_target: ComputeTarget::Cpu,
936 contention_factor: 1.0,
937 };
938
939 let estimate = estimator.estimate_job_duration(¶ms);
940
941 assert!(estimate.confidence > 0.0);
943 }
944
945 #[test]
946 fn test_production_queue_estimator_integration() {
947 let queue = ProductionQueue::new();
948
949 let params = JobEstimateParams {
951 workload_bytes: 50_000_000,
952 step_size: 0.01,
953 compute_target: ComputeTarget::Cpu,
954 contention_factor: 1.0,
955 };
956
957 let estimate = queue.estimate_job(¶ms);
958 assert!(estimate.estimated_duration.as_secs() > 0);
959
960 queue.set_power_throttle_factor(1.5);
962 assert_eq!(queue.get_power_throttle_factor(), 1.5);
963
964 let throttled_estimate = queue.estimate_job(¶ms);
966 assert!(throttled_estimate.estimated_duration >= estimate.estimated_duration);
967
968 queue.record_performance(ComputeTarget::Cpu, 10_000_000, Duration::from_secs(1));
970
971 queue.shutdown();
972 }
973
974 #[test]
975 fn test_iterative_ode_workload() {
976 use crate::modalities::calculus::ode_solver::{ExponentialDecay, Rk4Solver};
977
978 let queue = ProductionQueue::new();
979
980 let decay = ExponentialDecay::new(0.5);
982 let mut solver = Rk4Solver::new(decay, 0.01);
983
984 let num_steps = 10;
986 let mut y: f64 = 1.0;
987 let mut t: f64 = 0.0;
988
989 for step in 0..num_steps {
990 let mut quin = NQuin::default();
991 quin.object = y.to_bits() as u64;
992 quin.metadata = t.to_bits();
993
994 let job = Job::with_target(quin, 1000, ComputeTarget::Cpu);
995 queue.submit_job(job).unwrap();
996
997 y = solver.step(t, y, 0.01);
999 t += 0.01;
1000
1001 log::debug!("Step {}: t={}, y={}", step, t, y);
1002 }
1003
1004 std::thread::sleep(Duration::from_secs(2));
1006
1007 let results = queue.collect_results();
1009 assert!(results.len() > 0, "At least one job should complete");
1010
1011 for result in &results {
1013 assert_eq!(result.status, JobStatus::Completed);
1014 }
1015
1016 queue.shutdown();
1017 }
1018}