cove_runtime/runtime.rs
1//! What every thread of one run shares.
2//!
3//! ADR 0008 runs each spawned task on its own thread, so everything a task
4//! body needs has to outlive the stack frame that spawned it and be reachable
5//! from another thread: the resolved program the body resolves names in, the
6//! source map a diagnostic points into, the host boundary the body calls
7//! through, and where trace events go.
8//!
9//! Every one of them is either immutable or synchronized by its owner, so a
10//! `Runtime` is shared rather than copied: cloning one hands a task thread a
11//! handle to the same program, the same hosts, and the same trace.
12
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15
16use cove_diag::SourceMap;
17use cove_sema::resolve::Program;
18
19use crate::heap::HeapStats;
20use crate::host::HostRegistry;
21use crate::trace::{NullSink, TraceEvent, TraceSink};
22
23/// The task id the entry runs under.
24///
25/// The entry is not a spawned task and has no id of its own, so it takes the
26/// one id [`Runtime::next_task_id`] never hands out. Every event that names a
27/// task names it this way, so a trace has one convention for "which task"
28/// rather than one per event.
29pub const ENTRY_TASK: u64 = 0;
30
31/// The shared context of one run.
32#[derive(Clone)]
33pub struct Runtime {
34 program: Arc<Program>,
35 sources: Arc<SourceMap>,
36 hosts: Arc<HostRegistry>,
37 trace: Arc<dyn TraceSink>,
38 /// The next id [`Runtime::next_task_id`] hands out. Task ids are a trace
39 /// identity, so they are drawn from one counter for the whole run: two
40 /// tasks spawned at the same time on different threads still get
41 /// different ids.
42 next_task_id: Arc<AtomicU64>,
43 /// What every heap of this run has done, folded in as each one is retired.
44 ///
45 /// A heap belongs to one thread and is never shared, so nothing is
46 /// contended here while a task runs: a thread accumulates locally and
47 /// takes this lock once, when its heap ends.
48 heap: Arc<Mutex<HeapStats>>,
49}
50
51impl Runtime {
52 /// A run over `program`, reporting against `sources` and calling through
53 /// `hosts`, with tracing switched off.
54 pub fn new(program: Arc<Program>, sources: Arc<SourceMap>, hosts: Arc<HostRegistry>) -> Self {
55 Runtime {
56 program,
57 sources,
58 hosts,
59 trace: Arc::new(NullSink),
60 next_task_id: Arc::new(AtomicU64::new(1)),
61 heap: Arc::new(Mutex::new(HeapStats::default())),
62 }
63 }
64
65 /// Sends this run's trace events to `sink`. Replaces any sink installed
66 /// earlier; the default is [`NullSink`], which discards everything.
67 ///
68 /// This is where every event but one goes: task lifecycle
69 /// (`TaskSpawned`, `TaskCompleted`, `TaskCancelled`), a heap's
70 /// (`HeapCollected`, `HeapSummary`), and the entry's own (`EntryEnter`,
71 /// `EntryExit`, `RunEnded`). The one exception is
72 /// [`TraceEvent::HostCall`], which reports through
73 /// [`HostRegistry::set_trace`](crate::HostRegistry::set_trace) instead —
74 /// a separate sink on a separate object, defaulting to its own
75 /// `NullSink`. An embedding that installs a sink here and expects host
76 /// calls on the same tape gets a trace with everything but those, and no
77 /// diagnostic saying the other sink was never installed.
78 pub fn with_trace(mut self, sink: Arc<dyn TraceSink>) -> Self {
79 self.trace = sink;
80 self
81 }
82
83 pub fn program(&self) -> &Program {
84 &self.program
85 }
86
87 pub fn sources(&self) -> &SourceMap {
88 &self.sources
89 }
90
91 /// The host boundary, so a caller can read a run's counters after it
92 /// finishes.
93 pub fn hosts(&self) -> &HostRegistry {
94 &self.hosts
95 }
96
97 /// Records one trace event, from whichever thread produced it.
98 pub fn trace(&self, event: TraceEvent) {
99 self.trace.record(event);
100 }
101
102 /// The next task id, unique across every thread of this run, and never
103 /// [`ENTRY_TASK`].
104 pub fn next_task_id(&self) -> u64 {
105 self.next_task_id.fetch_add(1, Ordering::Relaxed)
106 }
107
108 /// Folds a finished heap's totals into the run's.
109 ///
110 /// Only the counters are folded. What a retired heap last measured as live
111 /// went with the thread that owned it, so summing those would report
112 /// memory that no longer exists; see [`Interpreter::heap_stats`] for where
113 /// the live figure comes from instead.
114 ///
115 /// [`Interpreter::heap_stats`]: crate::interp::Interpreter::heap_stats
116 pub fn retire_heap(&self, stats: &HeapStats) {
117 self.locked_heap().merge(stats);
118 }
119
120 /// What every heap retired so far has done.
121 ///
122 /// Only [`Interpreter`], the tree-walking backend, ever calls
123 /// [`Runtime::retire_heap`]. A run on [`Vm`] never does, so this stays at
124 /// `HeapStats::default()` for the whole of a VM run — not because the VM
125 /// is not counting, but because it counts memory in words rather than
126 /// the bytes and objects this struct holds, and there is no honest way to
127 /// fold one into the other. Reporting a zero here for a VM run would read
128 /// as a session that allocated nothing, which is worse than not
129 /// answering at all.
130 ///
131 /// A `Vm` embedder reads
132 /// [`heap_words`](crate::Vm::heap_words),
133 /// [`allocated_words`](crate::Vm::allocated_words),
134 /// [`collections`](crate::Vm::collections) and
135 /// [`live_words`](crate::Vm::live_words) instead — see
136 /// [`Vm::live_words`](crate::Vm::live_words) for which of those answers
137 /// "does this run still hold what it allocated".
138 ///
139 /// [`Interpreter`]: crate::interp::Interpreter
140 /// [`Vm`]: crate::Vm
141 pub fn heap_stats(&self) -> HeapStats {
142 *self.locked_heap()
143 }
144
145 /// A poisoned lock means a thread panicked while folding its totals in.
146 /// Statistics are not a state anything recovers from, so the numbers are
147 /// taken back rather than turned into a second, unrelated failure.
148 fn locked_heap(&self) -> std::sync::MutexGuard<'_, HeapStats> {
149 self.heap
150 .lock()
151 .unwrap_or_else(|poisoned| poisoned.into_inner())
152 }
153}