Skip to main content

ipfrs_tensorlogic/
op_scheduler.rs

1//! Tensor operation scheduler with priority, dependency tracking, and resource accounting.
2//!
3//! [`TensorOpScheduler`] manages a set of [`TensorOp`] entries. Each operation
4//! carries a [`OpPriority`], a list of dependency op-ids that must complete
5//! before the operation is eligible to run, and a resource estimate
6//! (`estimated_flops`). The scheduler advances a monotonic tick counter and
7//! transitions operations through the [`OpStatus`] state machine:
8//!
9//! ```text
10//! Pending  ──(all deps Completed)──►  Ready
11//!                                       │
12//!            ◄── fail_op ───────────────┤
13//!                                       │ start_op
14//!                                       ▼
15//!                                    Running
16//!                                       │
17//!            ◄── fail_op ───────────────┤
18//!                                       │ complete_op
19//!                                       ▼
20//!                                   Completed
21//! ```
22//!
23//! # Example
24//!
25//! ```
26//! use ipfrs_tensorlogic::op_scheduler::{TensorOpScheduler, OpPriority, OpStatus};
27//!
28//! let mut sched = TensorOpScheduler::new();
29//!
30//! // Enqueue a root operation (no deps).
31//! let root = sched.enqueue("matmul".to_string(), OpPriority::High, vec![], 1_000_000);
32//!
33//! // Enqueue a dependent operation.
34//! let dep = sched.enqueue("relu".to_string(), OpPriority::Normal, vec![root], 500_000);
35//!
36//! // Root is immediately ready (no deps). Advance tick to flush.
37//! sched.advance_tick();
38//! assert_eq!(sched.ops[&root].status, OpStatus::Ready);
39//! // dep still pending because root is not yet Completed.
40//! assert_eq!(sched.ops[&dep].status, OpStatus::Pending);
41//!
42//! // Run and complete root.
43//! assert!(sched.start_op(root));
44//! assert!(sched.complete_op(root));
45//!
46//! // Now dep can become Ready.
47//! sched.advance_tick();
48//! assert_eq!(sched.ops[&dep].status, OpStatus::Ready);
49//! ```
50
51use std::collections::HashMap;
52
53// ---------------------------------------------------------------------------
54// OpPriority
55// ---------------------------------------------------------------------------
56
57/// Scheduling priority for a tensor operation.
58///
59/// Higher numeric value = higher urgency. Used by [`TensorOpScheduler::next_ready`]
60/// to pick the most urgent ready operation.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
62pub enum OpPriority {
63    /// Background / best-effort work.
64    Low = 0,
65    /// Default priority for most operations.
66    Normal = 1,
67    /// Elevated priority; preferred over `Normal` and `Low`.
68    High = 2,
69    /// Highest priority; must run before everything else.
70    Critical = 3,
71}
72
73// ---------------------------------------------------------------------------
74// OpStatus
75// ---------------------------------------------------------------------------
76
77/// Lifecycle state of a scheduled tensor operation.
78#[derive(Clone, Debug, PartialEq)]
79pub enum OpStatus {
80    /// Waiting for one or more dependency operations to complete.
81    Pending,
82    /// All dependencies are satisfied; operation may be started.
83    Ready,
84    /// Operation is currently executing.
85    Running,
86    /// Operation finished successfully.
87    Completed,
88    /// Operation encountered an error.
89    Failed {
90        /// Human-readable description of the failure.
91        reason: String,
92    },
93}
94
95// ---------------------------------------------------------------------------
96// TensorOp
97// ---------------------------------------------------------------------------
98
99/// A single tensor operation tracked by the scheduler.
100#[derive(Clone, Debug)]
101pub struct TensorOp {
102    /// Monotonically increasing identifier assigned at enqueue time.
103    pub op_id: u64,
104    /// Human-readable operation name (e.g. `"matmul"`, `"relu"`).
105    pub name: String,
106    /// Scheduling priority of this operation.
107    pub priority: OpPriority,
108    /// Identifiers of operations that must reach [`OpStatus::Completed`] before
109    /// this operation may transition to [`OpStatus::Ready`].
110    pub deps: Vec<u64>,
111    /// Estimated floating-point operations (used for resource accounting).
112    pub estimated_flops: u64,
113    /// Current lifecycle state.
114    pub status: OpStatus,
115    /// Scheduler tick at which this operation was enqueued.
116    pub enqueued_at: u64,
117    /// Scheduler tick at which this operation transitioned to `Running`.
118    pub started_at: Option<u64>,
119    /// Scheduler tick at which this operation reached `Completed` or `Failed`.
120    pub completed_at: Option<u64>,
121}
122
123impl TensorOp {
124    /// Returns the number of ticks the operation spent running, or `None` if
125    /// the operation has not both started and completed.
126    #[must_use]
127    pub fn duration_ticks(&self) -> Option<u64> {
128        match (self.started_at, self.completed_at) {
129            (Some(s), Some(c)) => Some(c.saturating_sub(s)),
130            _ => None,
131        }
132    }
133}
134
135// ---------------------------------------------------------------------------
136// SchedulerStats
137// ---------------------------------------------------------------------------
138
139/// Aggregate statistics produced by [`TensorOpScheduler::stats`].
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct SchedulerStats {
142    /// Total number of operations enqueued since the scheduler was created.
143    pub total_enqueued: u64,
144    /// Number of operations in [`OpStatus::Completed`] state.
145    pub completed: u64,
146    /// Number of operations in [`OpStatus::Failed`] state.
147    pub failed: u64,
148    /// Number of operations currently in [`OpStatus::Pending`] state.
149    pub pending: usize,
150    /// Number of operations currently in [`OpStatus::Ready`] state.
151    pub ready: usize,
152    /// Number of operations currently in [`OpStatus::Running`] state.
153    pub running: usize,
154    /// Sum of `estimated_flops` for all `Completed` operations.
155    pub total_flops_completed: u64,
156}
157
158// ---------------------------------------------------------------------------
159// TensorOpScheduler
160// ---------------------------------------------------------------------------
161
162/// Priority-based tensor operation scheduler with dependency tracking.
163///
164/// See the [module-level documentation](self) for a full usage example.
165pub struct TensorOpScheduler {
166    /// All registered operations keyed by `op_id`.
167    pub ops: HashMap<u64, TensorOp>,
168    /// Next op_id to assign.
169    next_id: u64,
170    /// Current monotonic tick counter.
171    tick: u64,
172}
173
174impl TensorOpScheduler {
175    /// Creates an empty scheduler.
176    #[must_use]
177    pub fn new() -> Self {
178        Self {
179            ops: HashMap::new(),
180            next_id: 0,
181            tick: 0,
182        }
183    }
184
185    /// Enqueues a new operation and returns its assigned `op_id`.
186    ///
187    /// The operation starts in [`OpStatus::Pending`] regardless of whether its
188    /// dependency list is empty. Call [`advance_tick`](Self::advance_tick) to
189    /// promote operations with satisfied dependencies to [`OpStatus::Ready`].
190    ///
191    /// # Arguments
192    ///
193    /// * `name`     – Human-readable operation name.
194    /// * `priority` – Scheduling priority.
195    /// * `deps`     – IDs of operations that must complete before this one runs.
196    /// * `flops`    – Estimated floating-point operations for resource accounting.
197    pub fn enqueue(
198        &mut self,
199        name: String,
200        priority: OpPriority,
201        deps: Vec<u64>,
202        flops: u64,
203    ) -> u64 {
204        let op_id = self.next_id;
205        self.next_id += 1;
206
207        let op = TensorOp {
208            op_id,
209            name,
210            priority,
211            deps,
212            estimated_flops: flops,
213            status: OpStatus::Pending,
214            enqueued_at: self.tick,
215            started_at: None,
216            completed_at: None,
217        };
218        self.ops.insert(op_id, op);
219        self.tick += 1;
220        op_id
221    }
222
223    /// Advances the scheduler tick by one and re-evaluates dependency readiness.
224    ///
225    /// Each [`OpStatus::Pending`] operation whose every dependency is in
226    /// [`OpStatus::Completed`] is promoted to [`OpStatus::Ready`].
227    pub fn advance_tick(&mut self) {
228        self.tick += 1;
229
230        // Collect ids that should transition Pending → Ready.
231        // We cannot mutate `self.ops` while iterating it, so we gather the ids
232        // first.
233        let ids_to_ready: Vec<u64> = self
234            .ops
235            .iter()
236            .filter_map(|(&id, op)| {
237                if op.status != OpStatus::Pending {
238                    return None;
239                }
240                let all_done = op.deps.iter().all(|dep_id| {
241                    self.ops
242                        .get(dep_id)
243                        .map(|d| d.status == OpStatus::Completed)
244                        .unwrap_or(false)
245                });
246                if all_done {
247                    Some(id)
248                } else {
249                    None
250                }
251            })
252            .collect();
253
254        for id in ids_to_ready {
255            if let Some(op) = self.ops.get_mut(&id) {
256                op.status = OpStatus::Ready;
257            }
258        }
259    }
260
261    /// Attempts to transition the operation to [`OpStatus::Running`].
262    ///
263    /// Returns `true` on success. Returns `false` if the operation does not
264    /// exist or is not in [`OpStatus::Ready`] state.
265    pub fn start_op(&mut self, op_id: u64) -> bool {
266        match self.ops.get_mut(&op_id) {
267            Some(op) if op.status == OpStatus::Ready => {
268                op.status = OpStatus::Running;
269                op.started_at = Some(self.tick);
270                true
271            }
272            _ => false,
273        }
274    }
275
276    /// Attempts to transition the operation to [`OpStatus::Completed`].
277    ///
278    /// Returns `true` on success. Returns `false` if the operation does not
279    /// exist or is not in [`OpStatus::Running`] state.
280    pub fn complete_op(&mut self, op_id: u64) -> bool {
281        match self.ops.get_mut(&op_id) {
282            Some(op) if op.status == OpStatus::Running => {
283                op.status = OpStatus::Completed;
284                op.completed_at = Some(self.tick);
285                true
286            }
287            _ => false,
288        }
289    }
290
291    /// Attempts to transition the operation to [`OpStatus::Failed`].
292    ///
293    /// The operation must be in [`OpStatus::Running`] or [`OpStatus::Ready`]
294    /// state. Returns `true` on success, `false` otherwise.
295    pub fn fail_op(&mut self, op_id: u64, reason: String) -> bool {
296        match self.ops.get_mut(&op_id) {
297            Some(op) if op.status == OpStatus::Running || op.status == OpStatus::Ready => {
298                op.status = OpStatus::Failed { reason };
299                op.completed_at = Some(self.tick);
300                true
301            }
302            _ => false,
303        }
304    }
305
306    /// Returns the `op_id` of the highest-priority [`OpStatus::Ready`] operation.
307    ///
308    /// Ties in priority are broken by the lowest `op_id` (FIFO within priority).
309    /// Returns `None` if no operations are currently in `Ready` state.
310    #[must_use]
311    pub fn next_ready(&self) -> Option<u64> {
312        self.ops
313            .values()
314            .filter(|op| op.status == OpStatus::Ready)
315            .max_by(|a, b| {
316                // Primary: higher priority wins.
317                // Secondary: lower op_id wins (FIFO).
318                a.priority
319                    .cmp(&b.priority)
320                    .then_with(|| b.op_id.cmp(&a.op_id))
321            })
322            .map(|op| op.op_id)
323    }
324
325    /// Returns a snapshot of aggregate scheduler statistics.
326    #[must_use]
327    pub fn stats(&self) -> SchedulerStats {
328        let mut completed: u64 = 0;
329        let mut failed: u64 = 0;
330        let mut pending: usize = 0;
331        let mut ready: usize = 0;
332        let mut running: usize = 0;
333        let mut total_flops_completed: u64 = 0;
334
335        for op in self.ops.values() {
336            match &op.status {
337                OpStatus::Pending => pending += 1,
338                OpStatus::Ready => ready += 1,
339                OpStatus::Running => running += 1,
340                OpStatus::Completed => {
341                    completed += 1;
342                    total_flops_completed =
343                        total_flops_completed.saturating_add(op.estimated_flops);
344                }
345                OpStatus::Failed { .. } => failed += 1,
346            }
347        }
348
349        SchedulerStats {
350            total_enqueued: self.next_id,
351            completed,
352            failed,
353            pending,
354            ready,
355            running,
356            total_flops_completed,
357        }
358    }
359}
360
361impl Default for TensorOpScheduler {
362    fn default() -> Self {
363        Self::new()
364    }
365}
366
367// ---------------------------------------------------------------------------
368// Tests
369// ---------------------------------------------------------------------------
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    // ------------------------------------------------------------------
376    // Helper: create a scheduler, enqueue an op, advance, start, complete.
377    // ------------------------------------------------------------------
378
379    fn make_sched() -> TensorOpScheduler {
380        TensorOpScheduler::new()
381    }
382
383    // 1. Enqueue returns sequential IDs starting from 0.
384    #[test]
385    fn test_enqueue_sequential_ids() {
386        let mut s = make_sched();
387        let id0 = s.enqueue("a".to_string(), OpPriority::Normal, vec![], 100);
388        let id1 = s.enqueue("b".to_string(), OpPriority::Normal, vec![], 100);
389        let id2 = s.enqueue("c".to_string(), OpPriority::Normal, vec![], 100);
390        assert_eq!(id0, 0);
391        assert_eq!(id1, 1);
392        assert_eq!(id2, 2);
393    }
394
395    // 2. Fresh op starts as Pending.
396    #[test]
397    fn test_enqueue_status_pending() {
398        let mut s = make_sched();
399        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
400        assert_eq!(s.ops[&id].status, OpStatus::Pending);
401    }
402
403    // 3. advance_tick promotes dep-free Pending op to Ready.
404    #[test]
405    fn test_advance_tick_no_deps_becomes_ready() {
406        let mut s = make_sched();
407        let id = s.enqueue("op".to_string(), OpPriority::High, vec![], 500);
408        s.advance_tick();
409        assert_eq!(s.ops[&id].status, OpStatus::Ready);
410    }
411
412    // 4. Pending op with unsatisfied dep stays Pending after advance_tick.
413    #[test]
414    fn test_advance_tick_pending_dep_stays_pending() {
415        let mut s = make_sched();
416        let root = s.enqueue("root".to_string(), OpPriority::Normal, vec![], 0);
417        let child = s.enqueue("child".to_string(), OpPriority::Normal, vec![root], 0);
418        s.advance_tick();
419        // root: no deps → Ready
420        assert_eq!(s.ops[&root].status, OpStatus::Ready);
421        // child depends on root which is still Ready (not Completed) → stays Pending
422        assert_eq!(s.ops[&child].status, OpStatus::Pending);
423    }
424
425    // 5. Dependent op becomes Ready only after dep is Completed.
426    #[test]
427    fn test_advance_tick_after_dep_completed() {
428        let mut s = make_sched();
429        let root = s.enqueue("root".to_string(), OpPriority::Normal, vec![], 0);
430        let child = s.enqueue("child".to_string(), OpPriority::Normal, vec![root], 0);
431        s.advance_tick(); // root → Ready, child still Pending
432        assert!(s.start_op(root)); // root → Running
433        assert!(s.complete_op(root)); // root → Completed
434        s.advance_tick(); // child → Ready
435        assert_eq!(s.ops[&child].status, OpStatus::Ready);
436    }
437
438    // 6. start_op transitions Ready → Running.
439    #[test]
440    fn test_start_op_ready_to_running() {
441        let mut s = make_sched();
442        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
443        s.advance_tick();
444        assert_eq!(s.ops[&id].status, OpStatus::Ready);
445        let ok = s.start_op(id);
446        assert!(ok);
447        assert_eq!(s.ops[&id].status, OpStatus::Running);
448    }
449
450    // 7. start_op on Pending op returns false.
451    #[test]
452    fn test_start_op_pending_fails() {
453        let mut s = make_sched();
454        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
455        // do not advance_tick, op is still Pending
456        let ok = s.start_op(id);
457        assert!(!ok);
458        assert_eq!(s.ops[&id].status, OpStatus::Pending);
459    }
460
461    // 8. complete_op transitions Running → Completed.
462    #[test]
463    fn test_complete_op_running_to_completed() {
464        let mut s = make_sched();
465        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 42);
466        s.advance_tick();
467        s.start_op(id);
468        let ok = s.complete_op(id);
469        assert!(ok);
470        assert_eq!(s.ops[&id].status, OpStatus::Completed);
471    }
472
473    // 9. complete_op on non-Running op returns false.
474    #[test]
475    fn test_complete_op_non_running_fails() {
476        let mut s = make_sched();
477        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
478        s.advance_tick(); // op is Ready, not Running
479        let ok = s.complete_op(id);
480        assert!(!ok);
481    }
482
483    // 10. complete_op increments total_flops_completed in stats.
484    #[test]
485    fn test_complete_op_increments_flops() {
486        let mut s = make_sched();
487        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 1_000_000);
488        s.advance_tick();
489        s.start_op(id);
490        s.complete_op(id);
491        let stats = s.stats();
492        assert_eq!(stats.total_flops_completed, 1_000_000);
493    }
494
495    // 11. fail_op on Running op transitions to Failed with reason.
496    #[test]
497    fn test_fail_op_running_to_failed() {
498        let mut s = make_sched();
499        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
500        s.advance_tick();
501        s.start_op(id);
502        let ok = s.fail_op(id, "OOM".to_string());
503        assert!(ok);
504        assert_eq!(
505            s.ops[&id].status,
506            OpStatus::Failed {
507                reason: "OOM".to_string()
508            }
509        );
510    }
511
512    // 12. fail_op on Ready op also succeeds.
513    #[test]
514    fn test_fail_op_ready_to_failed() {
515        let mut s = make_sched();
516        let id = s.enqueue("op".to_string(), OpPriority::High, vec![], 0);
517        s.advance_tick();
518        assert_eq!(s.ops[&id].status, OpStatus::Ready);
519        let ok = s.fail_op(id, "device lost".to_string());
520        assert!(ok);
521        assert_eq!(
522            s.ops[&id].status,
523            OpStatus::Failed {
524                reason: "device lost".to_string()
525            }
526        );
527    }
528
529    // 13. fail_op on Pending op returns false.
530    #[test]
531    fn test_fail_op_pending_fails() {
532        let mut s = make_sched();
533        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
534        // no advance_tick → Pending
535        let ok = s.fail_op(id, "err".to_string());
536        assert!(!ok);
537        assert_eq!(s.ops[&id].status, OpStatus::Pending);
538    }
539
540    // 14. next_ready picks highest-priority Ready op.
541    #[test]
542    fn test_next_ready_highest_priority() {
543        let mut s = make_sched();
544        let low = s.enqueue("low".to_string(), OpPriority::Low, vec![], 0);
545        let high = s.enqueue("high".to_string(), OpPriority::High, vec![], 0);
546        let normal = s.enqueue("normal".to_string(), OpPriority::Normal, vec![], 0);
547        s.advance_tick();
548        let next = s.next_ready().expect("should have a ready op");
549        assert_eq!(next, high, "High priority op should be selected");
550        // suppress unused warnings
551        let _ = (low, normal);
552    }
553
554    // 15. next_ready tie-breaking: lower op_id wins (FIFO).
555    #[test]
556    fn test_next_ready_fifo_tiebreak() {
557        let mut s = make_sched();
558        // Enqueue three Normal-priority ops; they should be ready after advance.
559        let id0 = s.enqueue("first".to_string(), OpPriority::Normal, vec![], 0);
560        let id1 = s.enqueue("second".to_string(), OpPriority::Normal, vec![], 0);
561        let _id2 = s.enqueue("third".to_string(), OpPriority::Normal, vec![], 0);
562        s.advance_tick();
563        let next = s.next_ready().expect("should have ready op");
564        assert_eq!(next, id0, "Lowest op_id should win tie");
565        // Start and complete id0, then check again.
566        s.start_op(id0);
567        s.complete_op(id0);
568        let next2 = s.next_ready().expect("should still have ready ops");
569        assert_eq!(next2, id1);
570    }
571
572    // 16. next_ready returns None when no ops are Ready.
573    #[test]
574    fn test_next_ready_none_when_empty() {
575        let s = make_sched();
576        assert_eq!(s.next_ready(), None);
577    }
578
579    // 17. stats counts reflect all status categories correctly.
580    #[test]
581    fn test_stats_counts() {
582        let mut s = make_sched();
583        // Enqueue 4 ops
584        let a = s.enqueue("a".to_string(), OpPriority::Normal, vec![], 100);
585        let b = s.enqueue("b".to_string(), OpPriority::Normal, vec![], 200);
586        let _c = s.enqueue("c".to_string(), OpPriority::Normal, vec![a], 300);
587        let _d = s.enqueue("d".to_string(), OpPriority::Normal, vec![], 400);
588        // a, b, d: no deps → all Pending initially
589        s.advance_tick(); // a, b, d → Ready; c still Pending (dep on a)
590        s.start_op(a); // a → Running
591        s.complete_op(a); // a → Completed
592        s.start_op(b); // b → Running
593        s.fail_op(b, "timeout".to_string()); // b → Failed
594                                             // Now: a=Completed, b=Failed, c=Pending, d=Ready
595        s.advance_tick(); // c → Ready (a is Completed)
596                          // d still Ready, c now Ready
597        let stats = s.stats();
598        assert_eq!(stats.total_enqueued, 4);
599        assert_eq!(stats.completed, 1);
600        assert_eq!(stats.failed, 1);
601        assert_eq!(stats.pending, 0);
602        assert_eq!(stats.ready, 2); // c and d
603        assert_eq!(stats.running, 0);
604        assert_eq!(stats.total_flops_completed, 100); // only 'a' completed
605    }
606
607    // 18. duration_ticks returns None when not started.
608    #[test]
609    fn test_duration_ticks_not_started() {
610        let mut s = make_sched();
611        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
612        assert_eq!(s.ops[&id].duration_ticks(), None);
613    }
614
615    // 19. duration_ticks returns None when started but not completed.
616    #[test]
617    fn test_duration_ticks_started_not_completed() {
618        let mut s = make_sched();
619        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
620        s.advance_tick();
621        s.start_op(id);
622        assert_eq!(s.ops[&id].duration_ticks(), None);
623    }
624
625    // 20. duration_ticks returns correct delta after completion.
626    #[test]
627    fn test_duration_ticks_completed() {
628        let mut s = make_sched();
629        let id = s.enqueue("op".to_string(), OpPriority::Normal, vec![], 0);
630        s.advance_tick(); // tick advances; op becomes Ready
631        s.start_op(id); // started_at = current tick
632        let started_tick = s.ops[&id].started_at.expect("started_at must be set");
633        s.advance_tick(); // move tick forward
634        s.advance_tick(); // move tick forward again
635                          // complete_op uses current tick as completed_at
636        s.complete_op(id);
637        let completed_tick = s.ops[&id].completed_at.expect("completed_at must be set");
638        let expected = completed_tick - started_tick;
639        assert_eq!(s.ops[&id].duration_ticks(), Some(expected));
640        assert!(expected > 0);
641    }
642
643    // 21. Critical priority beats High, Normal, Low.
644    #[test]
645    fn test_next_ready_critical_beats_all() {
646        let mut s = make_sched();
647        let _low = s.enqueue("low".to_string(), OpPriority::Low, vec![], 0);
648        let _norm = s.enqueue("norm".to_string(), OpPriority::Normal, vec![], 0);
649        let _high = s.enqueue("high".to_string(), OpPriority::High, vec![], 0);
650        let crit = s.enqueue("crit".to_string(), OpPriority::Critical, vec![], 0);
651        s.advance_tick();
652        assert_eq!(s.next_ready(), Some(crit));
653    }
654
655    // 22. Chain of 3 ops completes sequentially.
656    #[test]
657    fn test_chain_three_ops() {
658        let mut s = make_sched();
659        let a = s.enqueue("a".to_string(), OpPriority::Normal, vec![], 10);
660        let b = s.enqueue("b".to_string(), OpPriority::Normal, vec![a], 20);
661        let c = s.enqueue("c".to_string(), OpPriority::Normal, vec![b], 30);
662
663        s.advance_tick();
664        assert_eq!(s.ops[&a].status, OpStatus::Ready);
665        assert_eq!(s.ops[&b].status, OpStatus::Pending);
666        assert_eq!(s.ops[&c].status, OpStatus::Pending);
667
668        s.start_op(a);
669        s.complete_op(a);
670        s.advance_tick();
671        assert_eq!(s.ops[&b].status, OpStatus::Ready);
672        assert_eq!(s.ops[&c].status, OpStatus::Pending);
673
674        s.start_op(b);
675        s.complete_op(b);
676        s.advance_tick();
677        assert_eq!(s.ops[&c].status, OpStatus::Ready);
678
679        s.start_op(c);
680        s.complete_op(c);
681
682        let stats = s.stats();
683        assert_eq!(stats.completed, 3);
684        assert_eq!(stats.total_flops_completed, 60);
685    }
686}