Skip to main content

oxirs/commands/
performance.rs

1//! Performance monitoring and profiling CLI commands
2//!
3//! This module provides comprehensive CLI commands for performance monitoring,
4//! profiling operations, and benchmarking within the Oxirs CLI toolkit.
5
6use crate::{
7    cli::{
8        error::{CliError, CliErrorKind},
9        output::OutputFormatter,
10    },
11    config::Config,
12    tools::performance::{MonitoringConfig, PerformanceMonitor, ProfilingResult},
13};
14use clap::{Args, Subcommand};
15use serde_json;
16use std::{path::PathBuf, time::Duration};
17use tokio::fs;
18use tracing::{info, warn};
19
20/// Performance monitoring and profiling commands
21#[derive(Debug, Args)]
22pub struct PerformanceArgs {
23    #[command(subcommand)]
24    pub command: PerformanceCommand,
25}
26
27/// Performance command variants
28#[derive(Debug, Subcommand)]
29pub enum PerformanceCommand {
30    /// Monitor system performance in real-time
31    Monitor(MonitorCommand),
32    /// Profile command execution
33    Profile(ProfileCommand),
34    /// Compare benchmark results
35    Compare(CompareCommand),
36    /// System health check
37    Health(HealthCommand),
38    /// Performance report generation
39    Report(ReportCommand),
40    /// Optimize RDF dataset performance with SciRS2-powered analysis
41    Optimizer(OptimizerCommand),
42    /// Analyze SPARQL queries for best practices and optimization opportunities
43    Advisor(AdvisorCommand),
44    /// ML-powered query performance prediction with confidence intervals
45    Predictor(PredictorCommand),
46}
47
48/// Performance monitoring command
49#[derive(Debug, Args)]
50pub struct MonitorCommand {
51    /// Duration to monitor in seconds (default: continuous)
52    #[arg(short, long)]
53    pub duration: Option<u64>,
54
55    /// Sampling interval in milliseconds
56    #[arg(short = 'I', long, default_value = "1000")]
57    pub interval: u64,
58
59    /// Output format (table, json, csv)
60    #[arg(short, long, default_value = "table")]
61    pub format: String,
62
63    /// Save monitoring data to file
64    #[arg(short = 's', long)]
65    pub save: Option<PathBuf>,
66
67    /// Enable continuous monitoring mode
68    #[arg(short = 'C', long)]
69    pub continuous: bool,
70
71    /// Monitor specific metrics only
72    #[arg(long)]
73    pub metrics: Option<Vec<String>>,
74
75    /// Set alert thresholds
76    #[arg(long)]
77    pub cpu_threshold: Option<f32>,
78
79    /// Memory usage threshold percentage
80    #[arg(long)]
81    pub memory_threshold: Option<f32>,
82}
83
84/// Profiling command
85#[derive(Debug, Args)]
86pub struct ProfileCommand {
87    /// Operation name to profile
88    #[arg(short, long)]
89    pub operation: String,
90
91    /// Command to profile
92    pub command: Vec<String>,
93
94    /// Output format (table, json, detailed)
95    #[arg(short, long, default_value = "table")]
96    pub format: String,
97
98    /// Save profiling results to file
99    #[arg(short = 's', long)]
100    pub save: Option<PathBuf>,
101
102    /// Enable detailed checkpointing
103    #[arg(short, long)]
104    pub detailed: bool,
105
106    /// Add custom metrics
107    #[arg(long)]
108    pub metrics: Option<Vec<String>>,
109
110    /// Profile memory allocations
111    #[arg(long)]
112    pub memory: bool,
113
114    /// Profile CPU usage
115    #[arg(long)]
116    pub cpu: bool,
117
118    /// Profile I/O operations
119    #[arg(long)]
120    pub io: bool,
121
122    /// Generate flame graph visualization
123    #[arg(long)]
124    pub flamegraph: bool,
125
126    /// Flame graph output file (default: `profile_<timestamp>.svg`)
127    #[arg(long)]
128    pub flamegraph_output: Option<PathBuf>,
129}
130
131/// Benchmark comparison command
132#[derive(Debug, Args)]
133pub struct CompareCommand {
134    /// Baseline benchmark file
135    #[arg(short, long)]
136    pub baseline: PathBuf,
137
138    /// Current benchmark file
139    #[arg(short = 'C', long)]
140    pub current: PathBuf,
141
142    /// Output format (table, json, report)
143    #[arg(short, long, default_value = "table")]
144    pub format: String,
145
146    /// Save comparison report
147    #[arg(short = 's', long)]
148    pub save: Option<PathBuf>,
149
150    /// Significance threshold percentage
151    #[arg(long, default_value = "5.0")]
152    pub threshold: f64,
153
154    /// Include detailed metrics
155    #[arg(short, long)]
156    pub detailed: bool,
157
158    /// Generate improvement recommendations
159    #[arg(long)]
160    pub recommendations: bool,
161}
162
163/// System health check command
164#[derive(Debug, Args)]
165pub struct HealthCommand {
166    /// Output format (table, json, summary)
167    #[arg(short, long, default_value = "table")]
168    pub format: String,
169
170    /// Include system recommendations
171    #[arg(short, long)]
172    pub recommendations: bool,
173
174    /// Check specific components
175    #[arg(long)]
176    pub components: Option<Vec<String>>,
177
178    /// Set health check thresholds
179    #[arg(long)]
180    pub thresholds: Option<PathBuf>,
181
182    /// Run continuous health monitoring
183    #[arg(short = 'C', long)]
184    pub continuous: bool,
185
186    /// Health check interval in seconds
187    #[arg(long, default_value = "30")]
188    pub interval: u64,
189}
190
191/// Performance report command
192#[derive(Debug, Args)]
193pub struct ReportCommand {
194    /// Report time period in hours
195    #[arg(short, long, default_value = "24")]
196    pub period: u64,
197
198    /// Output format (html, pdf, json, markdown)
199    #[arg(short, long, default_value = "html")]
200    pub format: String,
201
202    /// Output file path
203    #[arg(short, long)]
204    pub output: Option<PathBuf>,
205
206    /// Include performance graphs
207    #[arg(short, long)]
208    pub graphs: bool,
209
210    /// Include system metrics
211    #[arg(long)]
212    pub system_metrics: bool,
213
214    /// Include profiling data
215    #[arg(long)]
216    pub profiling_data: bool,
217
218    /// Include recommendations
219    #[arg(long)]
220    pub recommendations: bool,
221
222    /// Custom report template
223    #[arg(long)]
224    pub template: Option<PathBuf>,
225}
226
227/// RDF dataset performance optimizer command
228#[derive(Debug, Args)]
229pub struct OptimizerCommand {
230    /// Dataset path to optimize
231    pub dataset: PathBuf,
232
233    /// Output format (text, json)
234    #[arg(short, long, default_value = "text")]
235    pub format: String,
236
237    /// Save optimization report
238    #[arg(short = 's', long)]
239    pub save: Option<PathBuf>,
240
241    /// Include detailed analysis
242    #[arg(short, long)]
243    pub detailed: bool,
244}
245
246/// SPARQL query advisor command
247#[derive(Debug, Args)]
248pub struct AdvisorCommand {
249    /// SPARQL query string or file path
250    pub query: String,
251
252    /// Query is a file path
253    #[arg(short, long)]
254    pub file: bool,
255
256    /// Output format (text, json)
257    #[arg(short = 'o', long, default_value = "text")]
258    pub format: String,
259
260    /// Generate verbose report with metrics
261    #[arg(short = 'V', long)]
262    pub verbose: bool,
263
264    /// Save analysis report
265    #[arg(short = 's', long)]
266    pub save: Option<PathBuf>,
267}
268
269/// ML-powered query performance predictor command
270#[derive(Debug, Args)]
271pub struct PredictorCommand {
272    /// SPARQL query string or file path
273    pub query: String,
274
275    /// Query is a file path
276    #[arg(short, long)]
277    pub file: bool,
278
279    /// Training data file (optional - JSON format with query-time pairs)
280    #[arg(short, long)]
281    pub train: Option<PathBuf>,
282
283    /// Output format (text, json)
284    #[arg(short = 'o', long, default_value = "text")]
285    pub format: String,
286
287    /// Save prediction report
288    #[arg(short = 's', long)]
289    pub save: Option<PathBuf>,
290
291    /// Show detailed feature analysis
292    #[arg(short, long)]
293    pub detailed: bool,
294}
295
296impl PerformanceCommand {
297    /// Execute the performance command
298    pub async fn execute(&self, config: &Config) -> Result<(), CliError> {
299        match self {
300            PerformanceCommand::Monitor(cmd) => cmd.execute(config).await,
301            PerformanceCommand::Profile(cmd) => cmd.execute(config).await,
302            PerformanceCommand::Compare(cmd) => cmd.execute(config).await,
303            PerformanceCommand::Health(cmd) => cmd.execute(config).await,
304            PerformanceCommand::Report(cmd) => cmd.execute(config).await,
305            PerformanceCommand::Optimizer(cmd) => cmd.execute(config).await,
306            PerformanceCommand::Advisor(cmd) => cmd.execute(config).await,
307            PerformanceCommand::Predictor(cmd) => cmd.execute(config).await,
308        }
309    }
310}
311
312impl MonitorCommand {
313    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
314        info!("Starting performance monitoring");
315
316        let monitoring_config = MonitoringConfig {
317            enable_continuous_monitoring: true,
318            sampling_interval_ms: self.interval,
319            memory_tracking: true,
320            cpu_tracking: true,
321            io_tracking: true,
322            network_tracking: true,
323            auto_profiling: false,
324            profile_threshold_ms: 100,
325            max_sessions: 100,
326        };
327
328        let monitor = PerformanceMonitor::new(monitoring_config);
329        monitor.start_monitoring().await?;
330
331        if self.continuous || self.duration.is_none() {
332            info!("Running continuous monitoring (Ctrl+C to stop)");
333
334            // Set up monitoring loop
335            let mut interval = tokio::time::interval(Duration::from_millis(self.interval));
336
337            loop {
338                tokio::select! {
339                    _ = interval.tick() => {
340                        let report = monitor.generate_performance_report()?;
341
342                        // Check thresholds and alert if necessary
343                        if let Some(cpu_threshold) = self.cpu_threshold {
344                            if report.current_metrics.cpu_usage > cpu_threshold {
345                                warn!("CPU usage ({:.1}%) exceeds threshold ({:.1}%)",
346                                     report.current_metrics.cpu_usage, cpu_threshold);
347                            }
348                        }
349
350                        if let Some(memory_threshold) = self.memory_threshold {
351                            let memory_percentage = (report.current_metrics.memory_usage as f64 /
352                                                   report.current_metrics.memory_total as f64) * 100.0;
353                            if memory_percentage > memory_threshold as f64 {
354                                warn!("Memory usage ({:.1}%) exceeds threshold ({:.1}%)",
355                                     memory_percentage, memory_threshold);
356                            }
357                        }
358
359                        let formatter = OutputFormatter::new(&self.format);
360                        formatter.print_performance_report(&report)?;
361
362                        // Save to file if specified
363                        if let Some(save_path) = &self.save {
364                            let json_data = serde_json::to_string_pretty(&report)
365                                .map_err(|e| CliError::serialization_error(e.to_string()))?;
366                            fs::write(save_path, json_data).await
367                                .map_err(|e| CliError::io_error(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to save monitoring data: {e}"))))?;
368                        }
369                    }
370                    _ = tokio::signal::ctrl_c() => {
371                        info!("Monitoring stopped by user");
372                        break;
373                    }
374                }
375            }
376        } else {
377            let duration_secs = self.duration.unwrap_or(60); // Default to 60 seconds
378            let duration = Duration::from_secs(duration_secs);
379            info!("Monitoring for {} seconds", duration.as_secs());
380
381            tokio::time::sleep(duration).await;
382
383            let report = monitor.generate_performance_report()?;
384            let formatter = OutputFormatter::new(&self.format);
385            formatter.print_performance_report(&report)?;
386
387            if let Some(save_path) = &self.save {
388                let json_data = serde_json::to_string_pretty(&report)
389                    .map_err(|e| CliError::serialization_error(e.to_string()))?;
390                fs::write(save_path, json_data).await.map_err(|e| {
391                    CliError::io_error(std::io::Error::new(
392                        std::io::ErrorKind::Other,
393                        format!("Failed to save report: {e}"),
394                    ))
395                })?;
396                info!("Report saved to {}", save_path.display());
397            }
398        }
399
400        Ok(())
401    }
402}
403
404impl ProfileCommand {
405    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
406        info!("Starting profiling for operation: {}", self.operation);
407
408        let monitoring_config = MonitoringConfig {
409            enable_continuous_monitoring: true,
410            sampling_interval_ms: 100, // Higher frequency for profiling
411            memory_tracking: self.memory,
412            cpu_tracking: self.cpu,
413            io_tracking: self.io,
414            network_tracking: true,
415            auto_profiling: true,
416            profile_threshold_ms: 10,
417            max_sessions: 50,
418        };
419
420        let monitor = PerformanceMonitor::new(monitoring_config);
421        monitor.start_monitoring().await?;
422
423        let session_id = monitor.start_profiling(&self.operation)?;
424
425        if self.detailed {
426            monitor.add_checkpoint(&session_id, "initialization")?;
427        }
428
429        // Execute the command being profiled
430        if !self.command.is_empty() {
431            info!("Executing command: {}", self.command.join(" "));
432
433            if self.detailed {
434                monitor.add_checkpoint(&session_id, "command_start")?;
435            }
436
437            // Here we would integrate with actual command execution
438            // For now, simulate command execution
439            let execution_duration = Duration::from_millis(500); // Simulated
440            tokio::time::sleep(execution_duration).await;
441
442            if self.detailed {
443                monitor.add_checkpoint(&session_id, "command_complete")?;
444            }
445        }
446
447        if self.detailed {
448            monitor.add_checkpoint(&session_id, "finalization")?;
449        }
450
451        let result = monitor.finish_profiling(&session_id)?;
452
453        let formatter = OutputFormatter::new(&self.format);
454        formatter.print_profiling_result(&result)?;
455
456        if let Some(save_path) = &self.save {
457            let json_data = serde_json::to_string_pretty(&result)
458                .map_err(|e| CliError::serialization_error(e.to_string()))?;
459            fs::write(save_path, json_data).await.map_err(|e| {
460                CliError::io_error(std::io::Error::new(
461                    std::io::ErrorKind::Other,
462                    format!("Failed to save profile: {e}"),
463                ))
464            })?;
465            info!("Profiling results saved to {}", save_path.display());
466        }
467
468        // Generate flame graph if requested
469        if self.flamegraph {
470            let flamegraph_path = self.flamegraph_output.clone().unwrap_or_else(|| {
471                PathBuf::from(format!(
472                    "profile_{}.svg",
473                    chrono::Utc::now().format("%Y%m%d_%H%M%S")
474                ))
475            });
476
477            generate_flamegraph(&result, &flamegraph_path).await?;
478            info!("Flame graph generated: {}", flamegraph_path.display());
479        }
480
481        // Print performance summary
482        println!("\nPerformance Summary:");
483        println!("  Operation: {}", result.operation_name);
484        println!("  Duration: {:.3}s", result.total_duration.as_secs_f64());
485        println!(
486            "  Efficiency Score: {:.1}/100",
487            result.performance_summary.efficiency_score
488        );
489        println!(
490            "  Memory Delta: {:.2} MB",
491            result.performance_summary.memory_delta_bytes as f64 / 1_000_000.0
492        );
493        println!(
494            "  Average CPU: {:.1}%",
495            result.performance_summary.average_cpu_usage
496        );
497
498        Ok(())
499    }
500}
501
502impl CompareCommand {
503    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
504        info!(
505            "Comparing benchmarks: {} vs {}",
506            self.baseline.display(),
507            self.current.display()
508        );
509
510        // Load baseline results
511        let baseline_data = fs::read_to_string(&self.baseline).await.map_err(|e| {
512            CliError::io_error(std::io::Error::new(
513                std::io::ErrorKind::Other,
514                format!("Failed to read baseline file: {e}"),
515            ))
516        })?;
517        let baseline: ProfilingResult = serde_json::from_str(&baseline_data)
518            .map_err(|e| CliError::serialization_error(format!("Failed to parse baseline: {e}")))?;
519
520        // Load current results
521        let current_data = fs::read_to_string(&self.current).await.map_err(|e| {
522            CliError::io_error(std::io::Error::new(
523                std::io::ErrorKind::Other,
524                format!("Failed to read current file: {e}"),
525            ))
526        })?;
527        let current: ProfilingResult = serde_json::from_str(&current_data)
528            .map_err(|e| CliError::serialization_error(format!("Failed to parse current: {e}")))?;
529
530        // Perform comparison
531        let config = MonitoringConfig::default();
532        let monitor = PerformanceMonitor::new(config);
533        let comparison = monitor.compare_benchmarks(&baseline, &current)?;
534
535        let formatter = OutputFormatter::new(&self.format);
536        formatter.print_benchmark_comparison(&comparison)?;
537
538        // Print summary
539        println!("\nComparison Summary:");
540        println!("  {}", comparison.improvement_summary);
541        println!("  Time Ratio: {:.3}x", comparison.time_ratio);
542        println!("  Memory Ratio: {:.3}x", comparison.memory_ratio);
543        println!(
544            "  Overall Performance: {:.3}x",
545            comparison.performance_ratio
546        );
547
548        if self.recommendations {
549            println!("\nRecommendations:");
550            if comparison.performance_ratio < 0.9 {
551                println!("  ✅ Excellent performance improvement!");
552                println!("  ✅ Consider documenting the optimizations made");
553            } else if comparison.performance_ratio > 1.1 {
554                println!("  ⚠️  Performance regression detected");
555                println!("  ⚠️  Review recent changes for potential issues");
556                println!("  ⚠️  Consider profiling to identify bottlenecks");
557            } else {
558                println!("  ℹ️  Performance is comparable");
559                println!("  ℹ️  Consider micro-optimizations if needed");
560            }
561        }
562
563        if let Some(save_path) = &self.save {
564            let json_data = serde_json::to_string_pretty(&comparison)
565                .map_err(|e| CliError::serialization_error(e.to_string()))?;
566            fs::write(save_path, json_data).await.map_err(|e| {
567                CliError::io_error(std::io::Error::new(
568                    std::io::ErrorKind::Other,
569                    format!("Failed to save comparison: {e}"),
570                ))
571            })?;
572            info!("Comparison report saved to {}", save_path.display());
573        }
574
575        Ok(())
576    }
577}
578
579impl HealthCommand {
580    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
581        info!("Performing system health check");
582
583        let config = MonitoringConfig::default();
584        let monitor = PerformanceMonitor::new(config);
585        monitor.start_monitoring().await?;
586
587        if self.continuous {
588            info!("Running continuous health monitoring (Ctrl+C to stop)");
589
590            let mut interval = tokio::time::interval(Duration::from_secs(self.interval));
591
592            loop {
593                tokio::select! {
594                    _ = interval.tick() => {
595                        let report = monitor.generate_performance_report()?;
596
597                        let formatter = OutputFormatter::new(&self.format);
598                        formatter.print_system_health(&report.system_health)?;
599
600                        if self.recommendations {
601                            println!("\nRecommendations:");
602                            for rec in &report.recommendations {
603                                println!("  • {rec}");
604                            }
605                        }
606                    }
607                    _ = tokio::signal::ctrl_c() => {
608                        info!("Health monitoring stopped by user");
609                        break;
610                    }
611                }
612            }
613        } else {
614            let report = monitor.generate_performance_report()?;
615
616            let formatter = OutputFormatter::new(&self.format);
617            formatter.print_system_health(&report.system_health)?;
618
619            if self.recommendations {
620                println!("\nSystem Recommendations:");
621                for rec in &report.recommendations {
622                    println!("  • {rec}");
623                }
624
625                println!("\nHealth-specific Recommendations:");
626                for rec in &report.system_health.recommendations {
627                    println!("  • {rec}");
628                }
629            }
630        }
631
632        Ok(())
633    }
634}
635
636impl ReportCommand {
637    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
638        info!("Generating performance report for {} hours", self.period);
639
640        let config = MonitoringConfig::default();
641        let monitor = PerformanceMonitor::new(config);
642        monitor.start_monitoring().await?;
643
644        let report = monitor.generate_performance_report()?;
645
646        match self.format.as_str() {
647            "html" => self.generate_html_report(&report).await?,
648            #[cfg(feature = "pdf-export")]
649            "pdf" => self.generate_pdf_report(&report).await?,
650            #[cfg(not(feature = "pdf-export"))]
651            "pdf" => {
652                return Err(CliError::invalid_arguments(
653                    "PDF export requires the 'pdf-export' feature. Recompile with --features pdf-export".to_string(),
654                ));
655            }
656            "markdown" => self.generate_markdown_report(&report).await?,
657            "json" => self.generate_json_report(&report).await?,
658            _ => {
659                let formatter = OutputFormatter::new(&self.format);
660                formatter.print_performance_report(&report)?;
661            }
662        }
663
664        Ok(())
665    }
666
667    async fn generate_html_report(
668        &self,
669        report: &crate::tools::performance::PerformanceReport,
670    ) -> Result<(), CliError> {
671        let html_content = format!(
672            r#"<!DOCTYPE html>
673<html>
674<head>
675    <title>OxiRS Performance Report</title>
676    <style>
677        body {{ font-family: Arial, sans-serif; margin: 40px; }}
678        .header {{ background: #f4f4f4; padding: 20px; border-radius: 5px; }}
679        .metric {{ margin: 10px 0; }}
680        .status-healthy {{ color: green; }}
681        .status-warning {{ color: orange; }}
682        .status-critical {{ color: red; }}
683    </style>
684</head>
685<body>
686    <div class="header">
687        <h1>OxiRS Performance Report</h1>
688        <p>Generated: {}</p>
689    </div>
690    
691    <h2>System Health</h2>
692    <div class="metric">Status: <span class="status-{:?}">{:?}</span></div>
693    <div class="metric">CPU Usage: {:.1}%</div>
694    <div class="metric">Memory Usage: {:.1}%</div>
695    
696    <h2>Performance Metrics</h2>
697    <div class="metric">Memory Used: {:.2} GB</div>
698    <div class="metric">Memory Total: {:.2} GB</div>
699    <div class="metric">Active Sessions: {}</div>
700    
701    <h2>Recommendations</h2>
702    <ul>
703    {}
704    </ul>
705</body>
706</html>"#,
707            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
708            report.system_health.status,
709            report.system_health.status,
710            report.system_health.cpu_usage_percentage,
711            report.system_health.memory_usage_percentage,
712            report.current_metrics.memory_usage as f64 / 1_000_000_000.0,
713            report.current_metrics.memory_total as f64 / 1_000_000_000.0,
714            report.active_profiling_sessions,
715            report
716                .recommendations
717                .iter()
718                .map(|r| format!("<li>{r}</li>"))
719                .collect::<Vec<_>>()
720                .join("\n    ")
721        );
722
723        let output_path = self
724            .output
725            .as_ref()
726            .cloned()
727            .unwrap_or_else(|| PathBuf::from("performance_report.html"));
728
729        fs::write(&output_path, html_content).await.map_err(|e| {
730            CliError::io_error(std::io::Error::new(
731                std::io::ErrorKind::Other,
732                format!("Failed to write HTML report: {e}"),
733            ))
734        })?;
735
736        info!("HTML report saved to {}", output_path.display());
737        Ok(())
738    }
739
740    #[cfg(feature = "pdf-export")]
741    async fn generate_pdf_report(
742        &self,
743        report: &crate::tools::performance::PerformanceReport,
744    ) -> Result<(), CliError> {
745        use printpdf::*;
746
747        info!("Generating PDF performance report");
748
749        // Create PDF document
750        let mut doc = PdfDocument::new("OxiRS Performance Report");
751
752        // Build operations
753        let mut ops = vec![Op::SaveGraphicsState, Op::StartTextSection];
754
755        // Title
756        ops.push(Op::SetTextCursor {
757            pos: Point::new(Mm(20.0), Mm(270.0)),
758        });
759        ops.push(Op::SetFont {
760            font: PdfFontHandle::Builtin(BuiltinFont::HelveticaBold),
761            size: Pt(24.0),
762        });
763        ops.push(Op::SetLineHeight { lh: Pt(24.0) });
764        ops.push(Op::ShowText {
765            items: vec![TextItem::Text("OxiRS Performance Report".to_string())],
766        });
767        ops.push(Op::AddLineBreak);
768        ops.push(Op::AddLineBreak);
769
770        // Timestamp
771        let timestamp = chrono::Utc::now()
772            .format("%Y-%m-%d %H:%M:%S UTC")
773            .to_string();
774        ops.push(Op::SetFont {
775            font: PdfFontHandle::Builtin(BuiltinFont::Helvetica),
776            size: Pt(10.0),
777        });
778        ops.push(Op::SetLineHeight { lh: Pt(12.0) });
779        ops.push(Op::ShowText {
780            items: vec![TextItem::Text(format!("Generated: {}", timestamp))],
781        });
782        ops.push(Op::AddLineBreak);
783        ops.push(Op::AddLineBreak);
784
785        // System Health Section
786        ops.push(Op::SetFont {
787            font: PdfFontHandle::Builtin(BuiltinFont::HelveticaBold),
788            size: Pt(16.0),
789        });
790        ops.push(Op::SetLineHeight { lh: Pt(18.0) });
791        ops.push(Op::ShowText {
792            items: vec![TextItem::Text("System Health".to_string())],
793        });
794        ops.push(Op::AddLineBreak);
795
796        ops.push(Op::SetFont {
797            font: PdfFontHandle::Builtin(BuiltinFont::Helvetica),
798            size: Pt(10.0),
799        });
800        ops.push(Op::SetLineHeight { lh: Pt(12.0) });
801
802        let health_items = vec![
803            format!("Status: {:?}", report.system_health.status),
804            format!(
805                "CPU Usage: {:.1}%",
806                report.system_health.cpu_usage_percentage
807            ),
808            format!(
809                "Memory Usage: {:.1}%",
810                report.system_health.memory_usage_percentage
811            ),
812            format!(
813                "Disk Issues: {}",
814                if report.system_health.disk_space_issues {
815                    "Yes"
816                } else {
817                    "No"
818                }
819            ),
820        ];
821
822        for item in health_items {
823            ops.push(Op::ShowText {
824                items: vec![TextItem::Text(item)],
825            });
826            ops.push(Op::AddLineBreak);
827        }
828        ops.push(Op::AddLineBreak);
829
830        // Performance Metrics Section
831        ops.push(Op::SetFont {
832            font: PdfFontHandle::Builtin(BuiltinFont::HelveticaBold),
833            size: Pt(16.0),
834        });
835        ops.push(Op::SetLineHeight { lh: Pt(18.0) });
836        ops.push(Op::ShowText {
837            items: vec![TextItem::Text("Performance Metrics".to_string())],
838        });
839        ops.push(Op::AddLineBreak);
840
841        ops.push(Op::SetFont {
842            font: PdfFontHandle::Builtin(BuiltinFont::Helvetica),
843            size: Pt(10.0),
844        });
845        ops.push(Op::SetLineHeight { lh: Pt(12.0) });
846
847        let metrics = vec![
848            format!(
849                "Memory Used: {:.2} GB",
850                report.current_metrics.memory_usage as f64 / 1_000_000_000.0
851            ),
852            format!(
853                "Memory Total: {:.2} GB",
854                report.current_metrics.memory_total as f64 / 1_000_000_000.0
855            ),
856            format!("Active Sessions: {}", report.active_profiling_sessions),
857            format!(
858                "Performance Counters: {}",
859                report.performance_counters.len()
860            ),
861        ];
862
863        for item in metrics {
864            ops.push(Op::ShowText {
865                items: vec![TextItem::Text(item)],
866            });
867            ops.push(Op::AddLineBreak);
868        }
869        ops.push(Op::AddLineBreak);
870
871        // Recommendations Section (limited to first 20 to avoid page overflow)
872        if !report.recommendations.is_empty() {
873            ops.push(Op::SetFont {
874                font: PdfFontHandle::Builtin(BuiltinFont::HelveticaBold),
875                size: Pt(16.0),
876            });
877            ops.push(Op::SetLineHeight { lh: Pt(18.0) });
878            ops.push(Op::ShowText {
879                items: vec![TextItem::Text("Recommendations".to_string())],
880            });
881            ops.push(Op::AddLineBreak);
882
883            ops.push(Op::SetFont {
884                font: PdfFontHandle::Builtin(BuiltinFont::Helvetica),
885                size: Pt(10.0),
886            });
887            ops.push(Op::SetLineHeight { lh: Pt(12.0) });
888
889            for rec in report.recommendations.iter().take(20) {
890                let rec_text = if rec.len() > 80 {
891                    format!("• {}...", &rec[..77])
892                } else {
893                    format!("• {}", rec)
894                };
895
896                ops.push(Op::ShowText {
897                    items: vec![TextItem::Text(rec_text)],
898                });
899                ops.push(Op::AddLineBreak);
900            }
901        }
902
903        ops.push(Op::EndTextSection);
904        ops.push(Op::RestoreGraphicsState);
905
906        // Create page
907        let page = PdfPage::new(Mm(210.0), Mm(297.0), ops);
908
909        // Save PDF
910        let output_path = self
911            .output
912            .as_ref()
913            .cloned()
914            .unwrap_or_else(|| PathBuf::from("performance_report.pdf"));
915
916        let pdf_bytes = doc
917            .with_pages(vec![page])
918            .save(&PdfSaveOptions::default(), &mut Vec::new());
919
920        std::fs::write(&output_path, pdf_bytes).map_err(|e| {
921            CliError::io_error(std::io::Error::new(
922                std::io::ErrorKind::Other,
923                format!("Failed to save PDF: {}", e),
924            ))
925        })?;
926
927        info!("PDF report saved to {}", output_path.display());
928        Ok(())
929    }
930
931    async fn generate_markdown_report(
932        &self,
933        report: &crate::tools::performance::PerformanceReport,
934    ) -> Result<(), CliError> {
935        let markdown_content = format!(
936            r#"# OxiRS Performance Report
937
938**Generated**: {}
939
940## System Health
941
942- **Status**: {:?}
943- **CPU Usage**: {:.1}%
944- **Memory Usage**: {:.1}%
945
946## Performance Metrics
947
948- **Memory Used**: {:.2} GB
949- **Memory Total**: {:.2} GB
950- **Active Profiling Sessions**: {}
951
952## Recommendations
953
954{}
955"#,
956            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
957            report.system_health.status,
958            report.system_health.cpu_usage_percentage,
959            report.system_health.memory_usage_percentage,
960            report.current_metrics.memory_usage as f64 / 1_000_000_000.0,
961            report.current_metrics.memory_total as f64 / 1_000_000_000.0,
962            report.active_profiling_sessions,
963            report
964                .recommendations
965                .iter()
966                .map(|r| format!("- {r}"))
967                .collect::<Vec<_>>()
968                .join("\n")
969        );
970
971        let output_path = self
972            .output
973            .as_ref()
974            .cloned()
975            .unwrap_or_else(|| PathBuf::from("performance_report.md"));
976
977        fs::write(&output_path, markdown_content)
978            .await
979            .map_err(|e| {
980                CliError::io_error(std::io::Error::new(
981                    std::io::ErrorKind::Other,
982                    format!("Failed to write Markdown report: {e}"),
983                ))
984            })?;
985
986        info!("Markdown report saved to {}", output_path.display());
987        Ok(())
988    }
989
990    async fn generate_json_report(
991        &self,
992        report: &crate::tools::performance::PerformanceReport,
993    ) -> Result<(), CliError> {
994        let json_content = serde_json::to_string_pretty(report)
995            .map_err(|e| CliError::serialization_error(e.to_string()))?;
996
997        let output_path = self
998            .output
999            .as_ref()
1000            .cloned()
1001            .unwrap_or_else(|| PathBuf::from("performance_report.json"));
1002
1003        fs::write(&output_path, json_content).await.map_err(|e| {
1004            CliError::io_error(std::io::Error::new(
1005                std::io::ErrorKind::Other,
1006                format!("Failed to write JSON report: {e}"),
1007            ))
1008        })?;
1009
1010        info!("JSON report saved to {}", output_path.display());
1011        Ok(())
1012    }
1013}
1014
1015/// Generate a flame graph from profiling results
1016async fn generate_flamegraph(
1017    result: &ProfilingResult,
1018    output_path: &PathBuf,
1019) -> Result<(), CliError> {
1020    use inferno::flamegraph;
1021    use std::io::BufWriter;
1022    use std::str::FromStr;
1023
1024    // Convert profiling result to flame graph format
1025    // Format: <function_stack> <duration_in_ms>
1026    let mut lines = Vec::new();
1027
1028    // Add main operation
1029    let total_ms = result.total_duration.as_millis();
1030    lines.push(format!("{} {}", result.operation_name, total_ms));
1031
1032    // Add checkpoints as stack frames
1033    if !result.checkpoints.is_empty() {
1034        let mut prev_time = std::time::Duration::from_secs(0);
1035
1036        for (idx, checkpoint) in result.checkpoints.iter().enumerate() {
1037            let delta_ms = (checkpoint.duration_from_start - prev_time).as_millis();
1038
1039            if delta_ms > 0 {
1040                // Create stack frame: operation_name;checkpoint_name duration
1041                let stack_frame = format!(
1042                    "{};checkpoint_{:03}_{}",
1043                    result.operation_name,
1044                    idx + 1,
1045                    checkpoint.name.replace(' ', "_")
1046                );
1047                lines.push(format!("{} {}", stack_frame, delta_ms));
1048            }
1049
1050            prev_time = checkpoint.duration_from_start;
1051        }
1052    }
1053
1054    // Add resource usage as stack frames based on start/end metrics
1055    let memory_delta =
1056        result.end_metrics.memory_usage as i64 - result.start_metrics.memory_usage as i64;
1057    if memory_delta.abs() > 1_000_000 {
1058        // > 1MB change
1059        let mem_frame = format!("{};memory_delta", result.operation_name);
1060        let mem_weight = (memory_delta.abs() as f64 / 1_000_000.0 * total_ms as f64) as u128;
1061        if mem_weight > 0 {
1062            lines.push(format!("{} {}", mem_frame, mem_weight.min(total_ms)));
1063        }
1064    }
1065
1066    // Add CPU usage frame
1067    let avg_cpu = result.performance_summary.average_cpu_usage;
1068    if avg_cpu > 1.0 {
1069        let cpu_frame = format!("{};cpu_usage", result.operation_name);
1070        let cpu_weight = (avg_cpu * total_ms as f32 / 100.0) as u128;
1071        if cpu_weight > 0 {
1072            lines.push(format!("{} {}", cpu_frame, cpu_weight));
1073        }
1074    }
1075
1076    // If no detailed data, create a simple single-frame graph
1077    if lines.len() == 1 {
1078        lines.push(format!("{};execution {}", result.operation_name, total_ms));
1079    }
1080
1081    // Sort lines for better visualization
1082    lines.sort();
1083
1084    // Generate flame graph SVG
1085    let input_data = lines.join("\n");
1086    let input_bytes = input_data.as_bytes();
1087
1088    let output_file = std::fs::File::create(output_path).map_err(|e| {
1089        CliError::io_error(std::io::Error::new(
1090            std::io::ErrorKind::Other,
1091            format!("Failed to create flame graph file: {}", e),
1092        ))
1093    })?;
1094
1095    let writer = BufWriter::new(output_file);
1096
1097    let mut options = flamegraph::Options::default();
1098    options.title = format!("Profile: {}", result.operation_name);
1099    options.count_name = "ms".to_string();
1100    options.colors = flamegraph::color::Palette::from_str("hot")
1101        .unwrap_or(flamegraph::color::Palette::default());
1102    options.min_width = 0.1;
1103
1104    flamegraph::from_reader(&mut options, input_bytes, writer).map_err(|e| {
1105        CliError::io_error(std::io::Error::new(
1106            std::io::ErrorKind::Other,
1107            format!("Failed to generate flame graph: {}", e),
1108        ))
1109    })?;
1110
1111    Ok(())
1112}
1113
1114impl OptimizerCommand {
1115    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
1116        info!("Analyzing dataset performance: {}", self.dataset.display());
1117
1118        // Call the performance optimizer
1119        super::performance_optimizer::optimize_dataset_cmd(
1120            self.dataset.to_string_lossy().to_string(),
1121        )
1122        .await
1123        .map_err(|e| {
1124            CliError::new(CliErrorKind::Other(format!(
1125                "Dataset optimization failed: {}",
1126                e
1127            )))
1128        })?;
1129
1130        if self.detailed {
1131            info!("Detailed analysis complete. Check output for recommendations.");
1132        }
1133
1134        Ok(())
1135    }
1136}
1137
1138impl AdvisorCommand {
1139    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
1140        info!("Analyzing SPARQL query for best practices");
1141
1142        // Read query from file if specified
1143        let query = if self.file {
1144            tokio::fs::read_to_string(&self.query).await.map_err(|e| {
1145                CliError::io_error(std::io::Error::new(
1146                    std::io::ErrorKind::NotFound,
1147                    format!("Failed to read query file: {}", e),
1148                ))
1149            })?
1150        } else {
1151            self.query.clone()
1152        };
1153
1154        // Call the query advisor
1155        super::query_advisor::analyze_query_cmd(query, self.verbose)
1156            .await
1157            .map_err(|e| {
1158                CliError::new(CliErrorKind::Other(format!("Query analysis failed: {}", e)))
1159            })?;
1160
1161        if self.verbose {
1162            info!("Detailed query analysis complete with metrics.");
1163        }
1164
1165        Ok(())
1166    }
1167}
1168
1169impl PredictorCommand {
1170    pub async fn execute(&self, _config: &Config) -> Result<(), CliError> {
1171        info!("Predicting SPARQL query performance with ML");
1172
1173        // Read query from file if specified
1174        let query = if self.file {
1175            tokio::fs::read_to_string(&self.query).await.map_err(|e| {
1176                CliError::io_error(std::io::Error::new(
1177                    std::io::ErrorKind::NotFound,
1178                    format!("Failed to read query file: {}", e),
1179                ))
1180            })?
1181        } else {
1182            self.query.clone()
1183        };
1184
1185        // Prepare training data path if provided
1186        let train_data = self.train.as_ref().map(|p| p.to_string_lossy().to_string());
1187
1188        // Call the query predictor
1189        super::query_predictor::predict_query_performance_cmd(query, train_data)
1190            .await
1191            .map_err(|e| {
1192                CliError::new(CliErrorKind::Other(format!(
1193                    "Performance prediction failed: {}",
1194                    e
1195                )))
1196            })?;
1197
1198        if self.detailed {
1199            info!("Detailed performance prediction complete with feature analysis.");
1200        }
1201
1202        Ok(())
1203    }
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208    use super::*;
1209
1210    #[tokio::test]
1211    async fn test_monitor_command_validation() {
1212        let cmd = MonitorCommand {
1213            duration: Some(1),
1214            interval: 100,
1215            format: "json".to_string(),
1216            save: None,
1217            continuous: false,
1218            metrics: None,
1219            cpu_threshold: Some(80.0),
1220            memory_threshold: Some(90.0),
1221        };
1222
1223        // Basic validation test
1224        assert_eq!(cmd.interval, 100);
1225        assert_eq!(cmd.format, "json");
1226    }
1227
1228    #[tokio::test]
1229    async fn test_profile_command_creation() {
1230        let cmd = ProfileCommand {
1231            operation: "test_op".to_string(),
1232            command: vec!["echo".to_string(), "hello".to_string()],
1233            format: "table".to_string(),
1234            save: None,
1235            detailed: true,
1236            metrics: None,
1237            memory: true,
1238            cpu: true,
1239            io: false,
1240            flamegraph: true,
1241            flamegraph_output: Some(std::env::temp_dir().join("oxirs_perf_test_profile.svg")),
1242        };
1243
1244        assert_eq!(cmd.operation, "test_op");
1245        assert!(cmd.detailed);
1246        assert!(cmd.memory);
1247        assert!(cmd.flamegraph);
1248    }
1249
1250    #[test]
1251    fn test_optimizer_command_creation() {
1252        let dataset = std::env::temp_dir().join("oxirs_perf_test_dataset");
1253        let cmd = OptimizerCommand {
1254            dataset: dataset.clone(),
1255            format: "json".to_string(),
1256            save: Some(std::env::temp_dir().join("oxirs_perf_optimization_report.json")),
1257            detailed: true,
1258        };
1259
1260        assert_eq!(cmd.dataset, dataset);
1261        assert_eq!(cmd.format, "json");
1262        assert!(cmd.detailed);
1263        assert!(cmd.save.is_some());
1264    }
1265
1266    #[test]
1267    fn test_advisor_command_creation() {
1268        let cmd = AdvisorCommand {
1269            query: "SELECT * WHERE { ?s ?p ?o }".to_string(),
1270            file: false,
1271            format: "text".to_string(),
1272            verbose: true,
1273            save: None,
1274        };
1275
1276        assert_eq!(cmd.query, "SELECT * WHERE { ?s ?p ?o }");
1277        assert!(!cmd.file);
1278        assert!(cmd.verbose);
1279        assert!(cmd.save.is_none());
1280    }
1281
1282    #[test]
1283    fn test_advisor_command_file_mode() {
1284        let query = std::env::temp_dir()
1285            .join("oxirs_perf_query.sparql")
1286            .to_string_lossy()
1287            .into_owned();
1288        let save = std::env::temp_dir().join("oxirs_perf_analysis.json");
1289        let cmd = AdvisorCommand {
1290            query: query.clone(),
1291            file: true,
1292            format: "json".to_string(),
1293            verbose: false,
1294            save: Some(save.clone()),
1295        };
1296
1297        assert_eq!(cmd.query, query);
1298        assert!(cmd.file);
1299        assert!(!cmd.verbose);
1300        assert_eq!(cmd.save, Some(save));
1301    }
1302
1303    #[test]
1304    fn test_predictor_command_creation() {
1305        let cmd = PredictorCommand {
1306            query: "SELECT ?s WHERE { ?s ?p ?o } LIMIT 100".to_string(),
1307            file: false,
1308            train: None,
1309            format: "text".to_string(),
1310            save: None,
1311            detailed: false,
1312        };
1313
1314        assert_eq!(cmd.query, "SELECT ?s WHERE { ?s ?p ?o } LIMIT 100");
1315        assert!(!cmd.file);
1316        assert!(cmd.train.is_none());
1317        assert!(!cmd.detailed);
1318    }
1319
1320    #[test]
1321    fn test_predictor_command_with_training_data() {
1322        let train = std::env::temp_dir().join("oxirs_perf_training_data.json");
1323        let cmd = PredictorCommand {
1324            query: "SELECT * WHERE { ?s ?p ?o }".to_string(),
1325            file: false,
1326            train: Some(train.clone()),
1327            format: "json".to_string(),
1328            save: Some(std::env::temp_dir().join("oxirs_perf_prediction.json")),
1329            detailed: true,
1330        };
1331
1332        assert!(cmd.train.is_some());
1333        assert_eq!(cmd.train, Some(train));
1334        assert!(cmd.detailed);
1335        assert_eq!(cmd.format, "json");
1336    }
1337
1338    #[test]
1339    fn test_predictor_command_file_mode() {
1340        let query = std::env::temp_dir()
1341            .join("oxirs_perf_complex_query.sparql")
1342            .to_string_lossy()
1343            .into_owned();
1344        let cmd = PredictorCommand {
1345            query: query.clone(),
1346            file: true,
1347            train: Some(std::env::temp_dir().join("oxirs_perf_historical_data.json")),
1348            format: "text".to_string(),
1349            save: Some(std::env::temp_dir().join("oxirs_perf_prediction.txt")),
1350            detailed: true,
1351        };
1352
1353        assert_eq!(cmd.query, query);
1354        assert!(cmd.file);
1355        assert!(cmd.train.is_some());
1356        assert!(cmd.detailed);
1357    }
1358}