1use 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#[derive(Debug, Args)]
22pub struct PerformanceArgs {
23 #[command(subcommand)]
24 pub command: PerformanceCommand,
25}
26
27#[derive(Debug, Subcommand)]
29pub enum PerformanceCommand {
30 Monitor(MonitorCommand),
32 Profile(ProfileCommand),
34 Compare(CompareCommand),
36 Health(HealthCommand),
38 Report(ReportCommand),
40 Optimizer(OptimizerCommand),
42 Advisor(AdvisorCommand),
44 Predictor(PredictorCommand),
46}
47
48#[derive(Debug, Args)]
50pub struct MonitorCommand {
51 #[arg(short, long)]
53 pub duration: Option<u64>,
54
55 #[arg(short = 'I', long, default_value = "1000")]
57 pub interval: u64,
58
59 #[arg(short, long, default_value = "table")]
61 pub format: String,
62
63 #[arg(short = 's', long)]
65 pub save: Option<PathBuf>,
66
67 #[arg(short = 'C', long)]
69 pub continuous: bool,
70
71 #[arg(long)]
73 pub metrics: Option<Vec<String>>,
74
75 #[arg(long)]
77 pub cpu_threshold: Option<f32>,
78
79 #[arg(long)]
81 pub memory_threshold: Option<f32>,
82}
83
84#[derive(Debug, Args)]
86pub struct ProfileCommand {
87 #[arg(short, long)]
89 pub operation: String,
90
91 pub command: Vec<String>,
93
94 #[arg(short, long, default_value = "table")]
96 pub format: String,
97
98 #[arg(short = 's', long)]
100 pub save: Option<PathBuf>,
101
102 #[arg(short, long)]
104 pub detailed: bool,
105
106 #[arg(long)]
108 pub metrics: Option<Vec<String>>,
109
110 #[arg(long)]
112 pub memory: bool,
113
114 #[arg(long)]
116 pub cpu: bool,
117
118 #[arg(long)]
120 pub io: bool,
121
122 #[arg(long)]
124 pub flamegraph: bool,
125
126 #[arg(long)]
128 pub flamegraph_output: Option<PathBuf>,
129}
130
131#[derive(Debug, Args)]
133pub struct CompareCommand {
134 #[arg(short, long)]
136 pub baseline: PathBuf,
137
138 #[arg(short = 'C', long)]
140 pub current: PathBuf,
141
142 #[arg(short, long, default_value = "table")]
144 pub format: String,
145
146 #[arg(short = 's', long)]
148 pub save: Option<PathBuf>,
149
150 #[arg(long, default_value = "5.0")]
152 pub threshold: f64,
153
154 #[arg(short, long)]
156 pub detailed: bool,
157
158 #[arg(long)]
160 pub recommendations: bool,
161}
162
163#[derive(Debug, Args)]
165pub struct HealthCommand {
166 #[arg(short, long, default_value = "table")]
168 pub format: String,
169
170 #[arg(short, long)]
172 pub recommendations: bool,
173
174 #[arg(long)]
176 pub components: Option<Vec<String>>,
177
178 #[arg(long)]
180 pub thresholds: Option<PathBuf>,
181
182 #[arg(short = 'C', long)]
184 pub continuous: bool,
185
186 #[arg(long, default_value = "30")]
188 pub interval: u64,
189}
190
191#[derive(Debug, Args)]
193pub struct ReportCommand {
194 #[arg(short, long, default_value = "24")]
196 pub period: u64,
197
198 #[arg(short, long, default_value = "html")]
200 pub format: String,
201
202 #[arg(short, long)]
204 pub output: Option<PathBuf>,
205
206 #[arg(short, long)]
208 pub graphs: bool,
209
210 #[arg(long)]
212 pub system_metrics: bool,
213
214 #[arg(long)]
216 pub profiling_data: bool,
217
218 #[arg(long)]
220 pub recommendations: bool,
221
222 #[arg(long)]
224 pub template: Option<PathBuf>,
225}
226
227#[derive(Debug, Args)]
229pub struct OptimizerCommand {
230 pub dataset: PathBuf,
232
233 #[arg(short, long, default_value = "text")]
235 pub format: String,
236
237 #[arg(short = 's', long)]
239 pub save: Option<PathBuf>,
240
241 #[arg(short, long)]
243 pub detailed: bool,
244}
245
246#[derive(Debug, Args)]
248pub struct AdvisorCommand {
249 pub query: String,
251
252 #[arg(short, long)]
254 pub file: bool,
255
256 #[arg(short = 'o', long, default_value = "text")]
258 pub format: String,
259
260 #[arg(short = 'V', long)]
262 pub verbose: bool,
263
264 #[arg(short = 's', long)]
266 pub save: Option<PathBuf>,
267}
268
269#[derive(Debug, Args)]
271pub struct PredictorCommand {
272 pub query: String,
274
275 #[arg(short, long)]
277 pub file: bool,
278
279 #[arg(short, long)]
281 pub train: Option<PathBuf>,
282
283 #[arg(short = 'o', long, default_value = "text")]
285 pub format: String,
286
287 #[arg(short = 's', long)]
289 pub save: Option<PathBuf>,
290
291 #[arg(short, long)]
293 pub detailed: bool,
294}
295
296impl PerformanceCommand {
297 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 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 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 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); 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, 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 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 let execution_duration = Duration::from_millis(500); 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 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 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 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 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(¤t_data)
528 .map_err(|e| CliError::serialization_error(format!("Failed to parse current: {e}")))?;
529
530 let config = MonitoringConfig::default();
532 let monitor = PerformanceMonitor::new(config);
533 let comparison = monitor.compare_benchmarks(&baseline, ¤t)?;
534
535 let formatter = OutputFormatter::new(&self.format);
536 formatter.print_benchmark_comparison(&comparison)?;
537
538 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 let mut doc = PdfDocument::new("OxiRS Performance Report");
751
752 let mut ops = vec![Op::SaveGraphicsState, Op::StartTextSection];
754
755 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 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 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 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 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 let page = PdfPage::new(Mm(210.0), Mm(297.0), ops);
908
909 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
1015async 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 let mut lines = Vec::new();
1027
1028 let total_ms = result.total_duration.as_millis();
1030 lines.push(format!("{} {}", result.operation_name, total_ms));
1031
1032 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 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 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 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 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 lines.len() == 1 {
1078 lines.push(format!("{};execution {}", result.operation_name, total_ms));
1079 }
1080
1081 lines.sort();
1083
1084 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 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 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 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 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 let train_data = self.train.as_ref().map(|p| p.to_string_lossy().to_string());
1187
1188 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 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}