Skip to main content

moire_web/snapshot/
table.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::sync::{Mutex, OnceLock};
4
5use moire_trace_types::{BacktraceId, FrameId, RelPc};
6use moire_types::{
7    BacktraceFrameResolved, BacktraceFrameUnresolved, SnapshotBacktrace, SnapshotBacktraceFrame,
8    SnapshotCutResponse, SnapshotFrameRecord,
9};
10
11use crate::db::Db;
12use crate::snapshot::repository::load_backtrace_frame_batches;
13use crate::util::source_path::resolve_source_path;
14
15pub struct SnapshotBacktraceTable {
16    pub backtraces: Vec<SnapshotBacktrace>,
17    pub frames: Vec<SnapshotFrameRecord>,
18}
19
20pub fn collect_snapshot_backtrace_ids(snapshot: &SnapshotCutResponse) -> Vec<BacktraceId> {
21    let mut backtrace_ids = Vec::new();
22    for process in &snapshot.processes {
23        for entity in &process.snapshot.entities {
24            backtrace_ids.push(entity.backtrace);
25        }
26        for scope in &process.snapshot.scopes {
27            backtrace_ids.push(scope.backtrace);
28        }
29        for edge in &process.snapshot.edges {
30            backtrace_ids.push(edge.backtrace);
31        }
32        for event in &process.snapshot.events {
33            backtrace_ids.push(event.backtrace);
34        }
35    }
36    backtrace_ids.sort_unstable();
37    backtrace_ids.dedup();
38    backtrace_ids
39}
40
41pub fn is_pending_frame(frame: &SnapshotBacktraceFrame) -> bool {
42    matches!(
43        frame,
44        SnapshotBacktraceFrame::Unresolved(BacktraceFrameUnresolved { reason, .. })
45        if reason == "symbolication pending"
46    )
47}
48
49#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
50struct FrameDedupKey {
51    module_identity: String,
52    module_path: String,
53    rel_pc: RelPc,
54}
55
56pub async fn load_snapshot_backtrace_table(
57    db: Arc<Db>,
58    backtrace_ids: &[BacktraceId],
59) -> SnapshotBacktraceTable {
60    if backtrace_ids.is_empty() {
61        return SnapshotBacktraceTable {
62            backtraces: vec![],
63            frames: vec![],
64        };
65    }
66
67    let backtrace_ids = backtrace_ids.to_vec();
68    tokio::task::spawn_blocking(move || load_snapshot_backtrace_table_blocking(&db, &backtrace_ids))
69        .await
70        .unwrap_or_else(|error| panic!("join snapshot backtrace loader: {error}"))
71        .unwrap_or_else(|error| panic!("load snapshot backtrace table: {error}"))
72}
73
74fn load_snapshot_backtrace_table_blocking(
75    db: &Db,
76    backtrace_ids: &[BacktraceId],
77) -> Result<SnapshotBacktraceTable, String> {
78    // r[impl api.snapshot.frame-catalog]
79    let batches = load_backtrace_frame_batches(db, backtrace_ids)?;
80
81    let mut frame_id_by_key: BTreeMap<FrameDedupKey, FrameId> = BTreeMap::new();
82    let mut frame_by_id: BTreeMap<FrameId, SnapshotBacktraceFrame> = BTreeMap::new();
83    let mut backtrace_entries: Vec<SnapshotBacktrace> = Vec::with_capacity(batches.len());
84
85    for batch in batches {
86        let mut this_frame_ids: Vec<FrameId> = Vec::with_capacity(batch.raw_rows.len());
87        for raw in batch.raw_rows {
88            let frame = match batch.symbolicated_by_index.get(&raw.frame_index) {
89                Some(sym) if sym.status == "resolved" => {
90                    match (sym.function_name.as_ref(), sym.source_file_path.as_ref()) {
91                        (Some(function_name), Some(source_file)) => {
92                            SnapshotBacktraceFrame::Resolved(BacktraceFrameResolved {
93                                module_path: sym.module_path.clone(),
94                                function_name: function_name.clone(),
95                                source_file: resolve_source_path(source_file).into_owned(),
96                                line: sym.source_line.and_then(|line| u32::try_from(line).ok()),
97                            })
98                        }
99                        _ => SnapshotBacktraceFrame::Unresolved(BacktraceFrameUnresolved {
100                            module_path: sym.module_path.clone(),
101                            rel_pc: sym.rel_pc,
102                            reason: String::from(
103                                "resolved symbolication row missing function/source fields",
104                            ),
105                        }),
106                    }
107                }
108                Some(sym) => SnapshotBacktraceFrame::Unresolved(BacktraceFrameUnresolved {
109                    module_path: sym.module_path.clone(),
110                    rel_pc: sym.rel_pc,
111                    reason: sym
112                        .unresolved_reason
113                        .clone()
114                        .unwrap_or_else(|| String::from("symbolication unresolved")),
115                }),
116                None => SnapshotBacktraceFrame::Unresolved(BacktraceFrameUnresolved {
117                    module_path: raw.module_path.clone(),
118                    rel_pc: raw.rel_pc,
119                    reason: String::from("symbolication pending"),
120                }),
121            };
122            let key = FrameDedupKey {
123                module_identity: raw.module_identity,
124                module_path: raw.module_path.clone(),
125                rel_pc: raw.rel_pc,
126            };
127            let frame_id = match frame_id_by_key.get(&key) {
128                Some(existing) => {
129                    let existing_frame = frame_by_id.get(existing).cloned().ok_or_else(|| {
130                        format!(
131                            "invariant violated: frame id {} missing from frame map for backtrace {}",
132                            existing,
133                            batch.backtrace_id
134                        )
135                    })?;
136                    let merged = merge_frame_state(&existing_frame, &frame, &key)?;
137                    frame_by_id.insert(*existing, merged);
138                    *existing
139                }
140                None => {
141                    let assigned = stable_frame_id(&key)?;
142                    if let Some(existing_key) =
143                        frame_id_by_key
144                            .iter()
145                            .find_map(|(existing_key, existing_id)| {
146                                if *existing_id == assigned {
147                                    Some(existing_key.clone())
148                                } else {
149                                    None
150                                }
151                            })
152                        && existing_key != key
153                    {
154                        return Err(format!(
155                            "invariant violated: stable frame_id collision id={} existing=({}, {}, {:#x}) incoming=({}, {}, {:#x})",
156                            assigned,
157                            existing_key.module_identity,
158                            existing_key.module_path,
159                            existing_key.rel_pc.get(),
160                            key.module_identity,
161                            key.module_path,
162                            key.rel_pc.get()
163                        ));
164                    }
165                    frame_id_by_key.insert(key, assigned);
166                    frame_by_id.insert(assigned, frame.clone());
167                    assigned
168                }
169            };
170            this_frame_ids.push(frame_id);
171        }
172        backtrace_entries.push(SnapshotBacktrace {
173            backtrace_id: batch.backtrace_id,
174            frame_ids: this_frame_ids,
175        });
176    }
177
178    let frames = frame_by_id
179        .into_iter()
180        .map(|(frame_id, frame)| SnapshotFrameRecord { frame_id, frame })
181        .collect();
182
183    Ok(SnapshotBacktraceTable {
184        backtraces: backtrace_entries,
185        frames,
186    })
187}
188
189fn frame_resolution_rank(frame: &SnapshotBacktraceFrame) -> u8 {
190    match frame {
191        SnapshotBacktraceFrame::Resolved(_) => 2,
192        unresolved @ SnapshotBacktraceFrame::Unresolved(_) => {
193            if is_pending_frame(unresolved) {
194                0
195            } else {
196                1
197            }
198        }
199    }
200}
201
202fn merge_frame_state(
203    existing: &SnapshotBacktraceFrame,
204    incoming: &SnapshotBacktraceFrame,
205    key: &FrameDedupKey,
206) -> Result<SnapshotBacktraceFrame, String> {
207    if existing == incoming {
208        return Ok(existing.clone());
209    }
210
211    match (existing, incoming) {
212        (SnapshotBacktraceFrame::Resolved(a), SnapshotBacktraceFrame::Resolved(b)) if a != b => {
213            return Err(format!(
214                "invariant violated: conflicting resolved symbolication for frame key ({}, {}, {:#x})",
215                key.module_identity,
216                key.module_path,
217                key.rel_pc.get()
218            ));
219        }
220        _ => {}
221    }
222
223    let existing_rank = frame_resolution_rank(existing);
224    let incoming_rank = frame_resolution_rank(incoming);
225    if incoming_rank >= existing_rank {
226        Ok(incoming.clone())
227    } else {
228        Ok(existing.clone())
229    }
230}
231
232static FRAME_ID_REGISTRY: OnceLock<Mutex<BTreeMap<FrameDedupKey, FrameId>>> = OnceLock::new();
233
234/// Given a raw FrameId u64 value, return the `(frame_id, module_identity, rel_pc)` triple
235/// that was stored in the global registry when this frame was first seen.
236/// Returns `None` if the frame_id is not in the registry.
237// r[impl api.source.preview.security]
238pub fn lookup_frame_source_by_raw(raw_id: u64) -> Option<(FrameId, String, RelPc)> {
239    let registry = FRAME_ID_REGISTRY.get()?;
240    let guard = registry.lock().ok()?;
241    guard
242        .iter()
243        .find(|(_, id)| id.as_u64() == raw_id)
244        .map(|(key, id)| (*id, key.module_identity.clone(), key.rel_pc))
245}
246
247fn stable_frame_id(key: &FrameDedupKey) -> Result<FrameId, String> {
248    let registry = FRAME_ID_REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()));
249    let mut guard = registry
250        .lock()
251        .map_err(|_| String::from("invariant violated: frame id registry mutex poisoned"))?;
252    if let Some(existing) = guard.get(key).copied() {
253        return Ok(existing);
254    }
255    let frame_id = FrameId::next().map_err(|error| format!("invariant violated: {error}"))?;
256    guard.insert(key.clone(), frame_id);
257    Ok(frame_id)
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    // r[verify api.snapshot.frame-id-stable]
265    #[test]
266    fn stable_frame_id_is_deterministic_and_js_safe() {
267        let key_a = FrameDedupKey {
268            module_identity: String::from("debug_id:abc"),
269            module_path: String::from("/tmp/a"),
270            rel_pc: RelPc::new(0x1234).expect("valid rel_pc"),
271        };
272        let key_b = FrameDedupKey {
273            module_identity: String::from("debug_id:abc"),
274            module_path: String::from("/tmp/a"),
275            rel_pc: RelPc::new(0x5678).expect("valid rel_pc"),
276        };
277
278        let a1 = stable_frame_id(&key_a).expect("frame id for key_a");
279        let a2 = stable_frame_id(&key_a).expect("frame id for key_a (repeat)");
280        let b = stable_frame_id(&key_b).expect("frame id for key_b");
281
282        assert_eq!(a1, a2, "same frame key must map to same frame_id");
283        assert_ne!(
284            a1, b,
285            "different frame keys should map to different frame_id"
286        );
287        assert!(format!("{a1}").starts_with("FRAME#"));
288        assert!(format!("{b}").starts_with("FRAME#"));
289    }
290}