kailash 0.9.15__py3-none-any.whl → 0.9.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. kailash/middleware/database/base_models.py +7 -1
  2. kailash/migration/__init__.py +30 -0
  3. kailash/migration/cli.py +340 -0
  4. kailash/migration/compatibility_checker.py +662 -0
  5. kailash/migration/configuration_validator.py +837 -0
  6. kailash/migration/documentation_generator.py +1828 -0
  7. kailash/migration/examples/__init__.py +5 -0
  8. kailash/migration/examples/complete_migration_example.py +692 -0
  9. kailash/migration/migration_assistant.py +715 -0
  10. kailash/migration/performance_comparator.py +760 -0
  11. kailash/migration/regression_detector.py +1141 -0
  12. kailash/migration/tests/__init__.py +6 -0
  13. kailash/migration/tests/test_compatibility_checker.py +403 -0
  14. kailash/migration/tests/test_integration.py +463 -0
  15. kailash/migration/tests/test_migration_assistant.py +397 -0
  16. kailash/migration/tests/test_performance_comparator.py +433 -0
  17. kailash/nodes/data/async_sql.py +1507 -6
  18. kailash/runtime/local.py +1255 -8
  19. kailash/runtime/monitoring/__init__.py +1 -0
  20. kailash/runtime/monitoring/runtime_monitor.py +780 -0
  21. kailash/runtime/resource_manager.py +3033 -0
  22. kailash/sdk_exceptions.py +21 -0
  23. kailash/workflow/cyclic_runner.py +18 -2
  24. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/METADATA +1 -1
  25. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/RECORD +30 -12
  26. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/WHEEL +0 -0
  27. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/entry_points.txt +0 -0
  28. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/licenses/LICENSE +0 -0
  29. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/licenses/NOTICE +0 -0
  30. {kailash-0.9.15.dist-info → kailash-0.9.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,760 @@
1
+ """Performance comparison tools for LocalRuntime migration analysis.
2
+
3
+ This module provides comprehensive performance analysis capabilities to measure
4
+ and compare runtime performance before and after migration, identifying
5
+ bottlenecks, improvements, and regression risks.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import statistics
11
+ import threading
12
+ import time
13
+ from concurrent.futures import ThreadPoolExecutor
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional, Tuple, Union
18
+
19
+ import psutil
20
+
21
+ from kailash.runtime.local import LocalRuntime
22
+ from kailash.workflow import Workflow
23
+ from kailash.workflow.builder import WorkflowBuilder
24
+
25
+
26
+ @dataclass
27
+ class PerformanceMetric:
28
+ """Individual performance metric measurement."""
29
+
30
+ name: str
31
+ value: float
32
+ unit: str
33
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
34
+ metadata: Dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ @dataclass
38
+ class PerformanceBenchmark:
39
+ """Performance benchmark for a specific test case."""
40
+
41
+ test_name: str
42
+ configuration: Dict[str, Any]
43
+ metrics: List[PerformanceMetric] = field(default_factory=list)
44
+ execution_time_ms: float = 0.0
45
+ memory_usage_mb: float = 0.0
46
+ cpu_usage_percent: float = 0.0
47
+ success: bool = True
48
+ error_message: Optional[str] = None
49
+ run_timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
50
+
51
+
52
+ @dataclass
53
+ class ComparisonResult:
54
+ """Results of performance comparison between two benchmarks."""
55
+
56
+ metric_name: str
57
+ before_value: float
58
+ after_value: float
59
+ change_absolute: float
60
+ change_percentage: float
61
+ improvement: bool
62
+ significance: str # "major", "minor", "negligible", "regression"
63
+ unit: str
64
+
65
+
66
+ @dataclass
67
+ class PerformanceReport:
68
+ """Comprehensive performance comparison report."""
69
+
70
+ before_benchmarks: List[PerformanceBenchmark]
71
+ after_benchmarks: List[PerformanceBenchmark]
72
+ comparisons: List[ComparisonResult] = field(default_factory=list)
73
+ overall_improvement: bool = False
74
+ overall_change_percentage: float = 0.0
75
+ recommendations: List[str] = field(default_factory=list)
76
+ risk_assessment: str = "low" # low, medium, high
77
+ generated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
78
+
79
+
80
+ class PerformanceComparator:
81
+ """Comprehensive performance analysis and comparison tool."""
82
+
83
+ def __init__(
84
+ self, sample_size: int = 10, warmup_runs: int = 2, timeout_seconds: int = 300
85
+ ):
86
+ """Initialize the performance comparator.
87
+
88
+ Args:
89
+ sample_size: Number of measurements per benchmark
90
+ warmup_runs: Number of warmup runs before measurement
91
+ timeout_seconds: Timeout for individual test runs
92
+ """
93
+ self.sample_size = sample_size
94
+ self.warmup_runs = warmup_runs
95
+ self.timeout_seconds = timeout_seconds
96
+
97
+ # Standard test workflows for benchmarking
98
+ self.standard_workflows = self._create_standard_workflows()
99
+
100
+ # Performance thresholds for significance assessment
101
+ self.significance_thresholds = {
102
+ "major_improvement": -20.0, # 20% or more improvement
103
+ "minor_improvement": -5.0, # 5-20% improvement
104
+ "negligible": 5.0, # ±5% change
105
+ "minor_regression": 20.0, # 5-20% regression
106
+ "major_regression": 50.0, # 20%+ regression
107
+ }
108
+
109
+ def benchmark_configuration(
110
+ self,
111
+ config: Dict[str, Any],
112
+ test_workflows: Optional[List[Tuple[str, Workflow]]] = None,
113
+ ) -> List[PerformanceBenchmark]:
114
+ """Benchmark a specific LocalRuntime configuration.
115
+
116
+ Args:
117
+ config: LocalRuntime configuration parameters
118
+ test_workflows: Optional custom workflows to test
119
+
120
+ Returns:
121
+ List of performance benchmarks for each test case
122
+ """
123
+ test_workflows = test_workflows or self.standard_workflows
124
+ benchmarks = []
125
+
126
+ for test_name, workflow in test_workflows:
127
+ benchmark = self._run_benchmark(test_name, config, workflow)
128
+ benchmarks.append(benchmark)
129
+
130
+ return benchmarks
131
+
132
+ def compare_configurations(
133
+ self,
134
+ before_config: Dict[str, Any],
135
+ after_config: Dict[str, Any],
136
+ test_workflows: Optional[List[Tuple[str, Workflow]]] = None,
137
+ ) -> PerformanceReport:
138
+ """Compare performance between two configurations.
139
+
140
+ Args:
141
+ before_config: Original configuration
142
+ after_config: New/migrated configuration
143
+ test_workflows: Optional custom workflows to test
144
+
145
+ Returns:
146
+ Comprehensive performance comparison report
147
+ """
148
+ print("Benchmarking original configuration...")
149
+ before_benchmarks = self.benchmark_configuration(before_config, test_workflows)
150
+
151
+ print("Benchmarking migrated configuration...")
152
+ after_benchmarks = self.benchmark_configuration(after_config, test_workflows)
153
+
154
+ report = PerformanceReport(
155
+ before_benchmarks=before_benchmarks, after_benchmarks=after_benchmarks
156
+ )
157
+
158
+ # Generate comparisons
159
+ self._generate_comparisons(report)
160
+
161
+ # Assess overall performance
162
+ self._assess_overall_performance(report)
163
+
164
+ # Generate recommendations
165
+ self._generate_recommendations(report)
166
+
167
+ return report
168
+
169
+ def _create_standard_workflows(self) -> List[Tuple[str, Workflow]]:
170
+ """Create standard benchmark workflows."""
171
+ workflows = []
172
+
173
+ # Simple linear workflow
174
+ simple_builder = WorkflowBuilder()
175
+ simple_builder.add_node(
176
+ "PythonCodeNode",
177
+ "simple_calc",
178
+ {"code": "result = sum(range(1000))", "output_key": "calculation_result"},
179
+ )
180
+ workflows.append(("simple_linear", simple_builder.build()))
181
+
182
+ # Multiple node workflow
183
+ multi_builder = WorkflowBuilder()
184
+ multi_builder.add_node(
185
+ "PythonCodeNode",
186
+ "step1",
187
+ {
188
+ "code": "import time; result = [i**2 for i in range(100)]",
189
+ "output_key": "squares",
190
+ },
191
+ )
192
+ multi_builder.add_node(
193
+ "PythonCodeNode",
194
+ "step2",
195
+ {
196
+ "code": "result = sum(squares)",
197
+ "input_mapping": {"squares": "step1.squares"},
198
+ "output_key": "sum_squares",
199
+ },
200
+ )
201
+ multi_builder.add_node(
202
+ "PythonCodeNode",
203
+ "step3",
204
+ {
205
+ "code": "result = sum_squares / len(squares)",
206
+ "input_mapping": {
207
+ "sum_squares": "step2.sum_squares",
208
+ "squares": "step1.squares",
209
+ },
210
+ "output_key": "average",
211
+ },
212
+ )
213
+ workflows.append(("multi_node", multi_builder.build()))
214
+
215
+ # Memory intensive workflow
216
+ memory_builder = WorkflowBuilder()
217
+ memory_builder.add_node(
218
+ "PythonCodeNode",
219
+ "memory_test",
220
+ {
221
+ "code": """
222
+ import gc
223
+ # Create large data structure
224
+ large_list = [list(range(1000)) for _ in range(100)]
225
+ result = len(large_list)
226
+ del large_list
227
+ gc.collect()
228
+ """,
229
+ "output_key": "memory_result",
230
+ },
231
+ )
232
+ workflows.append(("memory_intensive", memory_builder.build()))
233
+
234
+ # Error handling workflow
235
+ error_builder = WorkflowBuilder()
236
+ error_builder.add_node(
237
+ "PythonCodeNode",
238
+ "error_test",
239
+ {
240
+ "code": """
241
+ try:
242
+ # Intentional error that gets caught
243
+ x = 1 / 0
244
+ except:
245
+ result = "error_handled"
246
+ """,
247
+ "output_key": "error_result",
248
+ },
249
+ )
250
+ workflows.append(("error_handling", error_builder.build()))
251
+
252
+ return workflows
253
+
254
+ def _run_benchmark(
255
+ self, test_name: str, config: Dict[str, Any], workflow: Workflow
256
+ ) -> PerformanceBenchmark:
257
+ """Run benchmark for a specific test case."""
258
+ benchmark = PerformanceBenchmark(
259
+ test_name=test_name, configuration=config.copy()
260
+ )
261
+
262
+ try:
263
+ # Create runtime with configuration
264
+ runtime = LocalRuntime(**config)
265
+
266
+ # Warmup runs
267
+ for _ in range(self.warmup_runs):
268
+ try:
269
+ runtime.execute(workflow)
270
+ except Exception:
271
+ pass # Ignore warmup errors
272
+
273
+ # Measurement runs
274
+ execution_times = []
275
+ memory_usages = []
276
+ cpu_usages = []
277
+
278
+ for run in range(self.sample_size):
279
+ # Measure system resources before
280
+ process = psutil.Process()
281
+ memory_before = process.memory_info().rss / 1024 / 1024 # MB
282
+ cpu_before = process.cpu_percent()
283
+
284
+ # Execute workflow with timing
285
+ start_time = time.perf_counter()
286
+ try:
287
+ results, run_id = runtime.execute(workflow)
288
+ success = True
289
+ error_msg = None
290
+ except Exception as e:
291
+ success = False
292
+ error_msg = str(e)
293
+ results = None
294
+
295
+ end_time = time.perf_counter()
296
+ execution_time = (end_time - start_time) * 1000 # Convert to ms
297
+
298
+ # Measure system resources after
299
+ memory_after = process.memory_info().rss / 1024 / 1024 # MB
300
+ cpu_after = process.cpu_percent()
301
+
302
+ # Record measurements
303
+ if success:
304
+ execution_times.append(execution_time)
305
+ memory_usages.append(memory_after - memory_before)
306
+ cpu_usages.append(max(0, cpu_after - cpu_before))
307
+ else:
308
+ benchmark.success = False
309
+ benchmark.error_message = error_msg
310
+ break
311
+
312
+ # Calculate statistics
313
+ if execution_times:
314
+ benchmark.execution_time_ms = statistics.mean(execution_times)
315
+ benchmark.memory_usage_mb = statistics.mean(memory_usages)
316
+ benchmark.cpu_usage_percent = statistics.mean(cpu_usages)
317
+
318
+ # Add detailed metrics
319
+ benchmark.metrics.extend(
320
+ [
321
+ PerformanceMetric(
322
+ "execution_time_mean", benchmark.execution_time_ms, "ms"
323
+ ),
324
+ PerformanceMetric(
325
+ "execution_time_median",
326
+ statistics.median(execution_times),
327
+ "ms",
328
+ ),
329
+ PerformanceMetric(
330
+ "execution_time_stddev",
331
+ (
332
+ statistics.stdev(execution_times)
333
+ if len(execution_times) > 1
334
+ else 0.0
335
+ ),
336
+ "ms",
337
+ ),
338
+ PerformanceMetric(
339
+ "memory_usage_mean", benchmark.memory_usage_mb, "mb"
340
+ ),
341
+ PerformanceMetric(
342
+ "cpu_usage_mean", benchmark.cpu_usage_percent, "percent"
343
+ ),
344
+ ]
345
+ )
346
+
347
+ except Exception as e:
348
+ benchmark.success = False
349
+ benchmark.error_message = str(e)
350
+
351
+ return benchmark
352
+
353
+ def _generate_comparisons(self, report: PerformanceReport) -> None:
354
+ """Generate performance comparisons between before and after."""
355
+ # Group benchmarks by test name
356
+ before_by_test = {b.test_name: b for b in report.before_benchmarks}
357
+ after_by_test = {b.test_name: b for b in report.after_benchmarks}
358
+
359
+ # Compare matching tests
360
+ for test_name in before_by_test.keys():
361
+ if test_name in after_by_test:
362
+ before_bench = before_by_test[test_name]
363
+ after_bench = after_by_test[test_name]
364
+
365
+ # Skip failed benchmarks
366
+ if not before_bench.success or not after_bench.success:
367
+ continue
368
+
369
+ # Compare execution time
370
+ report.comparisons.append(
371
+ self._create_comparison(
372
+ "execution_time",
373
+ before_bench.execution_time_ms,
374
+ after_bench.execution_time_ms,
375
+ "ms",
376
+ )
377
+ )
378
+
379
+ # Compare memory usage
380
+ report.comparisons.append(
381
+ self._create_comparison(
382
+ "memory_usage",
383
+ before_bench.memory_usage_mb,
384
+ after_bench.memory_usage_mb,
385
+ "mb",
386
+ )
387
+ )
388
+
389
+ # Compare CPU usage
390
+ report.comparisons.append(
391
+ self._create_comparison(
392
+ "cpu_usage",
393
+ before_bench.cpu_usage_percent,
394
+ after_bench.cpu_usage_percent,
395
+ "percent",
396
+ )
397
+ )
398
+
399
+ def _create_comparison(
400
+ self, metric_name: str, before_value: float, after_value: float, unit: str
401
+ ) -> ComparisonResult:
402
+ """Create a performance comparison result."""
403
+ change_absolute = after_value - before_value
404
+ change_percentage = (
405
+ ((after_value - before_value) / before_value * 100)
406
+ if before_value != 0
407
+ else 0.0
408
+ )
409
+
410
+ # Determine if this is an improvement (lower is better for time and resource usage)
411
+ improvement = change_percentage < 0
412
+
413
+ # Assess significance
414
+ significance = self._assess_significance(change_percentage)
415
+
416
+ return ComparisonResult(
417
+ metric_name=metric_name,
418
+ before_value=before_value,
419
+ after_value=after_value,
420
+ change_absolute=change_absolute,
421
+ change_percentage=change_percentage,
422
+ improvement=improvement,
423
+ significance=significance,
424
+ unit=unit,
425
+ )
426
+
427
+ def _assess_significance(self, change_percentage: float) -> str:
428
+ """Assess the significance of a performance change."""
429
+ if change_percentage <= self.significance_thresholds["major_improvement"]:
430
+ return "major_improvement"
431
+ elif change_percentage <= self.significance_thresholds["minor_improvement"]:
432
+ return "minor_improvement"
433
+ elif abs(change_percentage) <= self.significance_thresholds["negligible"]:
434
+ return "negligible"
435
+ elif change_percentage <= self.significance_thresholds["minor_regression"]:
436
+ return "minor_regression"
437
+ else:
438
+ return "major_regression"
439
+
440
+ def _assess_overall_performance(self, report: PerformanceReport) -> None:
441
+ """Assess overall performance change."""
442
+ if not report.comparisons:
443
+ return
444
+
445
+ # Calculate weighted overall change (execution time has highest weight)
446
+ weights = {"execution_time": 0.5, "memory_usage": 0.3, "cpu_usage": 0.2}
447
+
448
+ weighted_changes = []
449
+ for comparison in report.comparisons:
450
+ weight = weights.get(comparison.metric_name, 0.1)
451
+ weighted_changes.append(comparison.change_percentage * weight)
452
+
453
+ if weighted_changes:
454
+ report.overall_change_percentage = sum(weighted_changes) / sum(
455
+ weights.values()
456
+ )
457
+ report.overall_improvement = report.overall_change_percentage < 0
458
+
459
+ # Assess risk level
460
+ major_regressions = [
461
+ c for c in report.comparisons if c.significance == "major_regression"
462
+ ]
463
+ minor_regressions = [
464
+ c for c in report.comparisons if c.significance == "minor_regression"
465
+ ]
466
+
467
+ if major_regressions:
468
+ report.risk_assessment = "high"
469
+ elif len(minor_regressions) > 1:
470
+ report.risk_assessment = "medium"
471
+ else:
472
+ report.risk_assessment = "low"
473
+
474
+ def _generate_recommendations(self, report: PerformanceReport) -> None:
475
+ """Generate performance recommendations based on comparison results."""
476
+ recommendations = []
477
+
478
+ # Analyze execution time changes
479
+ exec_time_comparisons = [
480
+ c for c in report.comparisons if c.metric_name == "execution_time"
481
+ ]
482
+ if exec_time_comparisons:
483
+ avg_exec_change = sum(
484
+ c.change_percentage for c in exec_time_comparisons
485
+ ) / len(exec_time_comparisons)
486
+
487
+ if avg_exec_change > 10: # More than 10% slower
488
+ recommendations.append(
489
+ "Execution time has regressed significantly. Consider reviewing workflow complexity "
490
+ "and optimizing node configurations."
491
+ )
492
+ elif avg_exec_change < -10: # More than 10% faster
493
+ recommendations.append(
494
+ "Excellent execution time improvements detected. Migration benefits are clear."
495
+ )
496
+
497
+ # Analyze memory usage changes
498
+ memory_comparisons = [
499
+ c for c in report.comparisons if c.metric_name == "memory_usage"
500
+ ]
501
+ if memory_comparisons:
502
+ avg_memory_change = sum(
503
+ c.change_percentage for c in memory_comparisons
504
+ ) / len(memory_comparisons)
505
+
506
+ if avg_memory_change > 25: # More than 25% memory increase
507
+ recommendations.append(
508
+ "Memory usage has increased significantly. Consider enabling connection pooling "
509
+ "or reviewing resource_limits configuration."
510
+ )
511
+ elif avg_memory_change < -15: # More than 15% memory reduction
512
+ recommendations.append(
513
+ "Memory efficiency improvements detected. Enhanced LocalRuntime is optimizing resource usage."
514
+ )
515
+
516
+ # General recommendations based on risk assessment
517
+ if report.risk_assessment == "high":
518
+ recommendations.extend(
519
+ [
520
+ "High performance risk detected. Consider gradual migration or additional optimization.",
521
+ "Review enterprise features that might affect performance in your specific use case.",
522
+ "Consider running extended performance tests with production-like workloads.",
523
+ ]
524
+ )
525
+ elif report.risk_assessment == "medium":
526
+ recommendations.extend(
527
+ [
528
+ "Medium performance risk. Monitor performance closely after migration.",
529
+ "Consider performance profiling of specific workflows that showed regression.",
530
+ ]
531
+ )
532
+ else:
533
+ recommendations.append(
534
+ "Low performance risk. Migration appears safe from performance perspective."
535
+ )
536
+
537
+ # Configuration-specific recommendations
538
+ failed_benchmarks = [b for b in report.after_benchmarks if not b.success]
539
+ if failed_benchmarks:
540
+ recommendations.append(
541
+ f"Some benchmarks failed ({len(failed_benchmarks)} out of {len(report.after_benchmarks)}). "
542
+ "Review configuration parameters and error messages."
543
+ )
544
+
545
+ # Enterprise feature recommendations
546
+ if report.overall_improvement:
547
+ recommendations.append(
548
+ "Performance improvements suggest enhanced LocalRuntime is well-suited for your workloads. "
549
+ "Consider enabling additional enterprise features for further optimization."
550
+ )
551
+
552
+ report.recommendations = recommendations
553
+
554
+ def generate_performance_report(
555
+ self, report: PerformanceReport, output_format: str = "text"
556
+ ) -> str:
557
+ """Generate a comprehensive performance report.
558
+
559
+ Args:
560
+ report: Performance comparison report
561
+ output_format: Report format ("text", "json", "markdown")
562
+
563
+ Returns:
564
+ Formatted report string
565
+ """
566
+ if output_format == "json":
567
+ return self._generate_json_report(report)
568
+ elif output_format == "markdown":
569
+ return self._generate_markdown_report(report)
570
+ else:
571
+ return self._generate_text_report(report)
572
+
573
+ def _generate_text_report(self, report: PerformanceReport) -> str:
574
+ """Generate text format performance report."""
575
+ lines = []
576
+ lines.append("=" * 60)
577
+ lines.append("LocalRuntime Performance Comparison Report")
578
+ lines.append("=" * 60)
579
+ lines.append("")
580
+
581
+ # Executive summary
582
+ lines.append("EXECUTIVE SUMMARY")
583
+ lines.append("-" * 20)
584
+ lines.append(
585
+ f"Overall Performance Change: {report.overall_change_percentage:+.1f}%"
586
+ )
587
+ lines.append(
588
+ f"Overall Assessment: {'IMPROVEMENT' if report.overall_improvement else 'REGRESSION'}"
589
+ )
590
+ lines.append(f"Risk Level: {report.risk_assessment.upper()}")
591
+ lines.append(f"Tests Completed: {len(report.after_benchmarks)} benchmarks")
592
+ lines.append("")
593
+
594
+ # Detailed comparisons
595
+ lines.append("PERFORMANCE COMPARISONS")
596
+ lines.append("-" * 25)
597
+ lines.append(
598
+ f"{'Metric':<15} {'Before':<12} {'After':<12} {'Change':<12} {'Status'}"
599
+ )
600
+ lines.append("-" * 65)
601
+
602
+ for comparison in report.comparisons:
603
+ status = "↑ BETTER" if comparison.improvement else "↓ WORSE"
604
+ if comparison.significance == "negligible":
605
+ status = "→ SAME"
606
+
607
+ lines.append(
608
+ f"{comparison.metric_name:<15} "
609
+ f"{comparison.before_value:<12.2f} "
610
+ f"{comparison.after_value:<12.2f} "
611
+ f"{comparison.change_percentage:+7.1f}% "
612
+ f"{status}"
613
+ )
614
+
615
+ lines.append("")
616
+
617
+ # Recommendations
618
+ if report.recommendations:
619
+ lines.append("RECOMMENDATIONS")
620
+ lines.append("-" * 18)
621
+ for i, rec in enumerate(report.recommendations, 1):
622
+ lines.append(f"{i}. {rec}")
623
+ lines.append("")
624
+
625
+ # Benchmark details
626
+ lines.append("BENCHMARK DETAILS")
627
+ lines.append("-" * 20)
628
+
629
+ for benchmark in report.after_benchmarks:
630
+ lines.append(f"Test: {benchmark.test_name}")
631
+ lines.append(f" Success: {'Yes' if benchmark.success else 'No'}")
632
+ if benchmark.success:
633
+ lines.append(f" Execution Time: {benchmark.execution_time_ms:.2f} ms")
634
+ lines.append(f" Memory Usage: {benchmark.memory_usage_mb:.2f} MB")
635
+ lines.append(f" CPU Usage: {benchmark.cpu_usage_percent:.1f}%")
636
+ else:
637
+ lines.append(f" Error: {benchmark.error_message}")
638
+ lines.append("")
639
+
640
+ return "\n".join(lines)
641
+
642
+ def _generate_json_report(self, report: PerformanceReport) -> str:
643
+ """Generate JSON format performance report."""
644
+ data = {
645
+ "summary": {
646
+ "overall_change_percentage": report.overall_change_percentage,
647
+ "overall_improvement": report.overall_improvement,
648
+ "risk_assessment": report.risk_assessment,
649
+ "generated_at": report.generated_at.isoformat(),
650
+ },
651
+ "comparisons": [
652
+ {
653
+ "metric": c.metric_name,
654
+ "before_value": c.before_value,
655
+ "after_value": c.after_value,
656
+ "change_percentage": c.change_percentage,
657
+ "improvement": c.improvement,
658
+ "significance": c.significance,
659
+ "unit": c.unit,
660
+ }
661
+ for c in report.comparisons
662
+ ],
663
+ "recommendations": report.recommendations,
664
+ "benchmarks": {
665
+ "before": [
666
+ {
667
+ "test_name": b.test_name,
668
+ "success": b.success,
669
+ "execution_time_ms": b.execution_time_ms,
670
+ "memory_usage_mb": b.memory_usage_mb,
671
+ "cpu_usage_percent": b.cpu_usage_percent,
672
+ "error_message": b.error_message,
673
+ }
674
+ for b in report.before_benchmarks
675
+ ],
676
+ "after": [
677
+ {
678
+ "test_name": b.test_name,
679
+ "success": b.success,
680
+ "execution_time_ms": b.execution_time_ms,
681
+ "memory_usage_mb": b.memory_usage_mb,
682
+ "cpu_usage_percent": b.cpu_usage_percent,
683
+ "error_message": b.error_message,
684
+ }
685
+ for b in report.after_benchmarks
686
+ ],
687
+ },
688
+ }
689
+
690
+ return json.dumps(data, indent=2)
691
+
692
+ def _generate_markdown_report(self, report: PerformanceReport) -> str:
693
+ """Generate markdown format performance report."""
694
+ lines = []
695
+ lines.append("# LocalRuntime Performance Comparison Report")
696
+ lines.append("")
697
+
698
+ # Summary table
699
+ lines.append("## Executive Summary")
700
+ lines.append("")
701
+ lines.append("| Metric | Value |")
702
+ lines.append("|--------|-------|")
703
+ lines.append(
704
+ f"| Overall Performance Change | {report.overall_change_percentage:+.1f}% |"
705
+ )
706
+ lines.append(
707
+ f"| Assessment | {'Improvement' if report.overall_improvement else 'Regression'} |"
708
+ )
709
+ lines.append(f"| Risk Level | {report.risk_assessment.title()} |")
710
+ lines.append(f"| Tests Completed | {len(report.after_benchmarks)} |")
711
+ lines.append("")
712
+
713
+ # Performance comparisons
714
+ lines.append("## Performance Comparisons")
715
+ lines.append("")
716
+ lines.append("| Metric | Before | After | Change | Status |")
717
+ lines.append("|--------|---------|--------|---------|---------|")
718
+
719
+ for comparison in report.comparisons:
720
+ status = "✅ Better" if comparison.improvement else "❌ Worse"
721
+ if comparison.significance == "negligible":
722
+ status = "➡️ Same"
723
+
724
+ lines.append(
725
+ f"| {comparison.metric_name} | "
726
+ f"{comparison.before_value:.2f} {comparison.unit} | "
727
+ f"{comparison.after_value:.2f} {comparison.unit} | "
728
+ f"{comparison.change_percentage:+.1f}% | "
729
+ f"{status} |"
730
+ )
731
+
732
+ lines.append("")
733
+
734
+ # Recommendations
735
+ if report.recommendations:
736
+ lines.append("## Recommendations")
737
+ lines.append("")
738
+ for i, rec in enumerate(report.recommendations, 1):
739
+ lines.append(f"{i}. {rec}")
740
+ lines.append("")
741
+
742
+ return "\n".join(lines)
743
+
744
+ def save_report(
745
+ self,
746
+ report: PerformanceReport,
747
+ file_path: Union[str, Path],
748
+ format: str = "json",
749
+ ) -> None:
750
+ """Save performance report to file.
751
+
752
+ Args:
753
+ report: Performance report to save
754
+ file_path: Output file path
755
+ format: Report format ("text", "json", "markdown")
756
+ """
757
+ content = self.generate_performance_report(report, format)
758
+
759
+ with open(file_path, "w", encoding="utf-8") as f:
760
+ f.write(content)