1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10use tokio::sync::broadcast;
11
12#[derive(Debug, thiserror::Error)]
14pub enum DebuggerError {
15 #[error("Debugger not started")]
16 NotStarted,
17
18 #[error("Breakpoint not found: {0}")]
19 BreakpointNotFound(String),
20
21 #[error("Invalid debug command: {0}")]
22 InvalidCommand(String),
23
24 #[error("Execution failed: {0}")]
25 ExecutionFailed(String),
26
27 #[error("State inspection failed: {0}")]
28 StateInspectionFailed(String),
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Breakpoint {
34 pub id: String,
35 pub location: BreakpointLocation,
36 pub condition: Option<String>,
37 pub enabled: bool,
38 pub hit_count: usize,
39 pub created_at: u64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub enum BreakpointLocation {
45 Function { name: String },
46 Line { file: String, line: u32 },
47 Render { chart_type: String },
48 DataProcessing { operation: String },
49 Performance { threshold_ms: u64 },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum ExecutionState {
55 Running,
56 Paused { location: String, reason: String },
57 Stopped,
58 Error { message: String },
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum DebugCommand {
64 Continue,
65 StepOver,
66 StepInto,
67 StepOut,
68 Stop,
69 Restart,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct VariableInfo {
75 pub name: String,
76 pub value: String,
77 pub type_name: String,
78 pub children: Vec<VariableInfo>,
79 pub memory_address: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct StackFrame {
85 pub function_name: String,
86 pub file: Option<String>,
87 pub line: Option<u32>,
88 pub variables: Vec<VariableInfo>,
89}
90
91#[derive(Debug, Clone)]
93pub struct DebugSession {
94 pub id: String,
95 pub name: String,
96 pub state: ExecutionState,
97 pub breakpoints: HashMap<String, Breakpoint>,
98 pub call_stack: Vec<StackFrame>,
99 pub start_time: Instant,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct EnhancedError {
105 pub original_error: String,
106 pub error_type: String,
107 pub location: Option<String>,
108 pub context: HashMap<String, String>,
109 pub suggestions: Vec<String>,
110 pub related_documentation: Vec<String>,
111 pub severity: ErrorSeverity,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum ErrorSeverity {
116 Info,
117 Warning,
118 Error,
119 Critical,
120}
121
122pub struct InteractiveDebugger {
124 sessions: Arc<Mutex<HashMap<String, DebugSession>>>,
125 current_session: Arc<Mutex<Option<String>>>,
126 command_sender: broadcast::Sender<DebugCommand>,
127 state_sender: broadcast::Sender<ExecutionState>,
128 enabled: bool,
129 enhanced_errors: bool,
130}
131
132impl InteractiveDebugger {
133 pub fn new() -> Self {
135 let (command_sender, _) = broadcast::channel(100);
136 let (state_sender, _) = broadcast::channel(100);
137
138 Self {
139 sessions: Arc::new(Mutex::new(HashMap::new())),
140 current_session: Arc::new(Mutex::new(None)),
141 command_sender,
142 state_sender,
143 enabled: true,
144 enhanced_errors: true,
145 }
146 }
147
148 pub fn start_session(&mut self, name: &str) -> Result<String, DebuggerError> {
150 if !self.enabled {
151 return Err(DebuggerError::NotStarted);
152 }
153
154 let session_id = uuid::Uuid::new_v4().to_string();
155 let session = DebugSession {
156 id: session_id.clone(),
157 name: name.to_string(),
158 state: ExecutionState::Running,
159 breakpoints: HashMap::new(),
160 call_stack: Vec::new(),
161 start_time: Instant::now(),
162 };
163
164 let mut sessions = self.sessions.lock().unwrap();
165 sessions.insert(session_id.clone(), session);
166
167 let mut current = self.current_session.lock().unwrap();
168 *current = Some(session_id.clone());
169
170 println!("🐛 Started debug session: {} ({})", name, session_id);
171 Ok(session_id)
172 }
173
174 pub fn add_breakpoint(
176 &mut self,
177 location: BreakpointLocation,
178 condition: Option<String>,
179 ) -> Result<String, DebuggerError> {
180 let current_session = self.current_session.lock().unwrap();
181 let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
182
183 let breakpoint_id = uuid::Uuid::new_v4().to_string();
184 let breakpoint = Breakpoint {
185 id: breakpoint_id.clone(),
186 location,
187 condition,
188 enabled: true,
189 hit_count: 0,
190 created_at: Instant::now().elapsed().as_millis() as u64,
191 };
192
193 let mut sessions = self.sessions.lock().unwrap();
194 if let Some(session) = sessions.get_mut(session_id) {
195 session
196 .breakpoints
197 .insert(breakpoint_id.clone(), breakpoint);
198 }
199
200 println!("🔴 Added breakpoint: {}", breakpoint_id);
201 Ok(breakpoint_id)
202 }
203
204 pub fn remove_breakpoint(&mut self, breakpoint_id: &str) -> Result<(), DebuggerError> {
206 let current_session = self.current_session.lock().unwrap();
207 let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
208
209 let mut sessions = self.sessions.lock().unwrap();
210 if let Some(session) = sessions.get_mut(session_id) {
211 session
212 .breakpoints
213 .remove(breakpoint_id)
214 .ok_or_else(|| DebuggerError::BreakpointNotFound(breakpoint_id.to_string()))?;
215 }
216
217 println!("⭕ Removed breakpoint: {}", breakpoint_id);
218 Ok(())
219 }
220
221 pub fn execute_command(&mut self, command: DebugCommand) -> Result<(), DebuggerError> {
223 let current_session = self.current_session.lock().unwrap();
224 let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
225
226 let _ = self.command_sender.send(command.clone());
228
229 let new_state = {
231 let mut sessions = self.sessions.lock().unwrap();
232 if let Some(session) = sessions.get_mut(session_id) {
233 session.state = match command {
234 DebugCommand::Continue => ExecutionState::Running,
235 DebugCommand::Stop => ExecutionState::Stopped,
236 DebugCommand::StepOver | DebugCommand::StepInto | DebugCommand::StepOut => {
237 ExecutionState::Paused {
238 location: "next_instruction".to_string(),
239 reason: "step_command".to_string(),
240 }
241 }
242 DebugCommand::Restart => {
243 session.call_stack.clear();
244 ExecutionState::Running
245 }
246 };
247 session.state.clone()
248 } else {
249 ExecutionState::Error {
250 message: "Session not found".to_string(),
251 }
252 }
253 };
254
255 let _ = self.state_sender.send(new_state);
257
258 println!("🎮 Executed debug command: {:?}", command);
259 Ok(())
260 }
261
262 pub fn should_pause(&self, location: &str, context: &HashMap<String, String>) -> bool {
264 let current_session = self.current_session.lock().unwrap();
265 if let Some(session_id) = current_session.as_ref() {
266 let sessions = self.sessions.lock().unwrap();
267 if let Some(session) = sessions.get(session_id) {
268 for breakpoint in session.breakpoints.values() {
269 if !breakpoint.enabled {
270 continue;
271 }
272
273 let location_matches = match &breakpoint.location {
274 BreakpointLocation::Function { name } => location.contains(name),
275 BreakpointLocation::Line { file, line: _ } => location.contains(file),
276 BreakpointLocation::Render { chart_type } => {
277 location.contains("render")
278 && context.get("chart_type") == Some(chart_type)
279 }
280 BreakpointLocation::DataProcessing { operation } => {
281 location.contains("data") && context.get("operation") == Some(operation)
282 }
283 BreakpointLocation::Performance { threshold_ms } => {
284 if let Some(duration_str) = context.get("duration_ms") {
285 if let Ok(duration_ms) = duration_str.parse::<u64>() {
286 return duration_ms > *threshold_ms;
287 }
288 }
289 false
290 }
291 };
292
293 if location_matches {
294 if let Some(condition) = &breakpoint.condition {
295 if self.evaluate_condition(condition, context) {
296 return true;
297 }
298 } else {
299 return true;
300 }
301 }
302 }
303 }
304 }
305 false
306 }
307
308 pub fn inspect_variables(&self, scope: &str) -> Result<Vec<VariableInfo>, DebuggerError> {
310 let mut variables = Vec::new();
312
313 match scope {
314 "chart" => {
315 variables.push(VariableInfo {
316 name: "spec".to_string(),
317 value: "ChartSpec { mark: Line, ... }".to_string(),
318 type_name: "ChartSpec".to_string(),
319 children: vec![VariableInfo {
320 name: "mark".to_string(),
321 value: "Line { stroke_width: 2.0 }".to_string(),
322 type_name: "MarkType".to_string(),
323 children: Vec::new(),
324 memory_address: Some("0x7fff5fbff890".to_string()),
325 }],
326 memory_address: Some("0x7fff5fbff880".to_string()),
327 });
328 }
329 "data" => {
330 variables.push(VariableInfo {
331 name: "dataframe".to_string(),
332 value: "DataFrame { shape: (1000, 5) }".to_string(),
333 type_name: "polars::DataFrame".to_string(),
334 children: Vec::new(),
335 memory_address: Some("0x7fff5fbff8a0".to_string()),
336 });
337 }
338 _ => {
339 return Err(DebuggerError::StateInspectionFailed(format!(
340 "Unknown scope: {}",
341 scope
342 )));
343 }
344 }
345
346 Ok(variables)
347 }
348
349 pub fn get_call_stack(&self) -> Result<Vec<StackFrame>, DebuggerError> {
351 let current_session = self.current_session.lock().unwrap();
352 let session_id = current_session.as_ref().ok_or(DebuggerError::NotStarted)?;
353
354 let sessions = self.sessions.lock().unwrap();
355 if let Some(session) = sessions.get(session_id) {
356 Ok(session.call_stack.clone())
357 } else {
358 Ok(Vec::new())
359 }
360 }
361
362 pub fn enhance_error(&self, error: &str, location: Option<&str>) -> EnhancedError {
364 if !self.enhanced_errors {
365 return EnhancedError {
366 original_error: error.to_string(),
367 error_type: "Unknown".to_string(),
368 location: location.map(|s| s.to_string()),
369 context: HashMap::new(),
370 suggestions: Vec::new(),
371 related_documentation: Vec::new(),
372 severity: ErrorSeverity::Error,
373 };
374 }
375
376 let (error_type, suggestions, docs, severity) = self.analyze_error(error);
377 let context = self.gather_error_context(error, location);
378
379 EnhancedError {
380 original_error: error.to_string(),
381 error_type,
382 location: location.map(|s| s.to_string()),
383 context,
384 suggestions,
385 related_documentation: docs,
386 severity,
387 }
388 }
389
390 fn analyze_error(&self, error: &str) -> (String, Vec<String>, Vec<String>, ErrorSeverity) {
392 let error_lower = error.to_lowercase();
393
394 if error_lower.contains("webgpu") {
395 (
396 "WebGPU Error".to_string(),
397 vec![
398 "Check if WebGPU is supported in current browser".to_string(),
399 "Verify GPU drivers are up to date".to_string(),
400 "Consider enabling WebGL2 fallback".to_string(),
401 ],
402 vec![
403 "https://docs.helios.dev/webgpu-support".to_string(),
404 "https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API".to_string(),
405 ],
406 ErrorSeverity::Error,
407 )
408 } else if error_lower.contains("polars") || error_lower.contains("dataframe") {
409 (
410 "Data Processing Error".to_string(),
411 vec![
412 "Verify data format and schema compatibility".to_string(),
413 "Check for null values or invalid data types".to_string(),
414 "Consider using lazy evaluation for large datasets".to_string(),
415 ],
416 vec![
417 "https://docs.helios.dev/data-processing".to_string(),
418 "https://docs.pola.rs/".to_string(),
419 ],
420 ErrorSeverity::Error,
421 )
422 } else if error_lower.contains("leptos") || error_lower.contains("signal") {
423 (
424 "Reactive System Error".to_string(),
425 vec![
426 "Ensure reactive updates are properly tracked".to_string(),
427 "Check for signal disposal and cleanup".to_string(),
428 "Verify component lifecycle management".to_string(),
429 ],
430 vec![
431 "https://docs.helios.dev/reactive-system".to_string(),
432 "https://leptos.dev/".to_string(),
433 ],
434 ErrorSeverity::Warning,
435 )
436 } else if error_lower.contains("performance") || error_lower.contains("timeout") {
437 (
438 "Performance Issue".to_string(),
439 vec![
440 "Enable performance profiling to identify bottlenecks".to_string(),
441 "Consider reducing data complexity or visualization detail".to_string(),
442 "Check for memory leaks or excessive allocations".to_string(),
443 ],
444 vec!["https://docs.helios.dev/performance-optimization".to_string()],
445 ErrorSeverity::Warning,
446 )
447 } else {
448 (
449 "General Error".to_string(),
450 vec![
451 "Check the documentation for similar issues".to_string(),
452 "Enable debug logging for more details".to_string(),
453 "Consider filing an issue with reproduction steps".to_string(),
454 ],
455 vec![
456 "https://docs.helios.dev/troubleshooting".to_string(),
457 "https://github.com/helios-viz/helios/issues".to_string(),
458 ],
459 ErrorSeverity::Error,
460 )
461 }
462 }
463
464 fn gather_error_context(&self, error: &str, location: Option<&str>) -> HashMap<String, String> {
466 let mut context = HashMap::new();
467
468 context.insert("timestamp".to_string(), chrono::Utc::now().to_rfc3339());
469
470 if let Some(loc) = location {
471 context.insert("location".to_string(), loc.to_string());
472 }
473
474 context.insert(
475 "browser_info".to_string(),
476 "Chrome/120.0 (mock)".to_string(),
477 );
478
479 context.insert("webgpu_supported".to_string(), "true".to_string());
480
481 context.insert("memory_usage".to_string(), "45MB".to_string());
482
483 if error.to_lowercase().contains("webgpu") {
485 context.insert("webgpu_adapter".to_string(), "Default adapter".to_string());
486 }
487
488 context
489 }
490
491 fn evaluate_condition(&self, condition: &str, context: &HashMap<String, String>) -> bool {
493 if condition.contains("==") {
495 let parts: Vec<&str> = condition.split("==").collect();
496 if parts.len() == 2 {
497 let left = parts[0].trim();
498 let right = parts[1].trim().trim_matches('"');
499 return context.get(left) == Some(&right.to_string());
500 }
501 }
502
503 if condition.contains(">") {
504 let parts: Vec<&str> = condition.split(">").collect();
505 if parts.len() == 2 {
506 let left = parts[0].trim();
507 let right = parts[1].trim();
508 if let (Some(left_val), Ok(right_val)) = (context.get(left), right.parse::<f64>()) {
509 if let Ok(left_num) = left_val.parse::<f64>() {
510 return left_num > right_val;
511 }
512 }
513 }
514 }
515
516 true
518 }
519
520 pub fn subscribe_to_commands(&self) -> broadcast::Receiver<DebugCommand> {
522 self.command_sender.subscribe()
523 }
524
525 pub fn subscribe_to_state(&self) -> broadcast::Receiver<ExecutionState> {
527 self.state_sender.subscribe()
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[tokio::test]
536 async fn test_debug_session_lifecycle() {
537 let mut debugger = InteractiveDebugger::new();
538
539 let session_id = debugger.start_session("test_debug").unwrap();
541 assert!(!session_id.is_empty());
542
543 let bp_id = debugger
545 .add_breakpoint(
546 BreakpointLocation::Function {
547 name: "render".to_string(),
548 },
549 None,
550 )
551 .unwrap();
552 assert!(!bp_id.is_empty());
553
554 debugger.execute_command(DebugCommand::StepOver).unwrap();
556
557 debugger.remove_breakpoint(&bp_id).unwrap();
559 }
560
561 #[test]
562 fn test_breakpoint_evaluation() {
563 let debugger = InteractiveDebugger::new();
564 let mut context = HashMap::new();
565 context.insert("chart_type".to_string(), "Line".to_string());
566
567 assert!(!debugger.should_pause("render_line_chart", &context));
569 }
570
571 #[test]
572 fn test_error_enhancement() {
573 let debugger = InteractiveDebugger::new();
574 let error = "WebGPU adapter not found";
575 let enhanced = debugger.enhance_error(error, Some("render.rs:45"));
576
577 assert_eq!(enhanced.error_type, "WebGPU Error");
578 assert!(!enhanced.suggestions.is_empty());
579 assert!(!enhanced.related_documentation.is_empty());
580 assert!(matches!(enhanced.severity, ErrorSeverity::Error));
581 }
582
583 #[test]
584 fn test_condition_evaluation() {
585 let debugger = InteractiveDebugger::new();
586 let mut context = HashMap::new();
587 context.insert("duration_ms".to_string(), "150".to_string());
588
589 assert!(debugger.evaluate_condition("duration_ms > 100", &context));
591 assert!(!debugger.evaluate_condition("duration_ms > 200", &context));
592
593 context.insert("type".to_string(), "LineChart".to_string());
595 assert!(debugger.evaluate_condition("type == \"LineChart\"", &context));
596 }
597
598 #[test]
599 fn test_variable_inspection() {
600 let debugger = InteractiveDebugger::new();
601
602 let chart_vars = debugger.inspect_variables("chart").unwrap();
603 assert!(!chart_vars.is_empty());
604 assert_eq!(chart_vars[0].name, "spec");
605 assert_eq!(chart_vars[0].type_name, "ChartSpec");
606 }
607}