Skip to main content

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}