harn_vm/vm/
module_phase_timing.rs1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use parking_lot::Mutex;
5use serde::{Deserialize, Serialize};
6
7use super::Vm;
8
9#[non_exhaustive]
14#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
15pub struct ModulePhaseStats {
16 pub module_compile_ms: u64,
18 pub module_load_ms: u64,
20 pub modules_compiled: u64,
22 pub modules_loaded: u64,
24}
25
26impl ModulePhaseStats {
27 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#[derive(Clone, Debug, Default)]
50pub struct ModulePhaseRecorder {
51 inner: Arc<Mutex<ModulePhaseAccumulator>>,
52}
53
54impl ModulePhaseRecorder {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 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 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}