Skip to main content

torsh_jit/
pgo.rs

1//! Profile-Guided Optimization for ToRSh JIT
2//!
3//! This module implements profile-guided optimization (PGO) to improve JIT compilation
4//! performance by using runtime profiling data to guide optimization decisions.
5
6use crate::{ComputationGraph, JitError, JitResult, NodeId};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10use std::time::Duration;
11
12/// Profile-guided optimization manager
13pub struct ProfileGuidedOptimizer {
14    profile_data: Arc<RwLock<ProfileData>>,
15    config: PgoConfig,
16    is_profiling: bool,
17}
18
19/// Configuration for profile-guided optimization
20#[derive(Debug, Clone)]
21pub struct PgoConfig {
22    /// Minimum number of executions before applying optimizations
23    pub min_execution_count: u32,
24
25    /// Threshold for hot path detection (execution frequency)
26    pub hot_path_threshold: f64,
27
28    /// Maximum profile data size to prevent memory bloat
29    pub max_profile_entries: usize,
30
31    /// Enable branch prediction optimizations
32    pub enable_branch_prediction: bool,
33
34    /// Enable loop optimization based on iteration count
35    pub enable_loop_optimization: bool,
36
37    /// Enable function inlining based on call frequency
38    pub enable_inline_optimization: bool,
39
40    /// Profile data persistence file
41    pub profile_file: Option<String>,
42}
43
44impl Default for PgoConfig {
45    fn default() -> Self {
46        Self {
47            min_execution_count: 10,
48            hot_path_threshold: 0.1, // 10% of total executions
49            max_profile_entries: 10000,
50            enable_branch_prediction: true,
51            enable_loop_optimization: true,
52            enable_inline_optimization: true,
53            profile_file: None,
54        }
55    }
56}
57
58/// Runtime profiling data collected during execution
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ProfileData {
61    /// Execution counts for each node
62    node_execution_counts: HashMap<crate::graph::SerializableNodeIndex, u64>,
63
64    /// Average execution times for each node
65    node_execution_times: HashMap<crate::graph::SerializableNodeIndex, Duration>,
66
67    /// Branch taken frequencies
68    branch_frequencies: HashMap<crate::graph::SerializableNodeIndex, BranchData>,
69
70    /// Loop iteration counts
71    loop_iterations: HashMap<crate::graph::SerializableNodeIndex, LoopData>,
72
73    /// Function call frequencies
74    call_frequencies: HashMap<String, u64>,
75
76    /// Memory access patterns
77    memory_patterns: HashMap<crate::graph::SerializableNodeIndex, MemoryPattern>,
78
79    /// Total execution count
80    total_executions: u64,
81}
82
83/// Branch profiling data
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BranchData {
86    /// Number of times branch was taken
87    taken_count: u64,
88
89    /// Number of times branch was not taken
90    not_taken_count: u64,
91
92    /// Prediction accuracy (for adaptive optimization)
93    prediction_accuracy: f64,
94}
95
96/// Loop profiling data
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct LoopData {
99    /// Average number of iterations per execution
100    avg_iterations: f64,
101
102    /// Maximum iterations observed
103    max_iterations: u64,
104
105    /// Minimum iterations observed
106    min_iterations: u64,
107
108    /// Number of loop executions
109    execution_count: u64,
110}
111
112/// Memory access pattern data
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct MemoryPattern {
115    /// Cache hit rate
116    cache_hit_rate: f64,
117
118    /// Average memory latency
119    avg_latency: Duration,
120
121    /// Memory bandwidth utilization
122    bandwidth_utilization: f64,
123
124    /// Access locality score
125    locality_score: f64,
126}
127
128/// Optimization recommendations based on profiling data
129#[derive(Debug, Clone)]
130pub struct OptimizationRecommendation {
131    /// Node to optimize
132    pub node_id: NodeId,
133
134    /// Type of optimization
135    pub optimization_type: OptimizationType,
136
137    /// Expected performance improvement
138    pub expected_improvement: f64,
139
140    /// Confidence level (0.0 to 1.0)
141    pub confidence: f64,
142
143    /// Additional metadata
144    pub metadata: HashMap<String, String>,
145}
146
147/// Types of profile-guided optimizations
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum OptimizationType {
150    /// Inline function calls
151    FunctionInlining,
152
153    /// Optimize branch prediction
154    BranchPrediction,
155
156    /// Unroll loops
157    LoopUnrolling,
158
159    /// Optimize memory layout
160    MemoryLayout,
161
162    /// Vectorize operations
163    Vectorization,
164
165    /// Specialize for hot paths
166    HotPathSpecialization,
167
168    /// Dead code elimination
169    DeadCodeElimination,
170
171    /// Constant propagation
172    ConstantPropagation,
173}
174
175impl ProfileGuidedOptimizer {
176    /// Create a new profile-guided optimizer
177    pub fn new(config: PgoConfig) -> Self {
178        let profile_data = ProfileData {
179            node_execution_counts: HashMap::new(),
180            node_execution_times: HashMap::new(),
181            branch_frequencies: HashMap::new(),
182            loop_iterations: HashMap::new(),
183            call_frequencies: HashMap::new(),
184            memory_patterns: HashMap::new(),
185            total_executions: 0,
186        };
187
188        Self {
189            profile_data: Arc::new(RwLock::new(profile_data)),
190            config,
191            is_profiling: false,
192        }
193    }
194
195    /// Start profiling execution
196    pub fn start_profiling(&mut self) -> JitResult<()> {
197        self.is_profiling = true;
198
199        // Load existing profile data if available
200        if let Some(ref file) = self.config.profile_file {
201            self.load_profile_data(file)?;
202        }
203
204        Ok(())
205    }
206
207    /// Stop profiling execution
208    pub fn stop_profiling(&mut self) -> JitResult<()> {
209        self.is_profiling = false;
210
211        // Save profile data if configured
212        if let Some(ref file) = self.config.profile_file {
213            self.save_profile_data(file)?;
214        }
215
216        Ok(())
217    }
218
219    /// Record execution of a node
220    pub fn record_node_execution(&self, node_id: NodeId, execution_time: Duration) {
221        if !self.is_profiling {
222            return;
223        }
224
225        if let Ok(mut data) = self.profile_data.write() {
226            // Update execution count
227            let serializable_node_id = node_id.into();
228            *data
229                .node_execution_counts
230                .entry(serializable_node_id)
231                .or_insert(0) += 1;
232            data.total_executions += 1;
233
234            // Update average execution time
235            let count = data.node_execution_counts[&serializable_node_id];
236            let entry = data
237                .node_execution_times
238                .entry(serializable_node_id)
239                .or_insert(Duration::ZERO);
240            *entry = (*entry * (count - 1) as u32 + execution_time) / count as u32;
241
242            // Limit profile data size
243            if data.node_execution_counts.len() > self.config.max_profile_entries {
244                self.cleanup_old_data(&mut data);
245            }
246        }
247    }
248
249    /// Record branch taken/not taken
250    pub fn record_branch(&self, node_id: NodeId, taken: bool) {
251        if !self.is_profiling {
252            return;
253        }
254
255        if let Ok(mut data) = self.profile_data.write() {
256            let serializable_node_id = node_id.into();
257            let branch_data = data
258                .branch_frequencies
259                .entry(serializable_node_id)
260                .or_insert(BranchData {
261                    taken_count: 0,
262                    not_taken_count: 0,
263                    prediction_accuracy: 0.5,
264                });
265
266            if taken {
267                branch_data.taken_count += 1;
268            } else {
269                branch_data.not_taken_count += 1;
270            }
271
272            // Update prediction accuracy
273            let total = branch_data.taken_count + branch_data.not_taken_count;
274            let taken_ratio = branch_data.taken_count as f64 / total as f64;
275            branch_data.prediction_accuracy = taken_ratio.max(1.0 - taken_ratio);
276        }
277    }
278
279    /// Record loop execution
280    pub fn record_loop(&self, node_id: NodeId, iterations: u64) {
281        if !self.is_profiling {
282            return;
283        }
284
285        if let Ok(mut data) = self.profile_data.write() {
286            let serializable_node_id = node_id.into();
287            let loop_data = data
288                .loop_iterations
289                .entry(serializable_node_id)
290                .or_insert(LoopData {
291                    avg_iterations: 0.0,
292                    max_iterations: 0,
293                    min_iterations: u64::MAX,
294                    execution_count: 0,
295                });
296
297            loop_data.execution_count += 1;
298            loop_data.max_iterations = loop_data.max_iterations.max(iterations);
299            loop_data.min_iterations = loop_data.min_iterations.min(iterations);
300
301            // Update average
302            let count = loop_data.execution_count;
303            loop_data.avg_iterations =
304                (loop_data.avg_iterations * (count - 1) as f64 + iterations as f64) / count as f64;
305        }
306    }
307
308    /// Record function call
309    pub fn record_function_call(&self, function_name: &str) {
310        if !self.is_profiling {
311            return;
312        }
313
314        if let Ok(mut data) = self.profile_data.write() {
315            *data
316                .call_frequencies
317                .entry(function_name.to_string())
318                .or_insert(0) += 1;
319        }
320    }
321
322    /// Record memory access pattern
323    pub fn record_memory_access(&self, node_id: NodeId, cache_hit: bool, latency: Duration) {
324        if !self.is_profiling {
325            return;
326        }
327
328        if let Ok(mut data) = self.profile_data.write() {
329            // Get execution count first
330            let serializable_node_id = node_id.into();
331            let execution_count = data
332                .node_execution_counts
333                .get(&serializable_node_id)
334                .copied()
335                .unwrap_or(0);
336
337            let pattern =
338                data.memory_patterns
339                    .entry(serializable_node_id)
340                    .or_insert(MemoryPattern {
341                        cache_hit_rate: 0.0,
342                        avg_latency: Duration::ZERO,
343                        bandwidth_utilization: 0.0,
344                        locality_score: 0.0,
345                    });
346
347            // Update cache hit rate
348            if execution_count > 0 {
349                pattern.cache_hit_rate = (pattern.cache_hit_rate * (execution_count - 1) as f64
350                    + if cache_hit { 1.0 } else { 0.0 })
351                    / execution_count as f64;
352
353                pattern.avg_latency = (pattern.avg_latency * (execution_count - 1) as u32
354                    + latency)
355                    / execution_count as u32;
356            }
357        }
358    }
359
360    /// Generate optimization recommendations based on profiling data
361    pub fn generate_recommendations(&self) -> JitResult<Vec<OptimizationRecommendation>> {
362        let data = self
363            .profile_data
364            .read()
365            .map_err(|_| JitError::RuntimeError("Failed to read profile data".to_string()))?;
366
367        if data.total_executions < self.config.min_execution_count as u64 {
368            return Ok(Vec::new());
369        }
370
371        let mut recommendations = Vec::new();
372
373        // Analyze hot paths
374        recommendations.extend(self.analyze_hot_paths(&data)?);
375
376        // Analyze branch predictions
377        if self.config.enable_branch_prediction {
378            recommendations.extend(self.analyze_branches(&data)?);
379        }
380
381        // Analyze loops
382        if self.config.enable_loop_optimization {
383            recommendations.extend(self.analyze_loops(&data)?);
384        }
385
386        // Analyze function calls
387        if self.config.enable_inline_optimization {
388            recommendations.extend(self.analyze_function_calls(&data)?);
389        }
390
391        // Sort by expected improvement
392        recommendations.sort_by(|a, b| {
393            b.expected_improvement
394                .partial_cmp(&a.expected_improvement)
395                .unwrap_or(std::cmp::Ordering::Equal)
396        });
397
398        Ok(recommendations)
399    }
400
401    /// Apply optimizations to a computation graph
402    pub fn apply_optimizations(
403        &self,
404        graph: &mut ComputationGraph,
405        recommendations: &[OptimizationRecommendation],
406    ) -> JitResult<usize> {
407        let mut applied_count = 0;
408
409        for recommendation in recommendations {
410            if recommendation.confidence < 0.7 {
411                continue; // Skip low-confidence optimizations
412            }
413
414            match recommendation.optimization_type {
415                OptimizationType::FunctionInlining => {
416                    if self.apply_function_inlining(graph, recommendation)? {
417                        applied_count += 1;
418                    }
419                }
420                OptimizationType::BranchPrediction => {
421                    if self.apply_branch_optimization(graph, recommendation)? {
422                        applied_count += 1;
423                    }
424                }
425                OptimizationType::LoopUnrolling => {
426                    if self.apply_loop_unrolling(graph, recommendation)? {
427                        applied_count += 1;
428                    }
429                }
430                OptimizationType::HotPathSpecialization => {
431                    if self.apply_hot_path_specialization(graph, recommendation)? {
432                        applied_count += 1;
433                    }
434                }
435                _ => {
436                    // Other optimizations can be implemented as needed
437                }
438            }
439        }
440
441        Ok(applied_count)
442    }
443
444    /// Load profile data from file
445    pub fn load_profile_data(&self, file_path: &str) -> JitResult<()> {
446        match std::fs::read_to_string(file_path) {
447            Ok(contents) => {
448                let loaded_data: ProfileData = serde_json::from_str(&contents).map_err(|e| {
449                    JitError::RuntimeError(format!("Failed to parse profile data: {}", e))
450                })?;
451
452                if let Ok(mut data) = self.profile_data.write() {
453                    *data = loaded_data;
454                }
455                Ok(())
456            }
457            Err(_) => {
458                // File doesn't exist or can't be read, start with empty data
459                Ok(())
460            }
461        }
462    }
463
464    /// Save profile data to file
465    pub fn save_profile_data(&self, file_path: &str) -> JitResult<()> {
466        let data = self
467            .profile_data
468            .read()
469            .map_err(|_| JitError::RuntimeError("Failed to read profile data".to_string()))?;
470
471        let json = serde_json::to_string_pretty(&*data).map_err(|e| {
472            JitError::RuntimeError(format!("Failed to serialize profile data: {}", e))
473        })?;
474
475        std::fs::write(file_path, json)
476            .map_err(|e| JitError::RuntimeError(format!("Failed to write profile data: {}", e)))?;
477
478        Ok(())
479    }
480
481    /// Get profiling statistics
482    pub fn get_statistics(&self) -> JitResult<PgoStatistics> {
483        let data = self
484            .profile_data
485            .read()
486            .map_err(|_| JitError::RuntimeError("Failed to read profile data".to_string()))?;
487
488        let total_nodes = data.node_execution_counts.len();
489        let total_executions = data.node_execution_counts.values().sum::<u64>();
490        let avg_execution_time = if !data.node_execution_times.is_empty() {
491            data.node_execution_times.values().sum::<Duration>()
492                / data.node_execution_times.len() as u32
493        } else {
494            Duration::ZERO
495        };
496
497        let hot_nodes = data
498            .node_execution_counts
499            .iter()
500            .filter(|(_, &count)| {
501                count as f64 / total_executions as f64 > self.config.hot_path_threshold
502            })
503            .count();
504
505        Ok(PgoStatistics {
506            total_nodes,
507            total_executions,
508            avg_execution_time,
509            hot_nodes,
510            branch_count: data.branch_frequencies.len(),
511            loop_count: data.loop_iterations.len(),
512            function_count: data.call_frequencies.len(),
513        })
514    }
515
516    // Helper methods for analysis
517    fn analyze_hot_paths(&self, data: &ProfileData) -> JitResult<Vec<OptimizationRecommendation>> {
518        let mut recommendations = Vec::new();
519        let total_executions = data.node_execution_counts.values().sum::<u64>();
520
521        for (&node_id, &count) in &data.node_execution_counts {
522            let frequency = count as f64 / total_executions as f64;
523            if frequency > self.config.hot_path_threshold {
524                recommendations.push(OptimizationRecommendation {
525                    node_id: node_id.into(),
526                    optimization_type: OptimizationType::HotPathSpecialization,
527                    expected_improvement: frequency * 0.2, // Estimate 20% improvement
528                    confidence: 0.8,
529                    metadata: [("frequency".to_string(), frequency.to_string())].into(),
530                });
531            }
532        }
533
534        Ok(recommendations)
535    }
536
537    fn analyze_branches(&self, data: &ProfileData) -> JitResult<Vec<OptimizationRecommendation>> {
538        let mut recommendations = Vec::new();
539
540        for (&node_id, branch_data) in &data.branch_frequencies {
541            let total = branch_data.taken_count + branch_data.not_taken_count;
542            if total > 100 {
543                // Minimum sample size
544                let bias = (branch_data.taken_count as f64 / total as f64 - 0.5).abs();
545                if bias > 0.3 {
546                    // Highly biased branch
547                    recommendations.push(OptimizationRecommendation {
548                        node_id: node_id.into(),
549                        optimization_type: OptimizationType::BranchPrediction,
550                        expected_improvement: bias * 0.1,
551                        confidence: 0.7,
552                        metadata: [("bias".to_string(), bias.to_string())].into(),
553                    });
554                }
555            }
556        }
557
558        Ok(recommendations)
559    }
560
561    fn analyze_loops(&self, data: &ProfileData) -> JitResult<Vec<OptimizationRecommendation>> {
562        let mut recommendations = Vec::new();
563
564        for (&node_id, loop_data) in &data.loop_iterations {
565            if loop_data.execution_count > 10 {
566                // Recommend unrolling for small, frequent loops
567                if loop_data.avg_iterations < 10.0 && loop_data.avg_iterations > 2.0 {
568                    let improvement = (10.0 - loop_data.avg_iterations) / 10.0 * 0.15;
569                    recommendations.push(OptimizationRecommendation {
570                        node_id: node_id.into(),
571                        optimization_type: OptimizationType::LoopUnrolling,
572                        expected_improvement: improvement,
573                        confidence: 0.6,
574                        metadata: [(
575                            "avg_iterations".to_string(),
576                            loop_data.avg_iterations.to_string(),
577                        )]
578                        .into(),
579                    });
580                }
581            }
582        }
583
584        Ok(recommendations)
585    }
586
587    fn analyze_function_calls(
588        &self,
589        data: &ProfileData,
590    ) -> JitResult<Vec<OptimizationRecommendation>> {
591        let recommendations = Vec::new();
592        let total_calls = data.call_frequencies.values().sum::<u64>();
593
594        for (_function_name, &count) in &data.call_frequencies {
595            let frequency = count as f64 / total_calls as f64;
596            if frequency > 0.05 && count > 50 { // Frequent function calls
597                 // This would need actual node ID mapping from function names
598                 // For now, we'll skip this implementation
599            }
600        }
601
602        Ok(recommendations)
603    }
604
605    fn apply_function_inlining(
606        &self,
607        graph: &mut ComputationGraph,
608        recommendation: &OptimizationRecommendation,
609    ) -> JitResult<bool> {
610        // Find function call nodes and inline them if they meet criteria
611        let node_id = recommendation.node_id;
612
613        let node_name = if let Some(node) = graph.get_node(node_id) {
614            node.name.clone()
615        } else {
616            return Ok(false);
617        };
618
619        if !node_name.is_empty() {
620            // Locate the callee by name in the graph so we can measure its actual size.
621            // `call_node_id` is the call-site node; the callee's body nodes have matching names.
622            let callee_instruction_count = graph
623                .nodes()
624                .filter(|(id, n)| *id != node_id && n.name == node_name)
625                .count();
626
627            // Only inline functions whose body is non-empty (we found it) and small enough
628            // to be profitable (< 50 nodes).  An empty count means we cannot locate the
629            // callee in the current graph — conservatively decline rather than falsely claim
630            // success.
631            if callee_instruction_count == 0 {
632                // Callee not found in this graph — cannot inline.
633                return Ok(false);
634            }
635
636            if callee_instruction_count < 50 {
637                // Build a synthetic instruction list from the callee's node names so that
638                // `inline_function_body` can wire the actual nodes into the call-site.
639                let callee_instructions: Vec<String> = graph
640                    .nodes()
641                    .filter(|(id, n)| *id != node_id && n.name == node_name)
642                    .map(|(_, n)| n.name.clone())
643                    .collect();
644
645                self.inline_function_body(graph, node_id, &callee_instructions)?;
646                return Ok(true);
647            }
648        }
649
650        Ok(false)
651    }
652
653    fn apply_branch_optimization(
654        &self,
655        graph: &mut ComputationGraph,
656        recommendation: &OptimizationRecommendation,
657    ) -> JitResult<bool> {
658        let node_id = recommendation.node_id;
659
660        if let Some(node) = graph.get_node_mut(node_id) {
661            // Get branch statistics from metadata
662            if let Some(bias_str) = recommendation.metadata.get("bias") {
663                if let Ok(bias) = bias_str.parse::<f64>() {
664                    // Add branch prediction hint based on bias
665                    let prediction_hint = if bias > 0.5 { "likely" } else { "unlikely" };
666
667                    // Set branch prediction hint in node metadata
668                    node.set_optimization_hint("branch_prediction", prediction_hint)?;
669
670                    // If branch is highly biased, consider branch elimination
671                    if bias > 0.9 || bias < 0.1 {
672                        node.set_optimization_hint("branch_elimination_candidate", "true")?;
673                    }
674
675                    return Ok(true);
676                }
677            }
678        }
679
680        Ok(false)
681    }
682
683    fn apply_loop_unrolling(
684        &self,
685        graph: &mut ComputationGraph,
686        recommendation: &OptimizationRecommendation,
687    ) -> JitResult<bool> {
688        let node_id = recommendation.node_id;
689
690        if let Some(node) = graph.get_node_mut(node_id) {
691            // Get average iterations from metadata
692            if let Some(avg_iter_str) = recommendation.metadata.get("avg_iterations") {
693                if let Ok(avg_iterations) = avg_iter_str.parse::<f64>() {
694                    // Determine unroll factor based on average iterations
695                    let unroll_factor = if avg_iterations <= 4.0 {
696                        avg_iterations as usize
697                    } else if avg_iterations <= 8.0 {
698                        4
699                    } else {
700                        2
701                    };
702
703                    if unroll_factor > 1 {
704                        // Set loop unrolling optimization hint
705                        node.set_optimization_hint(
706                            "loop_unroll_factor",
707                            &unroll_factor.to_string(),
708                        )?;
709                        node.set_optimization_hint("loop_unroll_enabled", "true")?;
710
711                        // For very small loops, consider full unrolling
712                        if avg_iterations <= 3.0 {
713                            node.set_optimization_hint("loop_full_unroll", "true")?;
714                        }
715
716                        return Ok(true);
717                    }
718                }
719            }
720        }
721
722        Ok(false)
723    }
724
725    fn apply_hot_path_specialization(
726        &self,
727        graph: &mut ComputationGraph,
728        recommendation: &OptimizationRecommendation,
729    ) -> JitResult<bool> {
730        let node_id = recommendation.node_id;
731
732        if let Some(node) = graph.get_node_mut(node_id) {
733            // Get frequency from metadata
734            if let Some(frequency_str) = recommendation.metadata.get("frequency") {
735                if let Ok(frequency) = frequency_str.parse::<f64>() {
736                    // Apply hot path optimizations based on frequency
737                    if frequency > 0.5 {
738                        // Very hot path - aggressive optimizations
739                        node.set_optimization_hint("hot_path_priority", "high")?;
740                        node.set_optimization_hint("aggressive_optimization", "true")?;
741                        node.set_optimization_hint("inline_aggressive", "true")?;
742                        node.set_optimization_hint("vectorize_aggressive", "true")?;
743                    } else if frequency > 0.2 {
744                        // Moderately hot path - standard optimizations
745                        node.set_optimization_hint("hot_path_priority", "medium")?;
746                        node.set_optimization_hint("optimize_for_speed", "true")?;
747                        node.set_optimization_hint("inline_enabled", "true")?;
748                    } else {
749                        // Warm path - basic optimizations
750                        node.set_optimization_hint("hot_path_priority", "low")?;
751                        node.set_optimization_hint("optimize_for_size", "true")?;
752                    }
753
754                    // Create specialized version for hot paths
755                    if frequency > 0.3 {
756                        node.set_optimization_hint("create_specialized_version", "true")?;
757                        node.set_optimization_hint(
758                            "specialization_frequency",
759                            &frequency.to_string(),
760                        )?;
761                    }
762
763                    return Ok(true);
764                }
765            }
766        }
767
768        Ok(false)
769    }
770
771    fn inline_function_body(
772        &self,
773        graph: &mut ComputationGraph,
774        call_node_id: NodeId,
775        function_body: &[String],
776    ) -> JitResult<()> {
777        // Create new nodes for the inlined function body
778        let mut inline_nodes = Vec::new();
779
780        for (i, _instruction_name) in function_body.iter().enumerate() {
781            // Create a placeholder instruction for inlining
782            // In a real implementation, this would convert the instruction properly
783            // Create a placeholder node for inlining
784            let mut inline_node = crate::graph::Node::new(
785                crate::graph::Operation::Add,
786                format!("inline_placeholder_{}", i),
787            );
788            inline_node.device = torsh_core::DeviceType::Cpu;
789            inline_node.inputs = Vec::new();
790            inline_node.is_output = false;
791            let inline_node_id = graph.add_node(inline_node);
792            inline_nodes.push(inline_node_id);
793
794            // Connect nodes in sequence
795            if i > 0 {
796                graph.add_edge(
797                    inline_nodes[i - 1],
798                    inline_node_id,
799                    crate::graph::Edge::default(),
800                );
801            }
802        }
803
804        // Connect the inlined nodes to the graph
805        if !inline_nodes.is_empty() {
806            // Use the call_node_id parameter passed to the function
807
808            // Get incoming edges first and collect data
809            let incoming_edges = graph.incoming_edges(call_node_id);
810            let incoming_data: Vec<_> = incoming_edges
811                .into_iter()
812                .map(|(src, dst, edge)| (src, dst, edge.clone()))
813                .collect();
814
815            // Get outgoing edges and collect data
816            let outgoing_edges = graph.outgoing_edges(call_node_id);
817            let outgoing_data: Vec<_> = outgoing_edges
818                .into_iter()
819                .map(|(src, dst, edge)| (src, dst, edge.clone()))
820                .collect();
821
822            // Connect incoming edges to the first inlined node
823            for (source_id, _dst_id, edge) in incoming_data {
824                graph.add_edge(source_id, inline_nodes[0], edge);
825            }
826
827            // Connect the last inlined node to outgoing edges
828            let last_inline_node = *inline_nodes
829                .last()
830                .expect("inline_nodes should not be empty");
831            for (_src_id, target_id, edge) in outgoing_data {
832                graph.add_edge(last_inline_node, target_id, edge);
833            }
834
835            // Remove the original function call node
836            graph
837                .remove_node(call_node_id)
838                .ok_or_else(|| crate::JitError::GraphError("Failed to remove node".to_string()))?;
839        }
840
841        Ok(())
842    }
843
844    fn cleanup_old_data(&self, data: &mut ProfileData) {
845        // Remove least frequently used entries
846        let mut entries: Vec<_> = data
847            .node_execution_counts
848            .iter()
849            .map(|(&k, &v)| (k, v))
850            .collect();
851        entries.sort_by_key(|(_, count)| *count);
852
853        let remove_count = entries.len() / 10; // Remove 10% of entries
854        let nodes_to_remove: Vec<_> = entries
855            .iter()
856            .take(remove_count)
857            .map(|(node_id, _)| *node_id)
858            .collect();
859
860        for node_id in nodes_to_remove {
861            data.node_execution_counts.remove(&node_id);
862            data.node_execution_times.remove(&node_id);
863            data.branch_frequencies.remove(&node_id);
864            data.loop_iterations.remove(&node_id);
865            data.memory_patterns.remove(&node_id);
866        }
867    }
868}
869
870/// Statistics about profile-guided optimization
871#[derive(Debug, Clone)]
872pub struct PgoStatistics {
873    pub total_nodes: usize,
874    pub total_executions: u64,
875    pub avg_execution_time: Duration,
876    pub hot_nodes: usize,
877    pub branch_count: usize,
878    pub loop_count: usize,
879    pub function_count: usize,
880}
881
882#[cfg(test)]
883mod tests {
884    use super::*;
885    use std::time::Duration;
886
887    #[test]
888    fn test_pgo_creation() {
889        let config = PgoConfig::default();
890        let optimizer = ProfileGuidedOptimizer::new(config);
891        assert!(!optimizer.is_profiling);
892    }
893
894    #[test]
895    fn test_profiling_control() {
896        let mut optimizer = ProfileGuidedOptimizer::new(PgoConfig::default());
897        optimizer.start_profiling().unwrap();
898        assert!(optimizer.is_profiling);
899
900        optimizer.stop_profiling().unwrap();
901        assert!(!optimizer.is_profiling);
902    }
903
904    #[test]
905    fn test_node_execution_recording() {
906        let mut optimizer = ProfileGuidedOptimizer::new(PgoConfig::default());
907        optimizer.start_profiling().unwrap();
908
909        let node_id = NodeId::new(1);
910        optimizer.record_node_execution(node_id, Duration::from_millis(10));
911        optimizer.record_node_execution(node_id, Duration::from_millis(20));
912
913        let stats = optimizer.get_statistics().unwrap();
914        assert_eq!(stats.total_nodes, 1);
915        assert_eq!(stats.total_executions, 2);
916    }
917
918    #[test]
919    fn test_branch_recording() {
920        let mut optimizer = ProfileGuidedOptimizer::new(PgoConfig::default());
921        optimizer.start_profiling().unwrap();
922
923        let node_id = NodeId::new(1);
924        optimizer.record_branch(node_id, true);
925        optimizer.record_branch(node_id, true);
926        optimizer.record_branch(node_id, false);
927
928        let data = optimizer
929            .profile_data
930            .read()
931            .expect("lock should not be poisoned");
932        let branch_data = &data.branch_frequencies[&node_id.into()];
933        assert_eq!(branch_data.taken_count, 2);
934        assert_eq!(branch_data.not_taken_count, 1);
935    }
936
937    #[test]
938    fn test_recommendation_generation() {
939        let mut optimizer = ProfileGuidedOptimizer::new(PgoConfig {
940            min_execution_count: 1,
941            hot_path_threshold: 0.3,
942            ..Default::default()
943        });
944        optimizer.start_profiling().unwrap();
945
946        // Record some executions to create a hot path
947        let node_id = NodeId::new(1);
948        for _ in 0..100 {
949            optimizer.record_node_execution(node_id, Duration::from_millis(5));
950        }
951
952        let recommendations = optimizer.generate_recommendations().unwrap();
953        assert!(!recommendations.is_empty());
954        assert!(recommendations
955            .iter()
956            .any(|r| r.optimization_type == OptimizationType::HotPathSpecialization));
957    }
958}