1use crate::graph::{ComputationGraph, Edge, Node, NodeId, Operation};
7use crate::{JitError, JitResult};
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11use torsh_core::{DType, DeviceType, Shape};
12
13#[derive(Debug)]
15pub struct GraphTracer {
16 graph: ComputationGraph,
18
19 value_to_node: HashMap<TracedValueId, NodeId>,
21
22 next_value_id: TracedValueId,
24
25 active: bool,
27
28 op_stack: Vec<OpContext>,
30
31 profiler: Option<Profiler>,
33}
34
35#[derive(Debug)]
37pub struct Profiler {
38 pub op_timings: HashMap<String, Duration>,
40
41 pub memory_usage: HashMap<String, usize>,
43
44 pub op_counts: HashMap<String, usize>,
46
47 current_op_start: Option<Instant>,
49
50 current_op_name: Option<String>,
52}
53
54impl Default for Profiler {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl Profiler {
61 pub fn new() -> Self {
62 Self {
63 op_timings: HashMap::new(),
64 memory_usage: HashMap::new(),
65 op_counts: HashMap::new(),
66 current_op_start: None,
67 current_op_name: None,
68 }
69 }
70
71 pub fn start_op(&mut self, op_name: String) {
72 self.current_op_start = Some(Instant::now());
73 self.current_op_name = Some(op_name);
74 }
75
76 pub fn end_op(&mut self) {
77 if let (Some(start), Some(name)) =
78 (self.current_op_start.take(), self.current_op_name.take())
79 {
80 let duration = start.elapsed();
81 *self
82 .op_timings
83 .entry(name.clone())
84 .or_insert(Duration::ZERO) += duration;
85 *self.op_counts.entry(name).or_insert(0) += 1;
86 }
87 }
88
89 pub fn record_memory_usage(&mut self, op_name: String, bytes: usize) {
90 self.memory_usage.insert(op_name, bytes);
91 }
92
93 pub fn get_total_time(&self) -> Duration {
94 self.op_timings.values().sum()
95 }
96
97 pub fn get_slowest_ops(&self, count: usize) -> Vec<(String, Duration)> {
98 let mut ops: Vec<_> = self
99 .op_timings
100 .iter()
101 .map(|(name, duration)| (name.clone(), *duration))
102 .collect();
103 ops.sort_by(|a, b| b.1.cmp(&a.1));
104 ops.into_iter().take(count).collect()
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct TracedValueId(u64);
111
112#[derive(Debug, Clone)]
114struct OpContext {
115 #[allow(dead_code)]
117 op: Operation,
118
119 #[allow(dead_code)]
121 inputs: Vec<TracedValueId>,
122
123 #[allow(dead_code)]
125 metadata: OpMetadata,
126}
127
128#[derive(Debug, Clone)]
130struct OpMetadata {
131 #[allow(dead_code)]
133 source_location: Option<String>,
134
135 #[allow(dead_code)]
137 name: Option<String>,
138
139 #[allow(dead_code)]
141 attributes: HashMap<String, String>,
142}
143
144#[derive(Debug, Clone)]
146pub struct TracedValue {
147 pub id: TracedValueId,
149
150 pub shape: Shape,
152
153 pub dtype: DType,
155
156 pub device: DeviceType,
158
159 pub requires_grad: bool,
161
162 tracer: Arc<Mutex<GraphTracer>>,
164}
165
166impl GraphTracer {
167 pub fn new() -> Self {
169 Self {
170 graph: ComputationGraph::new(),
171 value_to_node: HashMap::new(),
172 next_value_id: TracedValueId(0),
173 active: false,
174 op_stack: Vec::new(),
175 profiler: None,
176 }
177 }
178
179 pub fn new_with_profiling() -> Self {
181 Self {
182 graph: ComputationGraph::new(),
183 value_to_node: HashMap::new(),
184 next_value_id: TracedValueId(0),
185 active: false,
186 op_stack: Vec::new(),
187 profiler: Some(Profiler::new()),
188 }
189 }
190
191 pub fn enable_profiling(&mut self) {
193 self.profiler = Some(Profiler::new());
194 }
195
196 pub fn disable_profiling(&mut self) {
198 self.profiler = None;
199 }
200
201 pub fn get_profiler(&self) -> Option<&Profiler> {
203 self.profiler.as_ref()
204 }
205
206 pub fn start_tracing(&mut self) {
208 self.active = true;
209 self.graph = ComputationGraph::new();
210 self.value_to_node.clear();
211 self.next_value_id = TracedValueId(0);
212 self.op_stack.clear();
213 }
214
215 pub fn stop_tracing(&mut self) -> ComputationGraph {
217 self.active = false;
218 std::mem::take(&mut self.graph)
219 }
220
221 pub fn is_tracing(&self) -> bool {
223 self.active
224 }
225
226 fn new_value_id(&mut self) -> TracedValueId {
228 let id = self.next_value_id;
229 self.next_value_id.0 += 1;
230 id
231 }
232
233 pub fn create_input(
235 &mut self,
236 name: impl Into<String>,
237 shape: Shape,
238 dtype: DType,
239 device: DeviceType,
240 ) -> TracedValueId {
241 if !self.active {
242 return TracedValueId(0);
243 }
244
245 let value_id = self.new_value_id();
246
247 let mut node = Node::new(Operation::Input, name.into());
248 node = node
249 .with_output_shapes(vec![Some(shape)])
250 .with_dtypes(vec![dtype])
251 .with_device(device);
252 node.inputs = vec![];
253 node.is_output = false;
254
255 let node_id = self.graph.add_node(node);
256 self.graph.add_input(node_id);
257 self.value_to_node.insert(value_id, node_id);
258
259 value_id
260 }
261
262 pub fn record_operation(
264 &mut self,
265 op: Operation,
266 inputs: &[TracedValueId],
267 output_shape: Shape,
268 output_dtype: DType,
269 output_device: DeviceType,
270 ) -> JitResult<TracedValueId> {
271 if !self.active {
272 return Ok(TracedValueId(0));
273 }
274
275 let op_name = format!("{:?}", op);
276
277 if let Some(ref mut profiler) = self.profiler {
279 profiler.start_op(op_name.clone());
280 }
281
282 let output_id = self.new_value_id();
283
284 let mut node = Node::new(op.clone(), format!("{:?}_{}", op, output_id.0));
286 node = node
287 .with_output_shapes(vec![Some(output_shape.clone())])
288 .with_dtypes(vec![output_dtype])
289 .with_device(output_device);
290 node.inputs = vec![];
291 node.is_output = false;
292
293 let node_id = self.graph.add_node(node);
294
295 for (i, &input_id) in inputs.iter().enumerate() {
297 if let Some(&input_node_id) = self.value_to_node.get(&input_id) {
298 self.graph.add_edge(
299 input_node_id,
300 node_id,
301 Edge {
302 src_output: 0,
303 dst_input: i,
304 },
305 );
306 }
307 }
308
309 self.value_to_node.insert(output_id, node_id);
310
311 if let Some(ref mut profiler) = self.profiler {
313 let memory_bytes = output_shape.numel() * dtype_size_bytes(output_dtype);
314 profiler.record_memory_usage(op_name.clone(), memory_bytes);
315 profiler.end_op();
316 }
317
318 Ok(output_id)
319 }
320
321 pub fn mark_output(&mut self, value_id: TracedValueId) {
323 if let Some(&node_id) = self.value_to_node.get(&value_id) {
324 self.graph.add_output(node_id);
325 }
326 }
327
328 pub fn get_graph(&self) -> &ComputationGraph {
330 &self.graph
331 }
332}
333
334impl TracedValue {
335 pub fn new(
337 shape: Shape,
338 dtype: DType,
339 device: DeviceType,
340 requires_grad: bool,
341 tracer: Arc<Mutex<GraphTracer>>,
342 ) -> Self {
343 let id = {
344 let mut t = tracer.lock().expect("lock should not be poisoned");
345 t.new_value_id()
346 };
347
348 Self {
349 id,
350 shape,
351 dtype,
352 device,
353 requires_grad,
354 tracer,
355 }
356 }
357
358 pub fn input(
360 name: impl Into<String>,
361 shape: Shape,
362 dtype: DType,
363 device: DeviceType,
364 tracer: Arc<Mutex<GraphTracer>>,
365 ) -> Self {
366 let id = {
367 let mut t = tracer.lock().expect("lock should not be poisoned");
368 t.create_input(name, shape.clone(), dtype, device)
369 };
370
371 Self {
372 id,
373 shape,
374 dtype,
375 device,
376 requires_grad: false,
377 tracer,
378 }
379 }
380
381 pub fn unary_op(&self, op: Operation) -> JitResult<TracedValue> {
383 let output_id = {
384 let mut tracer = self.tracer.lock().expect("lock should not be poisoned");
385 tracer.record_operation(op, &[self.id], self.shape.clone(), self.dtype, self.device)?
386 };
387
388 Ok(TracedValue {
389 id: output_id,
390 shape: self.shape.clone(),
391 dtype: self.dtype,
392 device: self.device,
393 requires_grad: self.requires_grad,
394 tracer: self.tracer.clone(),
395 })
396 }
397
398 pub fn binary_op(&self, other: &TracedValue, op: Operation) -> JitResult<TracedValue> {
400 let output_shape = if self.shape.dims() == other.shape.dims() {
402 self.shape.clone()
403 } else {
404 if self.shape.numel() >= other.shape.numel() {
406 self.shape.clone()
407 } else {
408 other.shape.clone()
409 }
410 };
411
412 let output_dtype = match (self.dtype, other.dtype) {
414 (DType::F64, _) | (_, DType::F64) => DType::F64,
415 (DType::F32, _) | (_, DType::F32) => DType::F32,
416 _ => self.dtype,
417 };
418
419 let output_id = {
420 let mut tracer = self.tracer.lock().expect("lock should not be poisoned");
421 tracer.record_operation(
422 op,
423 &[self.id, other.id],
424 output_shape.clone(),
425 output_dtype,
426 self.device,
427 )?
428 };
429
430 Ok(TracedValue {
431 id: output_id,
432 shape: output_shape,
433 dtype: output_dtype,
434 device: self.device,
435 requires_grad: self.requires_grad || other.requires_grad,
436 tracer: self.tracer.clone(),
437 })
438 }
439
440 pub fn matmul(&self, other: &TracedValue) -> JitResult<TracedValue> {
442 let self_dims = self.shape.dims();
444 let other_dims = other.shape.dims();
445
446 if self_dims.len() < 2 || other_dims.len() < 2 {
447 return Err(JitError::GraphError(
448 "Matrix multiplication requires at least 2D tensors".to_string(),
449 ));
450 }
451
452 let m = self_dims[self_dims.len() - 2];
453 let k1 = self_dims[self_dims.len() - 1];
454 let k2 = other_dims[other_dims.len() - 2];
455 let n = other_dims[other_dims.len() - 1];
456
457 if k1 != k2 {
458 return Err(JitError::GraphError(format!(
459 "Matrix multiplication dimension mismatch: {} != {}",
460 k1, k2
461 )));
462 }
463
464 let mut output_dims = self_dims[..self_dims.len() - 2].to_vec();
466 output_dims.push(m);
467 output_dims.push(n);
468
469 let output_shape = Shape::new(output_dims);
470 let output_dtype = match (self.dtype, other.dtype) {
471 (DType::F64, _) | (_, DType::F64) => DType::F64,
472 (DType::F32, _) | (_, DType::F32) => DType::F32,
473 _ => self.dtype,
474 };
475
476 let output_id = {
477 let mut tracer = self.tracer.lock().expect("lock should not be poisoned");
478 tracer.record_operation(
479 Operation::MatMul,
480 &[self.id, other.id],
481 output_shape.clone(),
482 output_dtype,
483 self.device,
484 )?
485 };
486
487 Ok(TracedValue {
488 id: output_id,
489 shape: output_shape,
490 dtype: output_dtype,
491 device: self.device,
492 requires_grad: self.requires_grad || other.requires_grad,
493 tracer: self.tracer.clone(),
494 })
495 }
496
497 pub fn reshape(&self, new_shape: &[isize]) -> JitResult<TracedValue> {
499 let output_shape = Shape::new(
500 new_shape
501 .iter()
502 .map(|&dim| {
503 if dim == -1 {
504 let total_elements = self.shape.numel();
506 let known_elements: usize = new_shape
507 .iter()
508 .filter(|&&d| d != -1)
509 .map(|&d| d as usize)
510 .product();
511 if known_elements == 0 {
512 total_elements
513 } else {
514 total_elements / known_elements
515 }
516 } else {
517 dim as usize
518 }
519 })
520 .collect(),
521 );
522
523 let output_id = {
524 let mut tracer = self.tracer.lock().expect("lock should not be poisoned");
525 tracer.record_operation(
526 Operation::Reshape {
527 shape: new_shape.to_vec(),
528 },
529 &[self.id],
530 output_shape.clone(),
531 self.dtype,
532 self.device,
533 )?
534 };
535
536 Ok(TracedValue {
537 id: output_id,
538 shape: output_shape,
539 dtype: self.dtype,
540 device: self.device,
541 requires_grad: self.requires_grad,
542 tracer: self.tracer.clone(),
543 })
544 }
545
546 pub fn mark_as_output(&self) {
548 let mut tracer = self.tracer.lock().expect("lock should not be poisoned");
549 tracer.mark_output(self.id);
550 }
551
552 pub fn add(&self, other: &TracedValue) -> JitResult<TracedValue> {
554 self.binary_op(other, Operation::Add)
555 }
556
557 pub fn sub(&self, other: &TracedValue) -> JitResult<TracedValue> {
558 self.binary_op(other, Operation::Sub)
559 }
560
561 pub fn mul(&self, other: &TracedValue) -> JitResult<TracedValue> {
562 self.binary_op(other, Operation::Mul)
563 }
564
565 pub fn div(&self, other: &TracedValue) -> JitResult<TracedValue> {
566 self.binary_op(other, Operation::Div)
567 }
568
569 pub fn relu(&self) -> JitResult<TracedValue> {
570 self.unary_op(Operation::Relu)
571 }
572
573 pub fn sigmoid(&self) -> JitResult<TracedValue> {
574 self.unary_op(Operation::Sigmoid)
575 }
576
577 pub fn tanh(&self) -> JitResult<TracedValue> {
578 self.unary_op(Operation::Tanh)
579 }
580
581 pub fn exp(&self) -> JitResult<TracedValue> {
582 self.unary_op(Operation::Exp)
583 }
584
585 pub fn log(&self) -> JitResult<TracedValue> {
586 self.unary_op(Operation::Log)
587 }
588}
589
590impl Default for GraphTracer {
591 fn default() -> Self {
592 Self::new()
593 }
594}
595
596pub fn trace_function<F, I, O>(func: F, example_inputs: I) -> JitResult<ComputationGraph>
598where
599 F: FnOnce(I) -> O,
600{
601 let tracer = Arc::new(Mutex::new(GraphTracer::new()));
602
603 {
605 let mut t = tracer.lock().expect("lock should not be poisoned");
606 t.start_tracing();
607 }
608
609 let _outputs = func(example_inputs);
611
612 let graph = {
614 let mut t = tracer.lock().expect("lock should not be poisoned");
615 t.stop_tracing()
616 };
617
618 Ok(graph)
619}
620
621fn dtype_size_bytes(dtype: DType) -> usize {
623 match dtype {
624 DType::Bool | DType::I8 | DType::U8 | DType::QInt8 | DType::QUInt8 => 1,
625 DType::I16 | DType::F16 | DType::BF16 => 2,
626 DType::I32 | DType::F32 | DType::U32 | DType::QInt32 => 4,
627 DType::I64 | DType::F64 | DType::C64 | DType::U64 => 8,
628 DType::C128 => 16,
629 }
630}
631
632#[derive(Debug, Clone)]
634pub struct PerformanceAnalysis {
635 pub total_time: Duration,
637
638 pub op_stats: HashMap<String, OpStats>,
640
641 pub memory_stats: MemoryStats,
643
644 pub bottlenecks: Vec<Bottleneck>,
646}
647
648#[derive(Debug, Clone)]
649pub struct OpStats {
650 pub count: usize,
651 pub total_time: Duration,
652 pub avg_time: Duration,
653 pub memory_usage: usize,
654}
655
656#[derive(Debug, Clone)]
657pub struct MemoryStats {
658 pub peak_usage: usize,
659 pub total_allocated: usize,
660 pub fragmentation_ratio: f32,
661}
662
663#[derive(Debug, Clone)]
664pub struct Bottleneck {
665 pub op_name: String,
666 pub time_percentage: f32,
667 pub memory_percentage: f32,
668 pub recommendation: String,
669}
670
671impl Profiler {
672 pub fn analyze(&self) -> PerformanceAnalysis {
674 let total_time = self.get_total_time();
675 let mut op_stats = HashMap::new();
676
677 for (op_name, &op_time) in &self.op_timings {
678 let count = self.op_counts.get(op_name).copied().unwrap_or(0);
679 let memory_usage = self.memory_usage.get(op_name).copied().unwrap_or(0);
680
681 op_stats.insert(
682 op_name.clone(),
683 OpStats {
684 count,
685 total_time: op_time,
686 avg_time: if count > 0 {
687 op_time / count as u32
688 } else {
689 Duration::ZERO
690 },
691 memory_usage,
692 },
693 );
694 }
695
696 let peak_usage = self.memory_usage.values().max().copied().unwrap_or(0);
697 let total_allocated = self.memory_usage.values().sum();
698
699 let memory_stats = MemoryStats {
700 peak_usage,
701 total_allocated,
702 fragmentation_ratio: 0.0, };
704
705 let bottlenecks = self.identify_bottlenecks(&total_time);
706
707 PerformanceAnalysis {
708 total_time,
709 op_stats,
710 memory_stats,
711 bottlenecks,
712 }
713 }
714
715 fn identify_bottlenecks(&self, total_time: &Duration) -> Vec<Bottleneck> {
716 let mut bottlenecks = Vec::new();
717 let total_memory: usize = self.memory_usage.values().sum();
718
719 for (op_name, &op_time) in &self.op_timings {
720 let time_percentage = if total_time.as_nanos() > 0 {
721 (op_time.as_nanos() as f32 / total_time.as_nanos() as f32) * 100.0
722 } else {
723 0.0
724 };
725
726 let memory_usage = self.memory_usage.get(op_name).copied().unwrap_or(0);
727 let memory_percentage = if total_memory > 0 {
728 (memory_usage as f32 / total_memory as f32) * 100.0
729 } else {
730 0.0
731 };
732
733 if time_percentage > 10.0 || memory_percentage > 20.0 {
734 let recommendation = if time_percentage > 20.0 {
735 "Consider optimizing this operation - it's consuming significant compute time"
736 .to_string()
737 } else if memory_percentage > 30.0 {
738 "High memory usage - consider reducing precision or using memory optimization"
739 .to_string()
740 } else {
741 "Monitor this operation for potential optimization".to_string()
742 };
743
744 bottlenecks.push(Bottleneck {
745 op_name: op_name.clone(),
746 time_percentage,
747 memory_percentage,
748 recommendation,
749 });
750 }
751 }
752
753 bottlenecks.sort_by(|a, b| {
754 b.time_percentage
755 .partial_cmp(&a.time_percentage)
756 .unwrap_or(std::cmp::Ordering::Equal)
757 });
758 bottlenecks
759 }
760}
761
762#[macro_export]
764macro_rules! trace {
765 ($tracer:expr, $op:expr, $($input:expr),*) => {{
766 let inputs = vec![$($input.id),*];
767 $tracer.lock().expect("lock should not be poisoned").record_operation($op, &inputs, Shape::new(vec![1]), DType::F32, DeviceType::Cpu)
769 }};
770}
771
772#[derive(Debug, Clone)]
774pub struct SourceMap {
775 pub node_to_source: HashMap<NodeId, SourceLocation>,
777
778 pub code_to_source: HashMap<usize, SourceLocation>,
780
781 pub source_files: HashMap<String, String>,
783
784 pub symbols: HashMap<String, SymbolInfo>,
786}
787
788#[derive(Debug, Clone, PartialEq)]
790pub struct SourceLocation {
791 pub file: String,
793 pub line: u32,
795 pub column: u32,
797 pub length: u32,
799 pub function: Option<String>,
801}
802
803#[derive(Debug, Clone)]
805pub struct SymbolInfo {
806 pub name: String,
808 pub symbol_type: SymbolType,
810 pub definition: SourceLocation,
812 pub data_type: Option<DType>,
814 pub shape: Option<Shape>,
816}
817
818#[derive(Debug, Clone, PartialEq)]
820pub enum SymbolType {
821 Variable,
822 Function,
823 Parameter,
824 Constant,
825 Temporary,
826}
827
828pub struct SourceMapBuilder {
830 source_map: SourceMap,
831 current_file: Option<String>,
832 current_function: Option<String>,
833}
834
835impl SourceMapBuilder {
836 pub fn new() -> Self {
838 Self {
839 source_map: SourceMap {
840 node_to_source: HashMap::new(),
841 code_to_source: HashMap::new(),
842 source_files: HashMap::new(),
843 symbols: HashMap::new(),
844 },
845 current_file: None,
846 current_function: None,
847 }
848 }
849
850 pub fn set_current_file(&mut self, file: String, content: String) {
852 self.current_file = Some(file.clone());
853 self.source_map.source_files.insert(file, content);
854 }
855
856 pub fn set_current_function(&mut self, function: String) {
858 self.current_function = Some(function);
859 }
860
861 pub fn add_node_mapping(&mut self, node_id: NodeId, location: SourceLocation) {
863 self.source_map.node_to_source.insert(node_id, location);
864 }
865
866 pub fn add_code_mapping(&mut self, address: usize, location: SourceLocation) {
868 self.source_map.code_to_source.insert(address, location);
869 }
870
871 pub fn add_symbol(&mut self, symbol: SymbolInfo) {
873 self.source_map.symbols.insert(symbol.name.clone(), symbol);
874 }
875
876 pub fn create_location(&self, line: u32, column: u32, length: u32) -> SourceLocation {
878 SourceLocation {
879 file: self
880 .current_file
881 .clone()
882 .unwrap_or_else(|| "<unknown>".to_string()),
883 line,
884 column,
885 length,
886 function: self.current_function.clone(),
887 }
888 }
889
890 pub fn build(self) -> SourceMap {
892 self.source_map
893 }
894}
895
896impl Default for SourceMapBuilder {
897 fn default() -> Self {
898 Self::new()
899 }
900}
901
902impl SourceMap {
903 pub fn new() -> Self {
905 Self {
906 node_to_source: HashMap::new(),
907 code_to_source: HashMap::new(),
908 source_files: HashMap::new(),
909 symbols: HashMap::new(),
910 }
911 }
912
913 pub fn get_node_location(&self, node_id: NodeId) -> Option<&SourceLocation> {
915 self.node_to_source.get(&node_id)
916 }
917
918 pub fn get_code_location(&self, address: usize) -> Option<&SourceLocation> {
920 self.code_to_source.get(&address)
921 }
922
923 pub fn get_symbol(&self, name: &str) -> Option<&SymbolInfo> {
925 self.symbols.get(name)
926 }
927
928 pub fn get_source_line(&self, location: &SourceLocation) -> Option<String> {
930 self.source_files.get(&location.file).and_then(|content| {
931 content
932 .lines()
933 .nth((location.line - 1) as usize)
934 .map(|s| s.to_string())
935 })
936 }
937
938 pub fn get_source_context(
940 &self,
941 location: &SourceLocation,
942 context_lines: u32,
943 ) -> Vec<(u32, String)> {
944 let mut lines = Vec::new();
945
946 if let Some(content) = self.source_files.get(&location.file) {
947 let file_lines: Vec<&str> = content.lines().collect();
948 let start_line = location.line.saturating_sub(context_lines);
949 let end_line = std::cmp::min(location.line + context_lines, file_lines.len() as u32);
950
951 for line_num in start_line..end_line {
952 if let Some(line_content) = file_lines.get(line_num as usize) {
953 lines.push((line_num + 1, line_content.to_string()));
954 }
955 }
956 }
957
958 lines
959 }
960
961 pub fn find_locations_in_file(&self, file: &str) -> Vec<(NodeId, &SourceLocation)> {
963 self.node_to_source
964 .iter()
965 .filter(|(_, loc)| loc.file == file)
966 .map(|(&node_id, loc)| (node_id, loc))
967 .collect()
968 }
969
970 pub fn find_symbols_by_type(&self, symbol_type: SymbolType) -> Vec<&SymbolInfo> {
972 self.symbols
973 .values()
974 .filter(|symbol| symbol.symbol_type == symbol_type)
975 .collect()
976 }
977
978 pub fn to_dwarf_info(&self) -> DebugInfo {
980 DebugInfo {
981 compilation_unit: self.current_file().unwrap_or_else(|| "<jit>".to_string()),
982 functions: self.extract_function_info(),
983 variables: self.extract_variable_info(),
984 line_table: self.generate_line_table(),
985 }
986 }
987
988 fn current_file(&self) -> Option<String> {
990 self.source_files.keys().next().cloned()
991 }
992
993 fn extract_function_info(&self) -> Vec<FunctionDebugInfo> {
995 let mut functions = Vec::new();
996 let mut seen_functions = std::collections::HashSet::new();
997
998 for symbol in self.symbols.values() {
999 if symbol.symbol_type == SymbolType::Function && seen_functions.insert(&symbol.name) {
1000 functions.push(FunctionDebugInfo {
1001 name: symbol.name.clone(),
1002 start_location: symbol.definition.clone(),
1003 parameters: self.find_function_parameters(&symbol.name),
1004 local_variables: self.find_function_locals(&symbol.name),
1005 });
1006 }
1007 }
1008
1009 functions
1010 }
1011
1012 fn find_function_parameters(&self, function_name: &str) -> Vec<String> {
1014 self.symbols
1015 .values()
1016 .filter(|symbol| {
1017 symbol.symbol_type == SymbolType::Parameter
1018 && symbol.definition.function.as_deref() == Some(function_name)
1019 })
1020 .map(|symbol| symbol.name.clone())
1021 .collect()
1022 }
1023
1024 fn find_function_locals(&self, function_name: &str) -> Vec<String> {
1026 self.symbols
1027 .values()
1028 .filter(|symbol| {
1029 matches!(
1030 symbol.symbol_type,
1031 SymbolType::Variable | SymbolType::Temporary
1032 ) && symbol.definition.function.as_deref() == Some(function_name)
1033 })
1034 .map(|symbol| symbol.name.clone())
1035 .collect()
1036 }
1037
1038 fn extract_variable_info(&self) -> Vec<VariableDebugInfo> {
1040 self.symbols
1041 .values()
1042 .filter(|symbol| {
1043 matches!(
1044 symbol.symbol_type,
1045 SymbolType::Variable | SymbolType::Parameter
1046 )
1047 })
1048 .map(|symbol| VariableDebugInfo {
1049 name: symbol.name.clone(),
1050 data_type: symbol.data_type.unwrap_or(DType::F32),
1051 location: symbol.definition.clone(),
1052 shape: symbol.shape.clone(),
1053 })
1054 .collect()
1055 }
1056
1057 fn generate_line_table(&self) -> Vec<LineTableEntry> {
1059 let mut entries = Vec::new();
1060
1061 for (&address, location) in &self.code_to_source {
1062 entries.push(LineTableEntry {
1063 address,
1064 file: location.file.clone(),
1065 line: location.line,
1066 column: location.column,
1067 });
1068 }
1069
1070 entries.sort_by_key(|entry| entry.address);
1071 entries
1072 }
1073}
1074
1075impl Default for SourceMap {
1076 fn default() -> Self {
1077 Self::new()
1078 }
1079}
1080
1081#[derive(Debug, Clone)]
1083pub struct DebugInfo {
1084 pub compilation_unit: String,
1085 pub functions: Vec<FunctionDebugInfo>,
1086 pub variables: Vec<VariableDebugInfo>,
1087 pub line_table: Vec<LineTableEntry>,
1088}
1089
1090#[derive(Debug, Clone)]
1092pub struct FunctionDebugInfo {
1093 pub name: String,
1094 pub start_location: SourceLocation,
1095 pub parameters: Vec<String>,
1096 pub local_variables: Vec<String>,
1097}
1098
1099#[derive(Debug, Clone)]
1101pub struct VariableDebugInfo {
1102 pub name: String,
1103 pub data_type: DType,
1104 pub location: SourceLocation,
1105 pub shape: Option<Shape>,
1106}
1107
1108#[derive(Debug, Clone)]
1110pub struct LineTableEntry {
1111 pub address: usize,
1112 pub file: String,
1113 pub line: u32,
1114 pub column: u32,
1115}
1116
1117pub struct DebugUtils;
1119
1120impl DebugUtils {
1121 pub fn create_source_map_from_graph(graph: &ComputationGraph) -> SourceMap {
1123 let mut builder = SourceMapBuilder::new();
1124 builder.set_current_file(
1125 "generated.py".to_string(),
1126 "# JIT generated code".to_string(),
1127 );
1128
1129 for (node_id, node) in graph.nodes() {
1130 let location = SourceLocation {
1131 file: "generated.py".to_string(),
1132 line: node_id.index() as u32 + 1,
1133 column: 1,
1134 length: node.name.len() as u32,
1135 function: Some("forward".to_string()),
1136 };
1137
1138 builder.add_node_mapping(node_id, location.clone());
1139
1140 let symbol = SymbolInfo {
1142 name: node.name.clone(),
1143 symbol_type: SymbolType::Variable,
1144 definition: location,
1145 data_type: Some(node.dtype),
1146 shape: Some(node.output_shape.clone()),
1147 };
1148 builder.add_symbol(symbol);
1149 }
1150
1151 builder.build()
1152 }
1153
1154 pub fn generate_stack_trace(source_map: &SourceMap, node_id: NodeId) -> String {
1156 if let Some(location) = source_map.get_node_location(node_id) {
1157 let mut trace = String::new();
1158 trace.push_str(&format!(
1159 " File \"{}\", line {}\n",
1160 location.file, location.line
1161 ));
1162
1163 if let Some(source_line) = source_map.get_source_line(location) {
1164 trace.push_str(&format!(" {}\n", source_line.trim()));
1165
1166 let indent = " ".repeat(4 + location.column as usize - 1);
1168 let caret = "^".repeat(location.length as usize);
1169 trace.push_str(&format!(" {}{}\n", indent, caret));
1170 }
1171
1172 if let Some(function) = &location.function {
1173 trace.push_str(&format!(" in {}\n", function));
1174 }
1175
1176 trace
1177 } else {
1178 format!(" Unknown location for node {:?}\n", node_id)
1179 }
1180 }
1181
1182 pub fn format_error_with_source(
1184 source_map: &SourceMap,
1185 node_id: NodeId,
1186 error: &str,
1187 ) -> String {
1188 let mut formatted = String::new();
1189 formatted.push_str(&format!("JIT Error: {}\n", error));
1190 formatted.push_str("Traceback (most recent call last):\n");
1191 formatted.push_str(&Self::generate_stack_trace(source_map, node_id));
1192 formatted
1193 }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1201 fn test_graph_tracer() {
1202 let mut tracer = GraphTracer::new();
1203 tracer.start_tracing();
1204
1205 let input_id =
1207 tracer.create_input("x", Shape::new(vec![32, 128]), DType::F32, DeviceType::Cpu);
1208
1209 let relu_id = tracer
1211 .record_operation(
1212 Operation::Relu,
1213 &[input_id],
1214 Shape::new(vec![32, 128]),
1215 DType::F32,
1216 DeviceType::Cpu,
1217 )
1218 .unwrap();
1219
1220 tracer.mark_output(relu_id);
1221
1222 let graph = tracer.stop_tracing();
1223
1224 assert_eq!(graph.inputs.len(), 1);
1225 assert_eq!(graph.outputs.len(), 1);
1226 assert!(graph.validate().is_ok());
1227 }
1228
1229 #[test]
1230 fn test_traced_value_operations() {
1231 let tracer = Arc::new(Mutex::new(GraphTracer::new()));
1232
1233 {
1234 let mut t = tracer.lock().expect("lock should not be poisoned");
1235 t.start_tracing();
1236 }
1237
1238 let x = TracedValue::input(
1239 "x",
1240 Shape::new(vec![10, 20]),
1241 DType::F32,
1242 DeviceType::Cpu,
1243 tracer.clone(),
1244 );
1245
1246 let y = TracedValue::input(
1247 "y",
1248 Shape::new(vec![10, 20]),
1249 DType::F32,
1250 DeviceType::Cpu,
1251 tracer.clone(),
1252 );
1253
1254 let z = x.add(&y).unwrap();
1255 let w = z.relu().unwrap();
1256
1257 w.mark_as_output();
1258
1259 let graph = {
1260 let mut t = tracer.lock().expect("lock should not be poisoned");
1261 t.stop_tracing()
1262 };
1263
1264 assert_eq!(graph.inputs.len(), 2);
1265 assert_eq!(graph.outputs.len(), 1);
1266 assert!(graph.validate().is_ok());
1267 }
1268
1269 #[test]
1270 fn test_matmul_tracing() {
1271 let tracer = Arc::new(Mutex::new(GraphTracer::new()));
1272
1273 {
1274 let mut t = tracer.lock().expect("lock should not be poisoned");
1275 t.start_tracing();
1276 }
1277
1278 let a = TracedValue::input(
1279 "a",
1280 Shape::new(vec![32, 128]),
1281 DType::F32,
1282 DeviceType::Cpu,
1283 tracer.clone(),
1284 );
1285
1286 let b = TracedValue::input(
1287 "b",
1288 Shape::new(vec![128, 64]),
1289 DType::F32,
1290 DeviceType::Cpu,
1291 tracer.clone(),
1292 );
1293
1294 let c = a.matmul(&b).unwrap();
1295 assert_eq!(c.shape.dims(), &[32, 64]);
1296
1297 c.mark_as_output();
1298
1299 let graph = {
1300 let mut t = tracer.lock().expect("lock should not be poisoned");
1301 t.stop_tracing()
1302 };
1303
1304 assert!(graph.validate().is_ok());
1305 }
1306
1307 #[test]
1308 fn test_profiling() {
1309 let tracer = Arc::new(Mutex::new(GraphTracer::new_with_profiling()));
1310
1311 {
1312 let mut t = tracer.lock().expect("lock should not be poisoned");
1313 t.start_tracing();
1314 }
1315
1316 let x = TracedValue::input(
1317 "x",
1318 Shape::new(vec![1000, 1000]),
1319 DType::F32,
1320 DeviceType::Cpu,
1321 tracer.clone(),
1322 );
1323
1324 let y = TracedValue::input(
1325 "y",
1326 Shape::new(vec![1000, 1000]),
1327 DType::F32,
1328 DeviceType::Cpu,
1329 tracer.clone(),
1330 );
1331
1332 let z1 = x.add(&y).unwrap();
1334 let z2 = z1.relu().unwrap();
1335 let z3 = z2.mul(&x).unwrap();
1336
1337 z3.mark_as_output();
1338
1339 let (graph, analysis) = {
1340 let mut t = tracer.lock().expect("lock should not be poisoned");
1341 let graph = t.stop_tracing();
1342 let analysis = t.get_profiler().map(|p| p.analyze());
1343 (graph, analysis)
1344 };
1345
1346 assert!(graph.validate().is_ok());
1347 assert!(analysis.is_some());
1348
1349 if let Some(analysis) = analysis {
1350 assert!(analysis.op_stats.contains_key("Add"));
1351 assert!(analysis.op_stats.contains_key("Relu"));
1352 assert!(analysis.op_stats.contains_key("Mul"));
1353 assert!(analysis.total_time >= Duration::ZERO);
1354 }
1355 }
1356
1357 #[test]
1358 fn test_profiler_bottleneck_detection() {
1359 let mut profiler = Profiler::new();
1360
1361 profiler.start_op("MatMul".to_string());
1365 let spin_until = std::time::Instant::now() + Duration::from_millis(20);
1366 while std::time::Instant::now() < spin_until {
1367 std::hint::spin_loop();
1368 }
1369 profiler.end_op();
1370
1371 profiler.start_op("Add".to_string());
1374 profiler.end_op();
1375
1376 profiler.record_memory_usage("MatMul".to_string(), 1000000);
1377 profiler.record_memory_usage("Add".to_string(), 100000);
1378
1379 let analysis = profiler.analyze();
1380
1381 assert!(!analysis.bottlenecks.is_empty());
1382 assert!(analysis.bottlenecks[0].op_name == "MatMul");
1383 assert!(analysis.bottlenecks[0].time_percentage > 50.0);
1384 }
1385}