Skip to main content

qualia_core_db/platform/
local_scheduler.rs

1//! Local Scheduler — Thread-per-Core Production Queue
2//!
3//! Implements multi-core parallelism for calculus operations by pinning isolated
4//! SLG VM loops to each logical processor. Each worker operates within a 512MB
5//! memory boundary and pulls jobs from a central lock-free ring buffer.
6//!
7//! Architecture:
8//! - Supervisor thread: Manages job queue and coordinates workers
9//! - Worker threads: Pinned to specific cores, process NQuin jobs
10//! - SPSC channels: Lock-free communication between supervisor and workers
11//! - NVMe WAL: Persistent backing store for job state (future)
12//!
13//! Environmental yielding:
14//! - Thermal governor integration for power-aware execution
15//! - Pause/resume capability via Quin state persistence
16//! - Solar/battery power constraint handling
17
18#![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
29/// Memory boundary for each worker cell (512MB)
30const WORKER_MEMORY_BOUNDARY: usize = 512 * 1024 * 1024;
31
32/// Ring buffer capacity for job distribution
33const JOB_QUEUE_CAPACITY: usize = 4096;
34
35/// Job status tracking
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum JobStatus {
38    Pending,
39    InProgress,
40    Completed,
41    Paused,
42    Failed,
43}
44
45/// Production queue job wrapper
46#[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, // Default to CPU
65        }
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    /// Calculate progress percentage from Quin object field (byte offset)
80    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    /// Calculate processing velocity (bytes per second)
89    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
104/// Worker cell - isolated execution context pinned to a specific core
105pub 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    /// Main worker loop - processes jobs from the receiver channel
136    pub fn run(self) {
137        // Pin this thread to the assigned core
138        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            // Check for pause signal (environmental yielding)
153            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            // Poll for new job with timeout to allow shutdown check
160            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                    // Process the job (dummy implementation for now)
172                    self.process_job(&mut job);
173
174                    job.status = JobStatus::Completed;
175                    job.completed_at = Some(Instant::now());
176
177                    // Send result back to supervisor
178                    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                    // No job available, continue loop
184                    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    /// Process a single job (placeholder for actual SLG VM execution)
200    fn process_job(&self, job: &mut Job) {
201        // Simulate processing by updating the Quin object field
202        // In production, this would execute the actual calculus operation
203        let chunk_size = 4096; // 4KB chunk
204        job.quin.object = job.quin.object.saturating_add(chunk_size);
205
206        // Simulate some work
207        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
217/// Production queue supervisor - coordinates worker threads
218pub 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    /// Create a new production queue with one worker per logical processor
233    pub fn new() -> Self {
234        Self::with_wal_path("jobs.wal")
235    }
236
237    /// Create a new production queue with a specific WAL path
238    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        // Initialize WAL
253        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        // Initialize job estimator
264        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        // Ingress dispatcher: shared queue → per-worker mailboxes.
276        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    /// Submit a job to the production queue
332    pub fn submit_job(&self, job: Job) -> Result<(), String> {
333        // Persist job to WAL if available
334        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    /// WAL path used for job persistence and recovery.
351    pub fn wal_path(&self) -> &str {
352        &self.wal_path
353    }
354
355    /// Recover pending jobs from WAL on startup
356    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)) // Default total_bytes, should be stored in metadata
364                            .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    /// Persist completed job state to WAL
380    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    /// Collect completed jobs from workers
392    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            // Persist completed job state to WAL
400            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    /// Get current queue statistics
411    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    /// Gracefully shutdown all workers
420    pub fn shutdown(&self) {
421        log::info!("Shutting down production queue");
422        self.shutdown_signal.store(true, Ordering::Relaxed);
423
424        // Give workers time to finish current jobs
425        std::thread::sleep(Duration::from_secs(2));
426
427        log::info!("Production queue shutdown complete");
428    }
429
430    /// Pause all workers (environmental yielding)
431    pub fn pause(&self) {
432        log::info!("Pausing production queue (environmental yielding)");
433        self.pause_signal.store(true, Ordering::Relaxed);
434    }
435
436    /// Resume all workers after environmental yielding
437    pub fn resume(&self) {
438        log::info!("Resuming production queue");
439        self.pause_signal.store(false, Ordering::Relaxed);
440    }
441
442    /// Check if the queue is currently paused
443    pub fn is_paused(&self) -> bool {
444        self.pause_signal.load(Ordering::Relaxed)
445    }
446
447    /// Get pre-execution job estimate
448    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            // Fallback if estimator is locked
453            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    /// Record job performance for velocity learning
463    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    /// Set power throttling factor (called by power daemon)
475    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    /// Get current power throttling factor
483    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    /// Calculate progress and telemetry from Quin fields
492    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        // Calculate overall progress from completed jobs
498        let overall_progress = if total > 0 {
499            (completed as f64 / total as f64) * 100.0
500        } else {
501            0.0
502        };
503
504        // Calculate bytes per second from job velocities
505        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        // Estimate time remaining based on velocity
514        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/// Queue statistics
542#[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/// Progress and telemetry data for the production queue
550#[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, // Percentage (0-100)
556    pub estimated_time_remaining: Option<Duration>,
557    pub bytes_per_second: f64,
558}
559
560/// Compute target for calculus operations
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum ComputeTarget {
563    Cpu,
564    WebGpu,
565    DirectMl,
566}
567
568/// Hardware velocity metrics for a specific compute target
569#[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    /// Update velocity with a new sample using exponential moving average
588    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            // Exponential moving average with alpha=0.2 (weights recent samples more)
594            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    /// Get current velocity with default fallback if no samples
608    pub fn get_velocity(&self) -> f64 {
609        if self.sample_count == 0 {
610            // Default velocities based on target (conservative estimates)
611            match self.target {
612                ComputeTarget::Cpu => 10_000_000.0,       // 10 MB/s CPU
613                ComputeTarget::WebGpu => 100_000_000.0,   // 100 MB/s WebGPU
614                ComputeTarget::DirectMl => 500_000_000.0, // 500 MB/s DirectML
615            }
616        } else {
617            self.bytes_per_second
618        }
619    }
620}
621
622/// Job estimation parameters
623#[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, // Multiplier for parallel execution overhead
629}
630
631/// Pre-execution job estimate
632#[derive(Debug, Clone)]
633pub struct JobEstimate {
634    pub estimated_duration: Duration,
635    pub confidence: f64, // 0-1 based on sample count
636    pub compute_target: ComputeTarget,
637    pub workload_bytes: u64,
638}
639
640/// Job estimator for pre-execution time prediction
641pub struct JobEstimator {
642    cpu_velocity: HardwareVelocity,
643    webgpu_velocity: HardwareVelocity,
644    directml_velocity: HardwareVelocity,
645    power_throttle_factor: f64, // Multiplier when power-constrained
646}
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, // No throttling by default
655        }
656    }
657
658    /// Estimate job duration before execution
659    pub fn estimate_job_duration(&self, params: &JobEstimateParams) -> JobEstimate {
660        // Get velocity for the target compute path
661        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        // Apply power throttling factor
668        let adjusted_velocity = velocity / self.power_throttle_factor;
669
670        // Apply contention factor for parallel execution
671        let effective_velocity = adjusted_velocity / params.contention_factor;
672
673        // Calculate workload (number of integration points)
674        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        // Get overhead for target
681        let overhead_secs = match params.compute_target {
682            ComputeTarget::Cpu => 0.005,      // 5ms loop startup
683            ComputeTarget::WebGpu => 0.250,   // 250ms shader dispatch
684            ComputeTarget::DirectMl => 0.100, // 100ms DirectML setup
685        };
686
687        // Calculate estimated duration
688        let duration_secs = (workload_points / effective_velocity) + overhead_secs;
689        let estimated_duration = Duration::from_secs_f64(duration_secs.max(0.0));
690
691        // Calculate confidence based on sample count
692        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        // Confidence increases with sample count, caps at 0.95
699        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    /// Update velocity metrics after job completion
710    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    /// Set power throttling factor (called by power daemon)
724    pub fn set_power_throttle_factor(&mut self, factor: f64) {
725        self.power_throttle_factor = factor.max(0.5).min(2.0); // Clamp between 0.5x and 2x
726    }
727
728    /// Get current power throttling factor
729    pub fn get_power_throttle_factor(&self) -> f64 {
730        self.power_throttle_factor
731    }
732
733    /// Get velocity for a specific target
734    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; // 5000 bytes processed
757
758        let job = Job::new(quin, 10000); // Total 10000 bytes
759
760        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        // 1000 bytes in 1 second ≈ 1000 bytes/sec (allow for timing precision)
774        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        // Submit dummy jobs
795        for i in 0..num_jobs {
796            let mut quin = NQuin::default();
797            quin.object = i * 1000; // Different starting offsets
798            let job = Job::new(quin, 10000);
799            queue.submit_job(job).unwrap();
800        }
801
802        // Wait for jobs to complete
803        std::thread::sleep(Duration::from_secs(2));
804
805        // Collect results
806        let results = queue.collect_results();
807        assert!(results.len() > 0, "At least one job should be completed");
808
809        // Verify progress was made
810        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        // Submit a job
823        let mut quin = NQuin::default();
824        quin.object = 1000;
825        let job = Job::new(quin, 10000);
826        queue.submit_job(job).unwrap();
827
828        // Wait a bit for job to start
829        std::thread::sleep(Duration::from_millis(100));
830
831        // Pause the queue (environmental yielding)
832        queue.pause();
833        assert!(queue.is_paused(), "Queue should be paused");
834
835        // Wait to ensure workers are paused
836        std::thread::sleep(Duration::from_millis(200));
837
838        // Resume the queue
839        queue.resume();
840        assert!(!queue.is_paused(), "Queue should not be paused");
841
842        // Wait for job to complete
843        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        // Submit jobs
854        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        // Wait for some jobs to complete
862        std::thread::sleep(Duration::from_secs(1));
863
864        // Calculate telemetry
865        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        // Update with some samples
878        velocity.update(1_000_000, Duration::from_millis(100)); // 10 MB/s
879        velocity.update(2_000_000, Duration::from_millis(200)); // 10 MB/s
880
881        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, // 100 MB
891            step_size: 0.01,
892            compute_target: ComputeTarget::Cpu,
893            contention_factor: 1.0,
894        };
895
896        let estimate = estimator.estimate_job_duration(&params);
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        // Set power throttling factor
908        estimator.set_power_throttle_factor(1.5); // 1.5x slower due to power constraints
909
910        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(&params);
918
919        // Verify throttling is applied
920        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        // Record some performance data
928        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        // Now estimates should be more confident
932        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(&params);
940
941        // Confidence should be higher after recording performance
942        assert!(estimate.confidence > 0.0);
943    }
944
945    #[test]
946    fn test_production_queue_estimator_integration() {
947        let queue = ProductionQueue::new();
948
949        // Estimate a job before submission
950        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(&params);
958        assert!(estimate.estimated_duration.as_secs() > 0);
959
960        // Test power throttling
961        queue.set_power_throttle_factor(1.5);
962        assert_eq!(queue.get_power_throttle_factor(), 1.5);
963
964        // Estimate with throttling should be longer
965        let throttled_estimate = queue.estimate_job(&params);
966        assert!(throttled_estimate.estimated_duration >= estimate.estimated_duration);
967
968        // Record performance
969        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        // Create ODE solver
981        let decay = ExponentialDecay::new(0.5);
982        let mut solver = Rk4Solver::new(decay, 0.01);
983
984        // Simulate iterative workload by submitting multiple jobs
985        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            // Advance solver state
998            y = solver.step(t, y, 0.01);
999            t += 0.01;
1000
1001            log::debug!("Step {}: t={}, y={}", step, t, y);
1002        }
1003
1004        // Wait for jobs to complete
1005        std::thread::sleep(Duration::from_secs(2));
1006
1007        // Collect results
1008        let results = queue.collect_results();
1009        assert!(results.len() > 0, "At least one job should complete");
1010
1011        // Verify results were processed
1012        for result in &results {
1013            assert_eq!(result.status, JobStatus::Completed);
1014        }
1015
1016        queue.shutdown();
1017    }
1018}