Skip to main content

moire_runtime/
lib.rs

1use ctor::ctor;
2use moire_trace_capture::{
3    CaptureOptions, CapturedBacktrace, capture_current, validate_frame_pointers_or_panic,
4};
5use moire_trace_types::{BacktraceId, FrameKey, ModuleId, RelPc, RuntimeBase};
6use moire_types::{
7    AetherEntity, Entity, EntityBody, EntityId, Event, EventKind, EventTarget, ProcessId,
8    ProcessScopeBody, ScopeBody, ScopeId, TaskScopeBody, next_process_id,
9};
10use std::cell::RefCell;
11use std::collections::BTreeMap;
12use std::ops::Bound;
13use std::sync::{Mutex as StdMutex, OnceLock};
14
15pub(crate) const MAX_EVENTS: usize = 16_384;
16pub(crate) const MAX_CHANGES_BEFORE_COMPACT: usize = 65_536;
17pub(crate) const COMPACT_TARGET_CHANGES: usize = 8_192;
18pub(crate) const DASHBOARD_PUSH_MAX_CHANGES: u32 = 2048;
19pub(crate) const DASHBOARD_PUSH_INTERVAL_MS: u64 = 100;
20pub(crate) const DASHBOARD_RECONNECT_DELAY_MS: u64 = 500;
21
22tokio::task_local! {
23    pub static FUTURE_CAUSAL_STACK: RefCell<Vec<EntityId>>;
24}
25thread_local! {
26    pub static HELD_MUTEX_STACK: RefCell<Vec<EntityId>> = const { RefCell::new(Vec::new()) };
27}
28
29pub(crate) mod api;
30pub(crate) mod dashboard;
31pub(crate) mod db;
32pub(crate) mod futures;
33pub(crate) mod handles;
34
35pub use self::api::*;
36pub use self::futures::*;
37pub use self::handles::*;
38
39static PROCESS_SCOPE: OnceLock<ScopeHandle> = OnceLock::new();
40static PROCESS_ID: OnceLock<ProcessId> = OnceLock::new();
41static BACKTRACE_RECORDS: OnceLock<StdMutex<BTreeMap<BacktraceId, moire_wire::BacktraceRecord>>> =
42    OnceLock::new();
43static MODULE_STATE: OnceLock<StdMutex<ModuleState>> = OnceLock::new();
44
45#[derive(Default)]
46struct ModuleState {
47    revision: u64,
48    by_key: BTreeMap<(RuntimeBase, String), ModuleId>,
49    by_id: BTreeMap<ModuleId, moire_wire::ModuleManifestEntry>,
50}
51
52// r[impl process.auto-init]
53#[ctor]
54fn init_diagnostics_runtime() {
55    validate_frame_pointers_or_panic();
56    init_runtime_from_macro();
57}
58
59pub fn init_runtime_from_macro() {
60    let process_name = std::env::current_exe().unwrap().display().to_string();
61    PROCESS_SCOPE.get_or_init(|| {
62        ScopeHandle::new(
63            process_name.clone(),
64            ScopeBody::Process(ProcessScopeBody {
65                pid: std::process::id(),
66            }),
67        )
68    });
69    dashboard::init_dashboard_push_loop(&process_name);
70}
71
72pub(crate) fn runtime_process_id() -> ProcessId {
73    PROCESS_ID.get_or_init(next_process_id).clone()
74}
75
76pub(crate) fn capture_backtrace_id() -> BacktraceId {
77    let backtrace_id = BacktraceId::next()
78        .expect("backtrace id invariant violated: generated id must be valid and JS-safe");
79
80    let captured = capture_current(backtrace_id, CaptureOptions::default()).unwrap_or_else(|err| {
81        panic!("failed to capture backtrace for enabled API boundary: {err}")
82    });
83    // r[impl wire.backtrace-record]
84    let remapped = remap_and_register_backtrace(captured);
85    remember_backtrace_record(remapped);
86
87    backtrace_id
88}
89
90fn module_state() -> &'static StdMutex<ModuleState> {
91    MODULE_STATE.get_or_init(|| StdMutex::new(ModuleState::default()))
92}
93
94fn module_identity_for(path: &str, runtime_base: RuntimeBase) -> moire_wire::ModuleIdentity {
95    // Deterministic runtime identity until build-id/debug-id extraction is wired.
96    moire_wire::ModuleIdentity::DebugId(format!("runtime:{:x}:{path}", runtime_base.get()))
97}
98
99fn remap_and_register_backtrace(captured: CapturedBacktrace) -> moire_wire::BacktraceRecord {
100    let Ok(mut modules) = module_state().lock() else {
101        panic!("module state mutex poisoned; cannot continue");
102    };
103
104    let mut local_to_global: BTreeMap<ModuleId, ModuleId> = BTreeMap::new();
105    for module in &captured.modules {
106        let key = (module.runtime_base, module.path.as_str().to_string());
107        let global = if let Some(existing) = modules.by_key.get(&key).copied() {
108            existing
109        } else {
110            let global = ModuleId::next()
111                .expect("invariant violated: generated module id must be valid and JS-safe");
112            modules.by_key.insert(key.clone(), global);
113            modules.by_id.insert(
114                global,
115                moire_wire::ModuleManifestEntry {
116                    module_id: global,
117                    module_path: key.1.clone(),
118                    runtime_base: key.0,
119                    identity: module_identity_for(&key.1, key.0),
120                    arch: std::env::consts::ARCH.to_string(),
121                },
122            );
123            modules.revision = modules.revision.saturating_add(1);
124            global
125        };
126        local_to_global.insert(module.id, global);
127    }
128
129    let remapped_frames = captured
130        .backtrace
131        .frames
132        .iter()
133        .map(|frame| {
134            let module_id = local_to_global
135                .get(&frame.module_id)
136                .copied()
137                .unwrap_or_else(|| {
138                    panic!(
139                        "invariant violated: missing local module mapping for module_id {}",
140                        frame.module_id
141                    )
142                });
143            FrameKey {
144                module_id,
145                rel_pc: RelPc::new(frame.rel_pc.get())
146                    .expect("invariant violated: rel_pc must be JS-safe"),
147            }
148        })
149        .collect();
150
151    moire_wire::BacktraceRecord::new(captured.backtrace.id, remapped_frames)
152        .expect("invariant violated: remapped backtrace must be valid")
153}
154
155pub(crate) fn module_manifest_snapshot() -> (u64, Vec<moire_wire::ModuleManifestEntry>) {
156    let Ok(modules) = module_state().lock() else {
157        panic!("module state mutex poisoned; cannot continue");
158    };
159    (
160        modules.revision,
161        modules.by_id.values().cloned().collect::<Vec<_>>(),
162    )
163}
164
165fn backtrace_records() -> &'static StdMutex<BTreeMap<BacktraceId, moire_wire::BacktraceRecord>> {
166    BACKTRACE_RECORDS.get_or_init(|| StdMutex::new(BTreeMap::new()))
167}
168
169// r[impl wire.backtrace-record]
170pub(crate) fn remember_backtrace_record(record: moire_wire::BacktraceRecord) {
171    let Ok(mut records) = backtrace_records().lock() else {
172        panic!("backtrace record mutex poisoned; cannot continue");
173    };
174    let record_id = record.id;
175    match records.get(&record_id) {
176        Some(existing) if existing == &record => {}
177        Some(_) => panic!(
178            "backtrace record invariant violated: conflicting payload for id {}",
179            record_id
180        ),
181        None => {
182            records.insert(record_id, record);
183        }
184    }
185}
186
187pub(crate) fn backtrace_records_after(
188    last_sent_backtrace_id: Option<BacktraceId>,
189) -> Vec<moire_wire::BacktraceRecord> {
190    let Ok(records) = backtrace_records().lock() else {
191        panic!("backtrace record mutex poisoned; cannot continue");
192    };
193    let lower = match last_sent_backtrace_id {
194        Some(id) => Bound::Excluded(id),
195        None => Bound::Unbounded,
196    };
197    records
198        .range((lower, Bound::Unbounded))
199        .map(|(_, record)| record.clone())
200        .collect()
201}
202
203pub(crate) fn aether_entity_for_current_task() -> Option<EntityId> {
204    let task_key = current_tokio_task_key().unwrap_or_else(|| "main".to_string());
205    let entity_id = EntityId::new(format!("AETHER#{task_key}"));
206    if let Ok(mut db) = db::runtime_db().lock() {
207        if !db.entities.contains_key(&entity_id) {
208            let mut entity = Entity::new(
209                capture_backtrace_id(),
210                format!("aether#{task_key}"),
211                EntityBody::Aether(AetherEntity {
212                    task_id: task_key.clone(),
213                }),
214            );
215            entity.id = entity_id.clone();
216            db.upsert_entity(entity);
217        }
218
219        // Keep fallback actors discoverable via task scopes so they don't float
220        // as unscoped graph poles.
221        let _ = db.link_entity_to_current_task_scope(&entity_id);
222    }
223    Some(entity_id)
224}
225
226pub fn current_process_scope_id() -> Option<ScopeId> {
227    PROCESS_SCOPE
228        .get()
229        .map(|scope| ScopeId::new(scope.id().as_str()))
230}
231
232pub fn current_tokio_task_key() -> Option<String> {
233    tokio::task::try_id().map(|id| id.to_string())
234}
235
236pub struct TaskScopeRegistration {
237    task_key: String,
238    scope: ScopeHandle,
239}
240
241impl Drop for TaskScopeRegistration {
242    fn drop(&mut self) {
243        if let Ok(mut db) = db::runtime_db().lock() {
244            db.unregister_task_scope_id(&self.task_key, self.scope.id());
245        }
246    }
247}
248
249pub fn register_current_task_scope(task_name: &str) -> Option<TaskScopeRegistration> {
250    let task_key = current_tokio_task_key()?;
251    let scope = ScopeHandle::new(
252        format!("task.{task_name}#{task_key}"),
253        ScopeBody::Task(TaskScopeBody {
254            task_key: task_key.clone(),
255        }),
256    );
257    if let Ok(mut db) = db::runtime_db().lock() {
258        db.register_task_scope_id(&task_key, scope.id());
259    }
260    Some(TaskScopeRegistration { task_key, scope })
261}
262
263pub fn new_event(target: EventTarget, kind: EventKind) -> Event {
264    Event::new(target, kind, capture_backtrace_id())
265}
266
267pub fn record_event(event: Event) {
268    if let Ok(mut db) = db::runtime_db().lock() {
269        db.record_event(event);
270    }
271}
272
273pub fn record_custom_event(
274    target: EventTarget,
275    kind: impl Into<String>,
276    display_name: impl Into<String>,
277    payload: moire_types::Json,
278) {
279    let event = new_event(
280        target,
281        EventKind::Custom(moire_types::CustomEventKind {
282            kind: kind.into(),
283            display_name: display_name.into(),
284            payload,
285        }),
286    );
287    record_event(event);
288}
289
290pub fn record_event_with_entity_source(mut event: Event, entity_id: &EntityId) {
291    if let Ok(mut db) = db::runtime_db().lock() {
292        if let Some(entity) = db.entities.get(entity_id) {
293            event.backtrace = entity.backtrace;
294        }
295        db.record_event(event);
296    }
297}
298
299pub fn init_dashboard_push_loop(process_name: &str) {
300    dashboard::init_dashboard_push_loop(process_name)
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    // r[verify model.backtrace.id-layout]
308    #[test]
309    fn backtrace_id_layout_is_js_safe_and_prefixed() {
310        let first = BacktraceId::next().expect("first backtrace id");
311        let second = BacktraceId::next().expect("second backtrace id");
312        assert_ne!(first, second, "backtrace ids must be unique");
313        assert!(
314            format!("{first}").starts_with("BACKTRACE#")
315                && format!("{second}").starts_with("BACKTRACE#")
316        );
317    }
318}