1pub mod disasm;
19
20use std::path::Path;
21
22use anyhow::Result;
23use rusqlite::{OptionalExtension, params};
24
25use super::canonicalizer::{CanonicalSymbol, for_lang};
26use super::{SessionDb, TargetClass};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum CollectTrigger {
33 BreakpointHit,
34 HotspotDrill,
35 Explicit,
36}
37
38impl CollectTrigger {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 CollectTrigger::BreakpointHit => "breakpoint-hit",
42 CollectTrigger::HotspotDrill => "hotspot-drill",
43 CollectTrigger::Explicit => "explicit",
44 }
45 }
46}
47
48pub struct CollectCtx<'a> {
50 pub target: &'a str,
51 pub target_class: TargetClass,
52 pub symbol: &'a str,
55 pub refresh: bool,
58 pub trigger: CollectTrigger,
59 pub cwd: &'a Path,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct DisasmOutput {
67 pub source: &'static str,
68 pub tier: Option<String>,
69 pub code_bytes: Option<i64>,
70 pub asm_text: String,
71 pub asm_lines_json: Option<String>,
72}
73
74pub trait LiveDebugger: Send + Sync {
78 fn send(&self, cmd: &str) -> Result<String>;
81 fn tool_name(&self) -> &'static str;
83}
84
85pub trait OnDemandCollector: Send + Sync {
87 fn kind(&self) -> &'static str;
89
90 fn supports(&self, class: TargetClass) -> bool;
92
93 fn collect(
95 &self,
96 ctx: &CollectCtx<'_>,
97 live: Option<&dyn LiveDebugger>,
98 ) -> Result<DisasmOutput>;
99}
100
101fn upsert_symbol(db: &SessionDb, sym: &CanonicalSymbol) -> Result<i64> {
105 let session_id = current_session_id(db)?;
106 db.conn().execute(
107 "INSERT OR IGNORE INTO symbols (session_id, lang, fqn, file, line, demangled, raw, is_synthetic)
108 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
109 params![
110 session_id,
111 sym.lang,
112 sym.fqn,
113 sym.file,
114 sym.line,
115 sym.demangled,
116 sym.raw,
117 sym.is_synthetic as i64,
118 ],
119 )?;
120 Ok(db.conn().query_row(
121 "SELECT id FROM symbols WHERE session_id=?1 AND lang=?2 AND fqn=?3",
122 params![session_id, sym.lang, sym.fqn],
123 |r| r.get(0),
124 )?)
125}
126
127fn current_session_id(db: &SessionDb) -> Result<String> {
128 Ok(db
129 .conn()
130 .query_row("SELECT id FROM sessions LIMIT 1", [], |r| {
131 r.get::<_, String>(0)
132 })?)
133}
134
135pub fn persist_disasm(db: &SessionDb, ctx: &CollectCtx<'_>, output: &DisasmOutput) -> Result<i64> {
141 db.conn().execute_batch("BEGIN IMMEDIATE")?;
142 let result = persist_disasm_inner(db, ctx, output);
143 match result {
144 Ok(id) => {
145 db.conn().execute_batch("COMMIT")?;
146 Ok(id)
147 }
148 Err(error) => {
149 let _ = db.conn().execute_batch("ROLLBACK");
150 Err(error)
151 }
152 }
153}
154
155fn persist_disasm_inner(
156 db: &SessionDb,
157 ctx: &CollectCtx<'_>,
158 output: &DisasmOutput,
159) -> Result<i64> {
160 let lang = lang_for_class(ctx.target_class);
161 let canon = match for_lang(lang) {
162 Some(c) => c.canonicalize(ctx.symbol),
163 None => CanonicalSymbol {
164 lang: "unknown",
165 fqn: ctx.symbol.to_string(),
166 file: None,
167 line: None,
168 demangled: None,
169 raw: ctx.symbol.to_string(),
170 is_synthetic: false,
171 },
172 };
173 let symbol_id = upsert_symbol(db, &canon)?;
174 let session_id = current_session_id(db)?;
175
176 if !ctx.refresh {
177 let existing: Option<i64> = db
178 .conn()
179 .query_row(
180 "SELECT id FROM disassembly
181 WHERE session_id=?1 AND symbol_id=?2 AND source=?3
182 AND tier IS ?4",
183 params![session_id, symbol_id, output.source, output.tier],
184 |r| r.get(0),
185 )
186 .optional()?;
187 if let Some(id) = existing {
188 return Ok(id);
189 }
190 } else {
191 db.conn().execute(
192 "DELETE FROM disassembly
193 WHERE session_id=?1 AND symbol_id=?2 AND source=?3
194 AND (tier IS ?4 OR tier=?4)",
195 params![session_id, symbol_id, output.source, output.tier],
196 )?;
197 }
198
199 db.conn().execute(
200 "INSERT INTO disassembly
201 (session_id, symbol_id, source, tier, code_bytes,
202 asm_text, asm_lines_json, collected_at, trigger)
203 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'), ?8)",
204 params![
205 session_id,
206 symbol_id,
207 output.source,
208 output.tier,
209 output.code_bytes,
210 output.asm_text,
211 output.asm_lines_json,
212 ctx.trigger.as_str(),
213 ],
214 )?;
215 Ok(db.conn().last_insert_rowid())
216}
217
218fn lang_for_class(class: TargetClass) -> &'static str {
223 match class {
224 TargetClass::Gpu => "cuda",
225 TargetClass::NativeCpu => "cpp",
226 TargetClass::ManagedDotnet => "dotnet",
227 TargetClass::Jvm => "jvm",
228 TargetClass::Python => "python",
229 TargetClass::JsNode => "js",
230 TargetClass::Ruby => "ruby",
231 TargetClass::Php => "php",
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::session_db::{CreateOptions, SessionKind};
239 use tempfile::TempDir;
240
241 fn test_db(tmp: &TempDir, class: TargetClass) -> SessionDb {
242 SessionDb::create(CreateOptions {
243 kind: SessionKind::Debug,
244 target: "./t",
245 target_class: class,
246 cwd: tmp.path(),
247 db_path: None,
248 label: Some("t".into()),
249 target_hash: Some("h".into()),
250 })
251 .unwrap()
252 }
253
254 fn ctx<'a>(tmp: &'a TempDir, symbol: &'a str, refresh: bool) -> CollectCtx<'a> {
255 CollectCtx {
256 target: "./t",
257 target_class: TargetClass::NativeCpu,
258 symbol,
259 refresh,
260 trigger: CollectTrigger::Explicit,
261 cwd: tmp.path(),
262 }
263 }
264
265 #[test]
266 fn persist_inserts_symbol_and_disasm() {
267 let tmp = TempDir::new().unwrap();
268 let db = test_db(&tmp, TargetClass::NativeCpu);
269 let out = DisasmOutput {
270 source: "lldb-disassemble",
271 tier: None,
272 code_bytes: Some(128),
273 asm_text: "mov rax, rbx\nret".into(),
274 asm_lines_json: None,
275 };
276 let id = persist_disasm(&db, &ctx(&tmp, "main", false), &out).unwrap();
277 assert!(id > 0);
278
279 let count: i64 = db
280 .conn()
281 .query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
282 .unwrap();
283 assert_eq!(count, 1);
284 let dcount: i64 = db
285 .conn()
286 .query_row("SELECT COUNT(*) FROM disassembly", [], |r| r.get(0))
287 .unwrap();
288 assert_eq!(dcount, 1);
289 }
290
291 #[test]
292 fn persist_dedups_without_refresh() {
293 let tmp = TempDir::new().unwrap();
294 let db = test_db(&tmp, TargetClass::NativeCpu);
295 let out = DisasmOutput {
296 source: "lldb-disassemble",
297 tier: None,
298 code_bytes: None,
299 asm_text: "a".into(),
300 asm_lines_json: None,
301 };
302 let a = persist_disasm(&db, &ctx(&tmp, "main", false), &out).unwrap();
303 let b = persist_disasm(&db, &ctx(&tmp, "main", false), &out).unwrap();
304 assert_eq!(a, b, "second call should return cached id");
305
306 let count: i64 = db
307 .conn()
308 .query_row("SELECT COUNT(*) FROM disassembly", [], |r| r.get(0))
309 .unwrap();
310 assert_eq!(count, 1);
311 }
312
313 #[test]
314 fn persist_refresh_replaces_row() {
315 let tmp = TempDir::new().unwrap();
316 let db = test_db(&tmp, TargetClass::NativeCpu);
317 let v1 = DisasmOutput {
318 source: "lldb-disassemble",
319 tier: None,
320 code_bytes: None,
321 asm_text: "old asm".into(),
322 asm_lines_json: None,
323 };
324 let v2 = DisasmOutput {
325 source: "lldb-disassemble",
326 tier: None,
327 code_bytes: None,
328 asm_text: "new asm".into(),
329 asm_lines_json: None,
330 };
331 let _ = persist_disasm(&db, &ctx(&tmp, "main", false), &v1).unwrap();
332 let _ = persist_disasm(&db, &ctx(&tmp, "main", true), &v2).unwrap();
333
334 let count: i64 = db
335 .conn()
336 .query_row("SELECT COUNT(*) FROM disassembly", [], |r| r.get(0))
337 .unwrap();
338 assert_eq!(count, 1);
339 let text: String = db
340 .conn()
341 .query_row("SELECT asm_text FROM disassembly", [], |r| r.get(0))
342 .unwrap();
343 assert_eq!(text, "new asm");
344 }
345
346 #[test]
347 fn persist_distinguishes_by_tier() {
348 let tmp = TempDir::new().unwrap();
349 let db = test_db(&tmp, TargetClass::ManagedDotnet);
350 let t0 = DisasmOutput {
351 source: "jitdasm",
352 tier: Some("tier0".into()),
353 code_bytes: None,
354 asm_text: "tier-0".into(),
355 asm_lines_json: None,
356 };
357 let t1 = DisasmOutput {
358 source: "jitdasm",
359 tier: Some("tier1".into()),
360 code_bytes: None,
361 asm_text: "tier-1".into(),
362 asm_lines_json: None,
363 };
364 let c = CollectCtx {
365 target: "./t",
366 target_class: TargetClass::ManagedDotnet,
367 symbol: "MyApp.Foo",
368 refresh: false,
369 trigger: CollectTrigger::Explicit,
370 cwd: tmp.path(),
371 };
372 persist_disasm(&db, &c, &t0).unwrap();
373 persist_disasm(&db, &c, &t1).unwrap();
374
375 let count: i64 = db
376 .conn()
377 .query_row("SELECT COUNT(*) FROM disassembly", [], |r| r.get(0))
378 .unwrap();
379 assert_eq!(count, 2, "tier0 and tier1 are distinct rows");
380 }
381
382 #[test]
383 fn lang_mapping_covers_every_class() {
384 assert_eq!(lang_for_class(TargetClass::Gpu), "cuda");
385 assert_eq!(lang_for_class(TargetClass::NativeCpu), "cpp");
386 assert_eq!(lang_for_class(TargetClass::ManagedDotnet), "dotnet");
387 assert_eq!(lang_for_class(TargetClass::Jvm), "jvm");
388 assert_eq!(lang_for_class(TargetClass::Python), "python");
389 assert_eq!(lang_for_class(TargetClass::JsNode), "js");
390 }
391}