Skip to main content

harn_vm/vm/
module_phase_timing.rs

1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use parking_lot::Mutex;
5use serde::{Deserialize, Serialize};
6
7use super::Vm;
8
9/// VM-scoped cumulative work-time and cardinality for module preparation and loading.
10///
11/// Concurrent child-VM spans are additive, so durations can exceed enclosing
12/// wall time. The record is attribution, not another top-level phase clock.
13#[non_exhaustive]
14#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
15pub struct ModulePhaseStats {
16    /// Wall time spent compiling module sources after cache misses.
17    pub module_compile_ms: u64,
18    /// Wall time spent reading, hydrating, instantiating, and exporting modules.
19    pub module_load_ms: u64,
20    /// Module source compilations that completed successfully.
21    pub modules_compiled: u64,
22    /// Successful first loads across the VMs in this execution tree.
23    pub modules_loaded: u64,
24}
25
26impl ModulePhaseStats {
27    /// Add two snapshots without overflowing counters or durations.
28    pub fn saturating_add(self, other: Self) -> Self {
29        Self {
30            module_compile_ms: self
31                .module_compile_ms
32                .saturating_add(other.module_compile_ms),
33            module_load_ms: self.module_load_ms.saturating_add(other.module_load_ms),
34            modules_compiled: self.modules_compiled.saturating_add(other.modules_compiled),
35            modules_loaded: self.modules_loaded.saturating_add(other.modules_loaded),
36        }
37    }
38}
39
40#[derive(Debug, Default)]
41struct ModulePhaseAccumulator {
42    compile: Duration,
43    load: Duration,
44    modules_compiled: u64,
45    modules_loaded: u64,
46}
47
48/// Opt-in recorder shared by a root VM and the child VMs it creates.
49#[derive(Clone, Debug, Default)]
50pub struct ModulePhaseRecorder {
51    inner: Arc<Mutex<ModulePhaseAccumulator>>,
52}
53
54impl ModulePhaseRecorder {
55    /// Create an empty recorder independent of any VM.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Return a stable value snapshot of the accumulated module work.
61    pub fn snapshot(&self) -> ModulePhaseStats {
62        let stats = self.inner.lock();
63        ModulePhaseStats {
64            module_compile_ms: duration_ms(stats.compile),
65            module_load_ms: duration_ms(stats.load),
66            modules_compiled: stats.modules_compiled,
67            modules_loaded: stats.modules_loaded,
68        }
69    }
70
71    pub(crate) fn compile_span(&self) -> ModulePhaseSpan {
72        ModulePhaseSpan::new(self.clone(), ModulePhase::Compile)
73    }
74
75    pub(crate) fn load_span(&self) -> ModulePhaseSpan {
76        ModulePhaseSpan::new(self.clone(), ModulePhase::Load)
77    }
78
79    pub(crate) fn record_module_loaded(&self) {
80        let mut stats = self.inner.lock();
81        stats.modules_loaded = stats.modules_loaded.saturating_add(1);
82    }
83}
84
85#[derive(Clone, Copy)]
86enum ModulePhase {
87    Compile,
88    Load,
89}
90
91pub(crate) struct ModulePhaseSpan {
92    recorder: ModulePhaseRecorder,
93    phase: ModulePhase,
94    started: Instant,
95    successful_compile: bool,
96}
97
98impl ModulePhaseSpan {
99    fn new(recorder: ModulePhaseRecorder, phase: ModulePhase) -> Self {
100        Self {
101            recorder,
102            phase,
103            started: Instant::now(),
104            successful_compile: false,
105        }
106    }
107
108    pub(crate) fn mark_compile_succeeded(&mut self) {
109        debug_assert!(matches!(self.phase, ModulePhase::Compile));
110        self.successful_compile = true;
111    }
112}
113
114impl Drop for ModulePhaseSpan {
115    fn drop(&mut self) {
116        let elapsed = self.started.elapsed();
117        let mut stats = self.recorder.inner.lock();
118        match self.phase {
119            ModulePhase::Compile => {
120                stats.compile = stats.compile.saturating_add(elapsed);
121                if self.successful_compile {
122                    stats.modules_compiled = stats.modules_compiled.saturating_add(1);
123                }
124            }
125            ModulePhase::Load => stats.load = stats.load.saturating_add(elapsed),
126        }
127    }
128}
129
130fn duration_ms(duration: Duration) -> u64 {
131    duration.as_millis().min(u128::from(u64::MAX)) as u64
132}
133
134impl Vm {
135    /// Enable module phase timing and return the recorder owned by this VM tree.
136    ///
137    /// Calling this more than once preserves the current recording session.
138    pub fn enable_module_phase_timing(&mut self) -> ModulePhaseRecorder {
139        self.module_phase_recorder
140            .get_or_insert_with(ModulePhaseRecorder::new)
141            .clone()
142    }
143
144    pub(crate) fn module_compile_span(&self) -> Option<ModulePhaseSpan> {
145        self.module_phase_recorder
146            .as_ref()
147            .map(ModulePhaseRecorder::compile_span)
148    }
149
150    pub(crate) fn module_load_span(&self) -> Option<ModulePhaseSpan> {
151        self.module_phase_recorder
152            .as_ref()
153            .map(ModulePhaseRecorder::load_span)
154    }
155
156    pub(crate) fn record_module_loaded(&self) {
157        if let Some(recorder) = &self.module_phase_recorder {
158            recorder.record_module_loaded();
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn stats_addition_saturates_each_field() {
169        let left = ModulePhaseStats {
170            module_compile_ms: u64::MAX,
171            module_load_ms: 2,
172            modules_compiled: 3,
173            modules_loaded: u64::MAX,
174        };
175        let right = ModulePhaseStats {
176            module_compile_ms: 1,
177            module_load_ms: 4,
178            modules_compiled: 5,
179            modules_loaded: 1,
180        };
181
182        assert_eq!(
183            left.saturating_add(right),
184            ModulePhaseStats {
185                module_compile_ms: u64::MAX,
186                module_load_ms: 6,
187                modules_compiled: 8,
188                modules_loaded: u64::MAX,
189            }
190        );
191    }
192
193    #[test]
194    fn child_vms_share_recorder_but_baselines_start_disabled() {
195        let mut vm = Vm::new();
196        assert!(vm.module_phase_recorder.is_none());
197
198        let recorder = vm.enable_module_phase_timing();
199        let child = vm.child_vm();
200        std::thread::spawn(move || child.record_module_loaded())
201            .join()
202            .expect("child records from another thread");
203
204        assert_eq!(recorder.snapshot().modules_loaded, 1);
205        assert!(vm.baseline().instantiate().module_phase_recorder.is_none());
206    }
207
208    #[test]
209    fn cancelled_span_releases_its_recorder_handle() {
210        let runtime = tokio::runtime::Builder::new_current_thread()
211            .enable_all()
212            .build()
213            .expect("runtime builds");
214        let recorder = ModulePhaseRecorder::new();
215        let task_recorder = recorder.clone();
216
217        runtime.block_on(async {
218            let (started_tx, started_rx) = tokio::sync::oneshot::channel();
219            let task = tokio::spawn(async move {
220                let _span = task_recorder.load_span();
221                let _ = started_tx.send(());
222                std::future::pending::<()>().await;
223            });
224            started_rx.await.expect("span starts");
225            task.abort();
226            assert!(task.await.expect_err("task is cancelled").is_cancelled());
227        });
228
229        assert_eq!(Arc::strong_count(&recorder.inner), 1);
230    }
231
232    #[test]
233    fn nested_module_loads_are_additive_across_real_child_vms() {
234        let runtime = tokio::runtime::Builder::new_current_thread()
235            .enable_all()
236            .build()
237            .expect("runtime builds");
238        let temp = tempfile::tempdir().expect("tempdir");
239        let dependency = temp.path().join("dependency.harn");
240        let importer = temp.path().join("importer.harn");
241        std::fs::write(&dependency, "pub fn value() { return 42 }\n").expect("write dependency");
242        std::fs::write(
243            &importer,
244            "import { value } from \"./dependency\"\npub fn answer() { return value() }\n",
245        )
246        .expect("write importer");
247
248        let mut vm = Vm::new();
249        let recorder = vm.enable_module_phase_timing();
250        let mut first = vm.child_vm();
251        let mut second = vm.child_vm();
252
253        runtime.block_on(async {
254            first
255                .load_module_exports(&importer)
256                .await
257                .expect("first nested load succeeds");
258            assert_eq!(recorder.snapshot().modules_loaded, 2);
259
260            second
261                .load_module_exports(&importer)
262                .await
263                .expect("second nested load succeeds");
264        });
265
266        assert_eq!(recorder.snapshot().modules_loaded, 4);
267    }
268}