1use crate::{ComputationGraph, JitError, JitResult, NodeId};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10use std::time::Duration;
11
12pub struct ProfileGuidedOptimizer {
14 profile_data: Arc<RwLock<ProfileData>>,
15 config: PgoConfig,
16 is_profiling: bool,
17}
18
19#[derive(Debug, Clone)]
21pub struct PgoConfig {
22 pub min_execution_count: u32,
24
25 pub hot_path_threshold: f64,
27
28 pub max_profile_entries: usize,
30
31 pub enable_branch_prediction: bool,
33
34 pub enable_loop_optimization: bool,
36
37 pub enable_inline_optimization: bool,
39
40 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, 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#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ProfileData {
61 node_execution_counts: HashMap<crate::graph::SerializableNodeIndex, u64>,
63
64 node_execution_times: HashMap<crate::graph::SerializableNodeIndex, Duration>,
66
67 branch_frequencies: HashMap<crate::graph::SerializableNodeIndex, BranchData>,
69
70 loop_iterations: HashMap<crate::graph::SerializableNodeIndex, LoopData>,
72
73 call_frequencies: HashMap<String, u64>,
75
76 memory_patterns: HashMap<crate::graph::SerializableNodeIndex, MemoryPattern>,
78
79 total_executions: u64,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BranchData {
86 taken_count: u64,
88
89 not_taken_count: u64,
91
92 prediction_accuracy: f64,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct LoopData {
99 avg_iterations: f64,
101
102 max_iterations: u64,
104
105 min_iterations: u64,
107
108 execution_count: u64,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct MemoryPattern {
115 cache_hit_rate: f64,
117
118 avg_latency: Duration,
120
121 bandwidth_utilization: f64,
123
124 locality_score: f64,
126}
127
128#[derive(Debug, Clone)]
130pub struct OptimizationRecommendation {
131 pub node_id: NodeId,
133
134 pub optimization_type: OptimizationType,
136
137 pub expected_improvement: f64,
139
140 pub confidence: f64,
142
143 pub metadata: HashMap<String, String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum OptimizationType {
150 FunctionInlining,
152
153 BranchPrediction,
155
156 LoopUnrolling,
158
159 MemoryLayout,
161
162 Vectorization,
164
165 HotPathSpecialization,
167
168 DeadCodeElimination,
170
171 ConstantPropagation,
173}
174
175impl ProfileGuidedOptimizer {
176 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 pub fn start_profiling(&mut self) -> JitResult<()> {
197 self.is_profiling = true;
198
199 if let Some(ref file) = self.config.profile_file {
201 self.load_profile_data(file)?;
202 }
203
204 Ok(())
205 }
206
207 pub fn stop_profiling(&mut self) -> JitResult<()> {
209 self.is_profiling = false;
210
211 if let Some(ref file) = self.config.profile_file {
213 self.save_profile_data(file)?;
214 }
215
216 Ok(())
217 }
218
219 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 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 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 if data.node_execution_counts.len() > self.config.max_profile_entries {
244 self.cleanup_old_data(&mut data);
245 }
246 }
247 }
248
249 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 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 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 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 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 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 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 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 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 recommendations.extend(self.analyze_hot_paths(&data)?);
375
376 if self.config.enable_branch_prediction {
378 recommendations.extend(self.analyze_branches(&data)?);
379 }
380
381 if self.config.enable_loop_optimization {
383 recommendations.extend(self.analyze_loops(&data)?);
384 }
385
386 if self.config.enable_inline_optimization {
388 recommendations.extend(self.analyze_function_calls(&data)?);
389 }
390
391 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 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; }
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 }
438 }
439 }
440
441 Ok(applied_count)
442 }
443
444 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 Ok(())
460 }
461 }
462 }
463
464 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 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 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, 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 let bias = (branch_data.taken_count as f64 / total as f64 - 0.5).abs();
545 if bias > 0.3 {
546 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 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 { }
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 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 let callee_instruction_count = graph
623 .nodes()
624 .filter(|(id, n)| *id != node_id && n.name == node_name)
625 .count();
626
627 if callee_instruction_count == 0 {
632 return Ok(false);
634 }
635
636 if callee_instruction_count < 50 {
637 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 if let Some(bias_str) = recommendation.metadata.get("bias") {
663 if let Ok(bias) = bias_str.parse::<f64>() {
664 let prediction_hint = if bias > 0.5 { "likely" } else { "unlikely" };
666
667 node.set_optimization_hint("branch_prediction", prediction_hint)?;
669
670 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 if let Some(avg_iter_str) = recommendation.metadata.get("avg_iterations") {
693 if let Ok(avg_iterations) = avg_iter_str.parse::<f64>() {
694 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 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 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 if let Some(frequency_str) = recommendation.metadata.get("frequency") {
735 if let Ok(frequency) = frequency_str.parse::<f64>() {
736 if frequency > 0.5 {
738 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 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 node.set_optimization_hint("hot_path_priority", "low")?;
751 node.set_optimization_hint("optimize_for_size", "true")?;
752 }
753
754 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 let mut inline_nodes = Vec::new();
779
780 for (i, _instruction_name) in function_body.iter().enumerate() {
781 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 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 if !inline_nodes.is_empty() {
806 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 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 for (source_id, _dst_id, edge) in incoming_data {
824 graph.add_edge(source_id, inline_nodes[0], edge);
825 }
826
827 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 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 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; 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#[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 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}