kailash 0.9.15__py3-none-any.whl → 0.9.17__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.
- kailash/__init__.py +4 -3
- kailash/middleware/database/base_models.py +7 -1
- kailash/migration/__init__.py +30 -0
- kailash/migration/cli.py +340 -0
- kailash/migration/compatibility_checker.py +662 -0
- kailash/migration/configuration_validator.py +837 -0
- kailash/migration/documentation_generator.py +1828 -0
- kailash/migration/examples/__init__.py +5 -0
- kailash/migration/examples/complete_migration_example.py +692 -0
- kailash/migration/migration_assistant.py +715 -0
- kailash/migration/performance_comparator.py +760 -0
- kailash/migration/regression_detector.py +1141 -0
- kailash/migration/tests/__init__.py +6 -0
- kailash/migration/tests/test_compatibility_checker.py +403 -0
- kailash/migration/tests/test_integration.py +463 -0
- kailash/migration/tests/test_migration_assistant.py +397 -0
- kailash/migration/tests/test_performance_comparator.py +433 -0
- kailash/monitoring/__init__.py +29 -2
- kailash/monitoring/asyncsql_metrics.py +275 -0
- kailash/nodes/data/async_sql.py +1828 -33
- kailash/runtime/local.py +1255 -8
- kailash/runtime/monitoring/__init__.py +1 -0
- kailash/runtime/monitoring/runtime_monitor.py +780 -0
- kailash/runtime/resource_manager.py +3033 -0
- kailash/sdk_exceptions.py +21 -0
- kailash/workflow/cyclic_runner.py +18 -2
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/METADATA +1 -1
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/RECORD +33 -14
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/WHEEL +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/entry_points.txt +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.dist-info}/licenses/NOTICE +0 -0
- {kailash-0.9.15.dist-info → kailash-0.9.17.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)
|