feat: implement virtual clock timing architecture (Phases 5-8)

Introduces a VirtualClock-based timing system replacing wall-clock
Instant-based timing for hardware-accurate simulation.

New modules:
- virtual_clock.rs: Shared virtual time source with condvar sync
- real_time_governor.rs: Thread that advances virtual time
- component_timing.rs: Per-component timing constraints

Key changes:
- Component trait: added update_at_time(), timing(), set_virtual_clock_optional()
- All Intel 400x components migrated to virtual-time-aware operation
- ConfigurableSystem: start() returns Result, spawns governor thread
- GUI: timing controls (speed slider, mode selector, step button)
- Console: speed/mode/step/time commands
- Backward compatible: legacy fallback for components without VirtualClock

Reviewed-by: code review found 2 warnings (clock resume() dead code,
reset() safety docs) — both fixed.
This commit is contained in:
Jan-Peter von Hunnius 2026-05-14 21:00:22 +02:00
parent add7ace674
commit 08f53d68fe
28 changed files with 7186 additions and 270 deletions

View file

@ -0,0 +1,898 @@
# Phase 5: Migrate Clock Components to Virtual Time
## Overview
Phase 5 replaces `Instant::now()` and `thread::sleep()` in both clock components with virtual time from `Arc<VirtualClock>`. This is the first phase that modifies existing components rather than adding new infrastructure.
**Files modified:**
- [`generic_clock.rs`](src/components/clock/generic_clock.rs) — GenericClock
- [`two_phase_clock.rs`](src/components/clock/two_phase_clock.rs) — TwoPhaseClock
- [`system_config.rs`](src/system_config.rs) — Clock factory closures (minor)
**Files NOT modified** (Phase 7 responsibility):
- [`virtual_clock.rs`](src/virtual_clock.rs) — Already complete
- [`real_time_governor.rs`](src/real_time_governor.rs) — Already complete
- [`component.rs`](src/component.rs) — Already has `update_at_time()` default
---
## Implementation Order
**Migrate `GenericClock` first, then `TwoPhaseClock`.**
Rationale:
1. `GenericClock` is simpler — single-phase, single CLK output
2. `TwoPhaseClock` has dual outputs (PHI1/PHI2) and inter-phase dependencies
3. Both follow the same pattern, so GenericClock establishes the template
4. `GenericClock` has existing unit tests to validate the migration
---
## Part A: GenericClock Migration
### A1. Current Timing Mechanism
[`GenericClock`](src/components/clock/generic_clock.rs:11) uses wall-clock time in 5 locations:
| Location | Line | Current Code | Purpose |
|----------|------|-------------|---------|
| [`last_transition`](src/components/clock/generic_clock.rs:16) field | 16 | `last_transition: Instant` | Track when last phase change occurred |
| [`pause_start`](src/components/clock/generic_clock.rs:21) field | 21 | `pause_start: Option<Instant>` | Track when pause began |
| [`new()`](src/components/clock/generic_clock.rs:34) constructor | 34 | `last_transition: Instant::now()` | Initialize transition timer |
| [`enable()`](src/components/clock/generic_clock.rs:68) | 68 | `self.last_transition = Instant::now()` | Reset timer on enable |
| [`pause()`](src/components/clock/generic_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start |
| [`resume()`](src/components/clock/generic_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration |
| [`should_transition()`](src/components/clock/generic_clock.rs:131) | 131 | `self.last_transition.elapsed()` | Check if phase duration elapsed |
| [`perform_transition()`](src/components/clock/generic_clock.rs:149) | 149 | `self.last_transition = Instant::now()` | Record transition time |
| [`run()`](src/components/clock/generic_clock.rs:205) | 205 | `thread::sleep(sleep_time)` | Busy-wait loop with sleep |
### A2. Required Changes
#### A2.1 — Add `VirtualClock` field (optional, gradual integration)
Replace `Instant`-based fields with virtual-time equivalents. Add `Option<Arc<VirtualClock>>` for gradual integration — clocks work without a VirtualClock (falling back to wall-clock), but use virtual time when one is injected.
```rust
// BEFORE (line 11-22):
pub struct GenericClock {
base: BaseComponent,
frequency: f64,
duty_cycle: f64,
current_state: PinValue,
last_transition: Instant,
high_time: Duration,
low_time: Duration,
enabled: bool,
paused: bool,
pause_start: Option<Instant>,
}
// AFTER:
pub struct GenericClock {
base: BaseComponent,
frequency: f64,
duty_cycle: f64,
current_state: PinValue,
last_transition_vtime: Duration, // Virtual time of last transition
high_time: Duration,
low_time: Duration,
enabled: bool,
paused: bool,
pause_start_vtime: Option<Duration>, // Virtual time when pause started
virtual_clock: Option<Arc<VirtualClock>>, // Injected in Phase 7
}
```
**Key change:** `last_transition: Instant` → `last_transition_vtime: Duration` (virtual nanoseconds since sim start). `pause_start: Option<Instant>` → `pause_start_vtime: Option<Duration>`.
#### A2.2 — Update constructor
```rust
// BEFORE (line 29-43):
let mut clock = GenericClock {
base: BaseComponent::new(name, pins),
frequency,
duty_cycle: 0.5,
current_state: PinValue::Low,
last_transition: Instant::now(),
high_time: Duration::from_secs_f64(0.5 / frequency),
low_time: Duration::from_secs_f64(0.5 / frequency),
enabled: true,
paused: false,
pause_start: None,
};
// AFTER:
let mut clock = GenericClock {
base: BaseComponent::new(name, pins),
frequency,
duty_cycle: 0.5,
current_state: PinValue::Low,
last_transition_vtime: Duration::ZERO, // Virtual time 0
high_time: Duration::from_secs_f64(0.5 / frequency),
low_time: Duration::from_secs_f64(0.5 / frequency),
enabled: true,
paused: false,
pause_start_vtime: None,
virtual_clock: None, // Set later via set_virtual_clock()
};
```
#### A2.3 — Add `set_virtual_clock()` method
New method for Phase 7 injection:
```rust
/// Inject a VirtualClock for virtual-time-based operation.
/// If not set, the clock falls back to wall-clock Instant-based timing.
/// This method is called by ConfigurableSystem in Phase 7.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
```
#### A2.4 — Update `enable()` method (line 64-69)
```rust
// BEFORE:
pub fn enable(&mut self) {
self.enabled = true;
self.current_state = PinValue::Low;
self.last_transition = Instant::now();
}
// AFTER:
pub fn enable(&mut self) {
self.enabled = true;
self.current_state = PinValue::Low;
self.last_transition_vtime = self.current_virtual_time();
}
```
Where `current_virtual_time()` is a new helper:
```rust
/// Get current virtual time, or fall back to Duration::ZERO if no VirtualClock.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
```
#### A2.5 — Update `pause()` and `resume()` (lines 78-94)
```rust
// BEFORE:
pub fn pause(&mut self) {
self.paused = true;
self.pause_start = Some(Instant::now());
}
pub fn resume(&mut self) {
if let Some(pause_start) = self.pause_start {
let pause_duration = pause_start.elapsed();
self.last_transition += pause_duration;
}
self.paused = false;
self.pause_start = None;
}
// AFTER:
pub fn pause(&mut self) {
self.paused = true;
self.pause_start_vtime = Some(self.current_virtual_time());
}
pub fn resume(&mut self) {
if let Some(pause_start_vtime) = self.pause_start_vtime {
let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime);
self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration);
}
self.paused = false;
self.pause_start_vtime = None;
}
```
#### A2.6 — Update `should_transition()` (line 130-138)
```rust
// BEFORE:
fn should_transition(&self) -> bool {
let elapsed = self.last_transition.elapsed();
match self.current_state {
PinValue::High => elapsed >= self.high_time,
PinValue::Low => elapsed >= self.low_time,
PinValue::HighZ => true,
}
}
// AFTER:
fn should_transition(&self, virtual_time: Duration) -> bool {
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
match self.current_state {
PinValue::High => elapsed >= self.high_time,
PinValue::Low => elapsed >= self.low_time,
PinValue::HighZ => true,
}
}
```
**Note:** `should_transition()` now takes `virtual_time: Duration` as a parameter instead of computing elapsed from `Instant`.
#### A2.7 — Update `perform_transition()` (line 140-150)
```rust
// BEFORE:
fn perform_transition(&mut self) {
let new_state = match self.current_state {
PinValue::High => PinValue::Low,
PinValue::Low => PinValue::High,
PinValue::HighZ => PinValue::Low,
};
self.current_state = new_state;
self.set_clock_output(new_state);
self.last_transition = Instant::now();
}
// AFTER:
fn perform_transition(&mut self, virtual_time: Duration) {
let new_state = match self.current_state {
PinValue::High => PinValue::Low,
PinValue::Low => PinValue::High,
PinValue::HighZ => PinValue::Low,
};
self.current_state = new_state;
self.set_clock_output(new_state);
self.last_transition_vtime = virtual_time;
}
```
#### A2.8 — Implement `update_at_time()` (new, replaces `update()` for virtual-time path)
Add the virtual-time-aware update method. This overrides the default from the `Component` trait:
```rust
impl Component for GenericClock {
// ... existing methods unchanged ...
fn update_at_time(&mut self, virtual_time: Duration) {
// Check pause state first - don't update if paused
if self.paused {
return;
}
// Read external enable control
let external_enable = self.read_enable_pin();
let should_be_enabled = self.enabled && external_enable;
if !should_be_enabled {
if self.current_state != PinValue::Low {
self.set_clock_output(PinValue::Low);
self.current_state = PinValue::Low;
}
return;
}
// Check if it's time to transition (using virtual time)
if self.should_transition(virtual_time) {
self.perform_transition(virtual_time);
}
}
// Keep update() for backward compatibility — delegates to update_at_time
// using current virtual time (or Duration::ZERO if no VirtualClock)
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
}
```
#### A2.9 — Rewrite `run()` to use `wait_until()` (line 190-210)
This is the most significant change. Replace the `thread::sleep()` busy-loop with `virtual_clock.wait_until()`:
```rust
// BEFORE:
fn run(&mut self) {
self.base.set_running(true);
self.enable();
while self.is_running() {
self.update();
let sleep_time = if self.frequency > 0.0 {
Duration::from_secs_f64(1.0 / self.frequency / 100.0)
.max(Duration::from_micros(1))
} else {
Duration::from_micros(100)
};
thread::sleep(sleep_time);
}
self.disable();
}
// AFTER:
fn run(&mut self) {
self.base.set_running(true);
self.enable();
// If no VirtualClock is injected, fall back to wall-clock run loop
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => {
// Legacy path: use old Instant-based timing
return self.run_legacy();
}
};
while self.is_running() {
// Calculate next transition time
let next_transition = self.next_transition_time();
// Block until virtual time reaches next transition
match vclock.wait_until(next_transition) {
Some(current_vtime) => {
if !self.is_running() {
break;
}
self.update_at_time(current_vtime);
}
None => break, // Simulation stopped
}
}
self.disable();
}
```
Add helper methods:
```rust
impl GenericClock {
/// Calculate the virtual time of the next phase transition.
fn next_transition_time(&self) -> Duration {
let phase_duration = match self.current_state {
PinValue::High => self.high_time,
PinValue::Low => self.low_time,
PinValue::HighZ => Duration::from_nanos(1),
};
self.last_transition_vtime.saturating_add(phase_duration)
}
/// Legacy run loop using wall-clock time (backward compatibility).
fn run_legacy(&mut self) {
while self.is_running() {
self.update();
let sleep_time = if self.frequency > 0.0 {
Duration::from_secs_f64(1.0 / self.frequency / 100.0)
.max(Duration::from_micros(1))
} else {
Duration::from_micros(100)
};
thread::sleep(sleep_time);
}
self.disable();
}
}
```
#### A2.10 — Add `timing()` override
```rust
impl Component for GenericClock {
// ... existing methods ...
fn timing(&self) -> ComponentTiming {
ComponentTiming {
name: "Generic Clock",
access_time: Duration::ZERO,
propagation_delay: Duration::ZERO,
setup_time: Duration::ZERO,
hold_time: Duration::ZERO,
max_clock_frequency: f64::INFINITY,
min_update_interval: self.high_time.min(self.low_time),
}
}
}
```
#### A2.11 — Update imports
```rust
// BEFORE (line 1-9):
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crate::component::{BaseComponent, Component};
use crate::pin::{Pin, PinValue};
// AFTER:
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
```
**Key change:** Remove `Instant` from imports, add `VirtualClock` and `ComponentTiming`.
### A3. GenericClock Test Impact
The existing tests in [`generic_clock.rs:238-258`](src/components/clock/generic_clock.rs:238) are:
| Test | What it checks | Impact |
|------|---------------|--------|
| [`test_clock_creation`](src/components/clock/generic_clock.rs:243) | Name and running state | **No impact** — doesn't use `Instant` |
| [`test_clock_timing_calculation`](src/components/clock/generic_clock.rs:250) | `frequency_to_duration()` | **No impact** — static calculation |
These tests will pass unchanged. No new tests needed for the migration itself — the existing tests validate construction and static calculations. Virtual-time behavior tests should be added in Phase 9.
---
## Part B: TwoPhaseClock Migration
### B1. Current Timing Mechanism
[`TwoPhaseClock`](src/components/clock/two_phase_clock.rs:11) uses wall-clock time in 5 locations:
| Location | Line | Current Code | Purpose |
|----------|------|-------------|---------|
| [`last_transition`](src/components/clock/two_phase_clock.rs:15) field | 15 | `last_transition: Instant` | Track when last phase swap occurred |
| [`pause_start`](src/components/clock/two_phase_clock.rs:19) field | 19 | `pause_start: Option<Instant>` | Track when pause began |
| [`new()`](src/components/clock/two_phase_clock.rs:38) constructor | 38 | `last_transition: Instant::now()` | Initialize transition timer |
| [`enable()`](src/components/clock/two_phase_clock.rs:64) | 64 | `self.last_transition = Instant::now()` | Reset timer on enable |
| [`pause()`](src/components/clock/two_phase_clock.rs:80) | 80 | `self.pause_start = Some(Instant::now())` | Record pause start |
| [`resume()`](src/components/clock/two_phase_clock.rs:88) | 88 | `pause_start.elapsed()` | Calculate pause duration |
| [`should_transition()`](src/components/clock/two_phase_clock.rs:137) | 137 | `self.last_transition.elapsed()` | Check if phase_time elapsed |
| [`perform_transition()`](src/components/clock/two_phase_clock.rs:160) | 160 | `self.last_transition = Instant::now()` | Record transition time |
| [`run()`](src/components/clock/two_phase_clock.rs:203) | 203 | `thread::sleep(Duration::from_micros(100))` | Fixed 100µs busy-wait |
### B2. Required Changes
#### B2.1 — Add `VirtualClock` field (optional, gradual integration)
```rust
// BEFORE (line 11-21):
pub struct TwoPhaseClock {
base: BaseComponent,
phi1_state: PinValue,
phi2_state: PinValue,
last_transition: Instant,
phase_time: Duration,
enabled: bool,
paused: bool,
pause_start: Option<Instant>,
frequency: f64,
}
// AFTER:
pub struct TwoPhaseClock {
base: BaseComponent,
phi1_state: PinValue,
phi2_state: PinValue,
last_transition_vtime: Duration, // Virtual time of last phase swap
phase_time: Duration,
enabled: bool,
paused: bool,
pause_start_vtime: Option<Duration>, // Virtual time when pause started
frequency: f64,
virtual_clock: Option<Arc<VirtualClock>>, // Injected in Phase 7
}
```
#### B2.2 — Update constructor (line 24-45)
```rust
// BEFORE:
TwoPhaseClock {
base: BaseComponent::new(name, pins),
phi1_state: PinValue::High,
phi2_state: PinValue::Low,
last_transition: Instant::now(),
phase_time,
enabled: true,
paused: false,
pause_start: None,
frequency,
}
// AFTER:
TwoPhaseClock {
base: BaseComponent::new(name, pins),
phi1_state: PinValue::High,
phi2_state: PinValue::Low,
last_transition_vtime: Duration::ZERO,
phase_time,
enabled: true,
paused: false,
pause_start_vtime: None,
frequency,
virtual_clock: None,
}
```
#### B2.3 — Add `set_virtual_clock()` and `current_virtual_time()` helpers
```rust
impl TwoPhaseClock {
/// Inject a VirtualClock for virtual-time-based operation.
pub fn set_virtual_clock(&mut self, clock: Arc<VirtualClock>) {
self.virtual_clock = Some(clock);
}
/// Get current virtual time, or Duration::ZERO if no VirtualClock.
fn current_virtual_time(&self) -> Duration {
self.virtual_clock.as_ref().map_or(Duration::ZERO, |vc| vc.now())
}
}
```
#### B2.4 — Update `enable()` (line 60-69)
```rust
// BEFORE:
pub fn enable(&mut self) {
self.enabled = true;
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
self.last_transition = Instant::now();
self.update_outputs();
self.update_outputs();
}
// AFTER:
pub fn enable(&mut self) {
self.enabled = true;
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
self.last_transition_vtime = self.current_virtual_time();
self.update_outputs();
self.update_outputs();
}
```
#### B2.5 — Update `pause()` and `resume()` (lines 79-95)
Same pattern as GenericClock:
```rust
pub fn pause(&mut self) {
self.paused = true;
self.pause_start_vtime = Some(self.current_virtual_time());
}
pub fn resume(&mut self) {
if let Some(pause_start_vtime) = self.pause_start_vtime {
let pause_duration = self.current_virtual_time().saturating_sub(pause_start_vtime);
self.last_transition_vtime = self.last_transition_vtime.saturating_add(pause_duration);
}
self.paused = false;
self.pause_start_vtime = None;
}
```
#### B2.6 — Update `should_transition()` (line 136-138)
```rust
// BEFORE:
fn should_transition(&self) -> bool {
self.last_transition.elapsed() >= self.phase_time
}
// AFTER:
fn should_transition(&self, virtual_time: Duration) -> bool {
let elapsed = virtual_time.saturating_sub(self.last_transition_vtime);
elapsed >= self.phase_time
}
```
#### B2.7 — Update `perform_transition()` (line 140-161)
```rust
// BEFORE:
fn perform_transition(&mut self) {
match (self.phi1_state, self.phi2_state) {
(PinValue::High, PinValue::Low) => {
self.phi1_state = PinValue::Low;
self.phi2_state = PinValue::High;
}
(PinValue::Low, PinValue::High) => {
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
}
_ => {
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
}
}
self.update_outputs();
self.last_transition = Instant::now();
}
// AFTER:
fn perform_transition(&mut self, virtual_time: Duration) {
match (self.phi1_state, self.phi2_state) {
(PinValue::High, PinValue::Low) => {
self.phi1_state = PinValue::Low;
self.phi2_state = PinValue::High;
}
(PinValue::Low, PinValue::High) => {
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
}
_ => {
self.phi1_state = PinValue::High;
self.phi2_state = PinValue::Low;
}
}
self.update_outputs();
self.last_transition_vtime = virtual_time;
}
```
#### B2.8 — Implement `update_at_time()` and update `update()`
```rust
impl Component for TwoPhaseClock {
// ... existing name/pins/get_pin unchanged ...
fn update_at_time(&mut self, virtual_time: Duration) {
if self.paused {
return;
}
if !self.enabled {
return;
}
self.update_outputs();
if self.should_transition(virtual_time) {
self.perform_transition(virtual_time);
}
}
fn update(&mut self) {
let vtime = self.current_virtual_time();
self.update_at_time(vtime);
}
// ... rest unchanged ...
}
```
#### B2.9 — Rewrite `run()` to use `wait_until()` (line 195-212)
```rust
// BEFORE:
fn run(&mut self) {
self.base.set_running(true);
self.enable();
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(100));
if !self.is_running() {
break;
}
}
self.disable();
}
// AFTER:
fn run(&mut self) {
self.base.set_running(true);
self.enable();
let vclock = match self.virtual_clock.as_ref() {
Some(vc) => Arc::clone(vc),
None => return self.run_legacy(),
};
while self.is_running() {
let next_transition = self.last_transition_vtime + self.phase_time;
match vclock.wait_until(next_transition) {
Some(current_vtime) => {
if !self.is_running() {
break;
}
self.update_at_time(current_vtime);
}
None => break,
}
}
self.disable();
}
```
Add legacy helper:
```rust
impl TwoPhaseClock {
/// Legacy run loop using wall-clock time (backward compatibility).
fn run_legacy(&mut self) {
while self.is_running() {
self.update();
thread::sleep(Duration::from_micros(100));
if !self.is_running() {
break;
}
}
self.disable();
}
}
```
#### B2.10 — Add `timing()` override
```rust
fn timing(&self) -> ComponentTiming {
ComponentTiming {
name: "Two-Phase Clock",
access_time: Duration::ZERO,
propagation_delay: Duration::ZERO,
setup_time: Duration::ZERO,
hold_time: Duration::ZERO,
max_clock_frequency: f64::INFINITY,
min_update_interval: self.phase_time,
}
}
```
#### B2.11 — Update imports
```rust
// BEFORE (line 1-9):
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crate::component::{BaseComponent, Component};
use crate::pin::{Pin, PinValue};
// AFTER:
use parking_lot::Mutex;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::component::{BaseComponent, Component};
use crate::component_timing::ComponentTiming;
use crate::pin::{Pin, PinValue};
use crate::virtual_clock::VirtualClock;
```
### B3. TwoPhaseClock Test Impact
No tests exist for `TwoPhaseClock` (no `#[cfg(test)]` module in the file). No test breakage risk.
---
## Part C: System Config Changes
### C1. Factory Closure Updates
In [`system_config.rs`](src/system_config.rs:241-277), the factory closures create clocks without a `VirtualClock`. In Phase 5, these remain unchanged — the `virtual_clock` field is `None` by default, and clocks use the legacy `run_legacy()` path.
In **Phase 7**, `ConfigurableSystem` will:
1. Create `Arc<VirtualClock>`
2. After constructing each clock, call `set_virtual_clock(Arc::clone(&virtual_clock))`
3. This requires changing the factory closure signature or adding a post-construction hook
For Phase 5, no changes to [`system_config.rs`](src/system_config.rs) are needed.
---
## Part D: Backward Compatibility Analysis
### D1. What stays the same
| Aspect | Compatibility |
|--------|--------------|
| `new(name, frequency)` constructor signature | **Unchanged** — no new required parameters |
| `Component` trait impl | **Unchanged** — `update()` still works via delegation |
| `enable()` / `disable()` / `pause()` / `resume()` | **Unchanged** — public API identical |
| `set_frequency()` / `get_frequency()` | **Unchanged** |
| Pin names (CLK, PHI1, PHI2, ENABLE) | **Unchanged** |
| `run()` without VirtualClock | **Works** — falls back to `run_legacy()` with `thread::sleep()` |
| Existing tests | **Pass** — no Instant dependency in test assertions |
### D2. What changes (internal only)
| Change | Visible externally? |
|--------|-------------------|
| `last_transition: Instant` → `last_transition_vtime: Duration` | No — private field |
| `pause_start: Option<Instant>` → `pause_start_vtime: Option<Duration>` | No — private field |
| `should_transition()` takes `Duration` param | No — private method |
| `perform_transition()` takes `Duration` param | No — private method |
| New `virtual_clock: Option<Arc<VirtualClock>>` field | No — private, `None` by default |
| New `set_virtual_clock()` method | Yes — but opt-in, not required |
### D3. Risk assessment
**Risk: LOW.** All changes are internal field replacements. The public API is unchanged. The `Option<Arc<VirtualClock>>` pattern ensures clocks work identically without a VirtualClock injected.
---
## Part E: Execution Checklist
These are the implementation steps in order:
- [ ] **E1.** Update `GenericClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming`
- [ ] **E2.** Replace `GenericClock` struct fields: `last_transition: Instant` → `last_transition_vtime: Duration`, `pause_start: Option<Instant>` → `pause_start_vtime: Option<Duration>`, add `virtual_clock: Option<Arc<VirtualClock>>`
- [ ] **E3.** Update `GenericClock::new()` — initialize `last_transition_vtime: Duration::ZERO`, `virtual_clock: None`
- [ ] **E4.** Add `GenericClock::set_virtual_clock()` and `GenericClock::current_virtual_time()` helper methods
- [ ] **E5.** Update `GenericClock::enable()` — use `current_virtual_time()` instead of `Instant::now()`
- [ ] **E6.** Update `GenericClock::pause()` — use `current_virtual_time()` instead of `Instant::now()`
- [ ] **E7.** Update `GenericClock::resume()` — compute pause duration from virtual time deltas
- [ ] **E8.** Update `GenericClock::should_transition()` — accept `virtual_time: Duration` parameter
- [ ] **E9.** Update `GenericClock::perform_transition()` — accept `virtual_time: Duration`, store in `last_transition_vtime`
- [ ] **E10.** Implement `GenericClock::update_at_time()` in `Component` impl
- [ ] **E11.** Update `GenericClock::update()` to delegate to `update_at_time(current_virtual_time())`
- [ ] **E12.** Add `GenericClock::next_transition_time()` and `GenericClock::run_legacy()` helper methods
- [ ] **E13.** Rewrite `GenericClock::run()` — use `wait_until()` when VirtualClock present, else `run_legacy()`
- [ ] **E14.** Add `GenericClock::timing()` override in `Component` impl
- [ ] **E15.** Verify `GenericClock` existing tests still pass
- [ ] **E16.** Update `TwoPhaseClock` imports — remove `Instant`, add `VirtualClock` + `ComponentTiming`
- [ ] **E17.** Replace `TwoPhaseClock` struct fields: same pattern as GenericClock
- [ ] **E18.** Update `TwoPhaseClock::new()` — initialize virtual time fields
- [ ] **E19.** Add `TwoPhaseClock::set_virtual_clock()` and `TwoPhaseClock::current_virtual_time()` helpers
- [ ] **E20.** Update `TwoPhaseClock::enable()` — use `current_virtual_time()`
- [ ] **E21.** Update `TwoPhaseClock::pause()` and `resume()` — virtual time deltas
- [ ] **E22.** Update `TwoPhaseClock::should_transition()` — accept `virtual_time: Duration`
- [ ] **E23.** Update `TwoPhaseClock::perform_transition()` — accept `virtual_time: Duration`
- [ ] **E24.** Implement `TwoPhaseClock::update_at_time()` in `Component` impl
- [ ] **E25.** Update `TwoPhaseClock::update()` to delegate to `update_at_time()`
- [ ] **E26.** Add `TwoPhaseClock::run_legacy()` helper
- [ ] **E27.** Rewrite `TwoPhaseClock::run()` — use `wait_until()` when VirtualClock present
- [ ] **E28.** Add `TwoPhaseClock::timing()` override in `Component` impl
- [ ] **E29.** Run `cargo build` to verify compilation
- [ ] **E30.** Run `cargo test` to verify all existing tests pass
---
## Flow Diagram: Clock Thread with Virtual Time
```mermaid
flowchart TD
A[run called] --> B{virtual_clock set?}
B -->|No| C[run_legacy - thread::sleep loop]
B -->|Yes| D[enable - reset phase state]
D --> E[Calculate next_transition_time]
E --> F[wait_until next_transition]
F --> G{wait_until returned}
G -->|None - stopped| H[disable and exit]
G -->|Some vtime| I{is_running?}
I -->|No| H
I -->|Yes| J[update_at_time with vtime]
J --> K[Transition phase if needed]
K --> E
```
## Flow Diagram: Phase Transition Logic in Virtual Time
```mermaid
flowchart LR
subgraph GenericClock
G1[last_transition_vtime = 0ns] --> G2{vtime >= high_time?}
G2 -->|Yes| G3[High to Low transition]
G3 --> G4[last_transition_vtime = vtime]
G4 --> G5{vtime >= low_time?}
G5 -->|Yes| G6[Low to High transition]
G6 --> G4
end
subgraph TwoPhaseClock
T1[last_transition_vtime = 0ns] --> T2{elapsed >= phase_time?}
T2 -->|Yes| T3[PHI1 High+PHI2 Low to PHI1 Low+PHI2 High]
T3 --> T4[last_transition_vtime = vtime]
T4 --> T5{elapsed >= phase_time?}
T5 -->|Yes| T6[PHI1 Low+PHI2 High to PHI1 High+PHI2 Low]
T6 --> T4
end
```