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#[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 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 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
169pub(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 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 #[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}