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}