Skip to main content

torsh_jit/
tracing.rs

1//! Graph capture and tracing for JIT compilation
2//!
3//! This module provides automatic graph capture by tracing function execution,
4//! similar to PyTorch's torch.jit.trace functionality.
5
6use 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/// Tracer for capturing computation graphs
14#[derive(Debug)]
15pub struct GraphTracer {
16    /// Current graph being built
17    graph: ComputationGraph,
18
19    /// Mapping from traced values to nodes
20    value_to_node: HashMap<TracedValueId, NodeId>,
21
22    /// Next value ID
23    next_value_id: TracedValueId,
24
25    /// Whether tracing is active
26    active: bool,
27
28    /// Stack of operation contexts
29    op_stack: Vec<OpContext>,
30
31    /// Profiling data
32    profiler: Option<Profiler>,
33}
34
35/// Profiler for collecting execution statistics
36#[derive(Debug)]
37pub struct Profiler {
38    /// Operation timings
39    pub op_timings: HashMap<String, Duration>,
40
41    /// Memory usage tracking
42    pub memory_usage: HashMap<String, usize>,
43
44    /// Operation counts
45    pub op_counts: HashMap<String, usize>,
46
47    /// Start time for current operation
48    current_op_start: Option<Instant>,
49
50    /// Current operation name
51    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/// Traced value identifier
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct TracedValueId(u64);
111
112/// Operation context during tracing
113#[derive(Debug, Clone)]
114struct OpContext {
115    /// Operation being traced
116    #[allow(dead_code)]
117    op: Operation,
118
119    /// Input values
120    #[allow(dead_code)]
121    inputs: Vec<TracedValueId>,
122
123    /// Operation metadata
124    #[allow(dead_code)]
125    metadata: OpMetadata,
126}
127
128/// Operation metadata
129#[derive(Debug, Clone)]
130struct OpMetadata {
131    /// Source location (for debugging)
132    #[allow(dead_code)]
133    source_location: Option<String>,
134
135    /// Operation name
136    #[allow(dead_code)]
137    name: Option<String>,
138
139    /// Additional attributes
140    #[allow(dead_code)]
141    attributes: HashMap<String, String>,
142}
143
144/// Traced value representing a tensor in the computation
145#[derive(Debug, Clone)]
146pub struct TracedValue {
147    /// Unique identifier
148    pub id: TracedValueId,
149
150    /// Shape of the tensor
151    pub shape: Shape,
152
153    /// Data type
154    pub dtype: DType,
155
156    /// Device placement
157    pub device: DeviceType,
158
159    /// Whether this value requires gradient
160    pub requires_grad: bool,
161
162    /// Reference to the tracer
163    tracer: Arc<Mutex<GraphTracer>>,
164}
165
166impl GraphTracer {
167    /// Create a new graph tracer
168    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    /// Create a new graph tracer with profiling enabled
180    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    /// Enable profiling
192    pub fn enable_profiling(&mut self) {
193        self.profiler = Some(Profiler::new());
194    }
195
196    /// Disable profiling
197    pub fn disable_profiling(&mut self) {
198        self.profiler = None;
199    }
200
201    /// Get profiling data
202    pub fn get_profiler(&self) -> Option<&Profiler> {
203        self.profiler.as_ref()
204    }
205
206    /// Start tracing
207    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    /// Stop tracing and return the captured graph
216    pub fn stop_tracing(&mut self) -> ComputationGraph {
217        self.active = false;
218        std::mem::take(&mut self.graph)
219    }
220
221    /// Check if tracing is active
222    pub fn is_tracing(&self) -> bool {
223        self.active
224    }
225
226    /// Create a new traced value ID
227    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    /// Create an input value
234    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    /// Record an operation
263    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        // Start profiling if enabled
278        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        // Create output node
285        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        // Connect inputs
296        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        // Record memory usage estimate
312        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    /// Mark a value as output
322    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    /// Get the current graph (for inspection)
329    pub fn get_graph(&self) -> &ComputationGraph {
330        &self.graph
331    }
332}
333
334impl TracedValue {
335    /// Create a new traced value
336    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    /// Create an input traced value
359    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    /// Perform a unary operation
382    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    /// Perform a binary operation
399    pub fn binary_op(&self, other: &TracedValue, op: Operation) -> JitResult<TracedValue> {
400        // Determine output shape (simplified broadcasting)
401        let output_shape = if self.shape.dims() == other.shape.dims() {
402            self.shape.clone()
403        } else {
404            // Simplified: use larger shape
405            if self.shape.numel() >= other.shape.numel() {
406                self.shape.clone()
407            } else {
408                other.shape.clone()
409            }
410        };
411
412        // Determine output type (use higher precision)
413        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    /// Matrix multiplication
441    pub fn matmul(&self, other: &TracedValue) -> JitResult<TracedValue> {
442        // Compute output shape for matrix multiplication
443        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        // Result shape: batch dims + [m, n]
465        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    /// Reshape operation
498    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                        // Infer dimension
505                        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    /// Mark this value as an output
547    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    // Common operations
553    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
596/// Trace a function and capture its computation graph
597pub 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    // Start tracing
604    {
605        let mut t = tracer.lock().expect("lock should not be poisoned");
606        t.start_tracing();
607    }
608
609    // Execute function
610    let _outputs = func(example_inputs);
611
612    // Stop tracing and get graph
613    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
621/// Get size in bytes for a data type
622fn 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/// Performance analysis results
633#[derive(Debug, Clone)]
634pub struct PerformanceAnalysis {
635    /// Total execution time
636    pub total_time: Duration,
637
638    /// Per-operation statistics
639    pub op_stats: HashMap<String, OpStats>,
640
641    /// Memory usage statistics
642    pub memory_stats: MemoryStats,
643
644    /// Bottleneck analysis
645    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    /// Generate performance analysis
673    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, // Would need more sophisticated tracking
703        };
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/// Utility macro for easy tracing
763#[macro_export]
764macro_rules! trace {
765    ($tracer:expr, $op:expr, $($input:expr),*) => {{
766        let inputs = vec![$($input.id),*];
767        // This would need to be expanded with proper shape/type inference
768        $tracer.lock().expect("lock should not be poisoned").record_operation($op, &inputs, Shape::new(vec![1]), DType::F32, DeviceType::Cpu)
769    }};
770}
771
772/// Source mapping for debugging JIT-compiled code
773#[derive(Debug, Clone)]
774pub struct SourceMap {
775    /// Mapping from node IDs to source locations
776    pub node_to_source: HashMap<NodeId, SourceLocation>,
777
778    /// Mapping from generated code addresses to source locations
779    pub code_to_source: HashMap<usize, SourceLocation>,
780
781    /// Source file contents for reference
782    pub source_files: HashMap<String, String>,
783
784    /// Symbol table for debugging
785    pub symbols: HashMap<String, SymbolInfo>,
786}
787
788/// Source location in original code
789#[derive(Debug, Clone, PartialEq)]
790pub struct SourceLocation {
791    /// Source file name/path
792    pub file: String,
793    /// Line number (1-based)
794    pub line: u32,
795    /// Column number (1-based)
796    pub column: u32,
797    /// Length of the source span
798    pub length: u32,
799    /// Function or scope name
800    pub function: Option<String>,
801}
802
803/// Symbol information for debugging
804#[derive(Debug, Clone)]
805pub struct SymbolInfo {
806    /// Symbol name
807    pub name: String,
808    /// Symbol type (variable, function, etc.)
809    pub symbol_type: SymbolType,
810    /// Source location where defined
811    pub definition: SourceLocation,
812    /// Data type information
813    pub data_type: Option<DType>,
814    /// Shape information for tensors
815    pub shape: Option<Shape>,
816}
817
818/// Types of symbols
819#[derive(Debug, Clone, PartialEq)]
820pub enum SymbolType {
821    Variable,
822    Function,
823    Parameter,
824    Constant,
825    Temporary,
826}
827
828/// Source map builder for creating debug information
829pub struct SourceMapBuilder {
830    source_map: SourceMap,
831    current_file: Option<String>,
832    current_function: Option<String>,
833}
834
835impl SourceMapBuilder {
836    /// Create a new source map builder
837    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    /// Set the current source file
851    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    /// Set the current function context
857    pub fn set_current_function(&mut self, function: String) {
858        self.current_function = Some(function);
859    }
860
861    /// Add a mapping from node to source location
862    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    /// Add a mapping from generated code address to source location
867    pub fn add_code_mapping(&mut self, address: usize, location: SourceLocation) {
868        self.source_map.code_to_source.insert(address, location);
869    }
870
871    /// Add symbol information
872    pub fn add_symbol(&mut self, symbol: SymbolInfo) {
873        self.source_map.symbols.insert(symbol.name.clone(), symbol);
874    }
875
876    /// Create a source location with current context
877    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    /// Build the final source map
891    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    /// Create an empty source map
904    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    /// Get source location for a node
914    pub fn get_node_location(&self, node_id: NodeId) -> Option<&SourceLocation> {
915        self.node_to_source.get(&node_id)
916    }
917
918    /// Get source location for a code address
919    pub fn get_code_location(&self, address: usize) -> Option<&SourceLocation> {
920        self.code_to_source.get(&address)
921    }
922
923    /// Get symbol information
924    pub fn get_symbol(&self, name: &str) -> Option<&SymbolInfo> {
925        self.symbols.get(name)
926    }
927
928    /// Get source line for a location
929    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    /// Get context lines around a location
939    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    /// Find all locations in a file
962    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    /// Find all symbols of a specific type
971    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    /// Generate debug information for external debuggers
979    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    /// Get the current file being debugged
989    fn current_file(&self) -> Option<String> {
990        self.source_files.keys().next().cloned()
991    }
992
993    /// Extract function debug information
994    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    /// Find parameters for a function
1013    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    /// Find local variables for a function
1025    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    /// Extract variable debug information
1039    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    /// Generate line number table for debugging
1058    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/// Debug information for external debuggers
1082#[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/// Function debug information
1091#[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/// Variable debug information
1100#[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/// Line table entry for debugging
1109#[derive(Debug, Clone)]
1110pub struct LineTableEntry {
1111    pub address: usize,
1112    pub file: String,
1113    pub line: u32,
1114    pub column: u32,
1115}
1116
1117/// Debugging utilities
1118pub struct DebugUtils;
1119
1120impl DebugUtils {
1121    /// Create a source map from a computation graph with simple heuristics
1122    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            // Add symbol for this node
1141            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    /// Generate a stack trace from a source map and node ID
1155    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                // Add caret pointing to the column
1167                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    /// Format an error with source information
1183    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        // Create input
1206        let input_id =
1207            tracer.create_input("x", Shape::new(vec![32, 128]), DType::F32, DeviceType::Cpu);
1208
1209        // Record ReLU operation
1210        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        // Perform several operations
1333        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        // Burn CPU time for MatMul with a spin loop so the measured duration is
1362        // deterministic regardless of OS scheduling jitter (thread::sleep is
1363        // unreliable at sub-millisecond granularity under heavy test-suite load).
1364        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        // Add is recorded without any deliberate delay so its elapsed time is
1372        // negligibly small compared to MatMul regardless of system load.
1373        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}