crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -253
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +670 -0
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +577 -0
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config-ai.yaml +0 -149
- crackerjack/.pre-commit-config-fast.yaml +0 -69
- crackerjack/.pre-commit-config.yaml +0 -114
- crackerjack/crackerjack.py +0 -4140
- crackerjack/pyproject.toml +0 -285
- crackerjack-0.29.0.dist-info/METADATA +0 -1289
- crackerjack-0.29.0.dist-info/RECORD +0 -17
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import statistics
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
import typing as t
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from crackerjack.models.protocols import FileSystemInterface
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BenchmarkResult:
|
|
19
|
+
name: str
|
|
20
|
+
duration_seconds: float
|
|
21
|
+
memory_usage_mb: float = 0.0
|
|
22
|
+
cpu_percent: float = 0.0
|
|
23
|
+
iterations: int = 1
|
|
24
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PerformanceReport:
|
|
29
|
+
total_duration: float
|
|
30
|
+
workflow_benchmarks: list[BenchmarkResult] = field(default_factory=list)
|
|
31
|
+
test_benchmarks: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
hook_performance: dict[str, float] = field(default_factory=dict)
|
|
33
|
+
file_operation_stats: dict[str, float] = field(default_factory=dict)
|
|
34
|
+
recommendations: list[str] = field(default_factory=list)
|
|
35
|
+
baseline_comparison: dict[str, float] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PerformanceBenchmarkService:
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
filesystem: FileSystemInterface,
|
|
42
|
+
console: Console | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.filesystem = filesystem
|
|
45
|
+
self.console = console or Console()
|
|
46
|
+
self.project_root = Path.cwd()
|
|
47
|
+
self.benchmarks_dir = self.project_root / ".benchmarks"
|
|
48
|
+
self.history_file = self.benchmarks_dir / "performance_history.json"
|
|
49
|
+
|
|
50
|
+
self.benchmarks_dir.mkdir(exist_ok=True)
|
|
51
|
+
|
|
52
|
+
def run_comprehensive_benchmark(
|
|
53
|
+
self,
|
|
54
|
+
run_tests: bool = True,
|
|
55
|
+
run_hooks: bool = True,
|
|
56
|
+
iterations: int = 1,
|
|
57
|
+
) -> PerformanceReport:
|
|
58
|
+
"""Run comprehensive performance benchmark across all components."""
|
|
59
|
+
self.console.print(
|
|
60
|
+
"[cyan]🚀 Starting comprehensive performance benchmark...[/cyan]",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
start_time = time.time()
|
|
64
|
+
report = self._initialize_performance_report()
|
|
65
|
+
|
|
66
|
+
self._run_requested_benchmarks(report, run_tests, run_hooks, iterations)
|
|
67
|
+
self._finalize_performance_report(report, start_time)
|
|
68
|
+
|
|
69
|
+
return report
|
|
70
|
+
|
|
71
|
+
def _initialize_performance_report(self) -> PerformanceReport:
|
|
72
|
+
"""Initialize a new performance report."""
|
|
73
|
+
return PerformanceReport(total_duration=0.0)
|
|
74
|
+
|
|
75
|
+
def _run_requested_benchmarks(
|
|
76
|
+
self,
|
|
77
|
+
report: PerformanceReport,
|
|
78
|
+
run_tests: bool,
|
|
79
|
+
run_hooks: bool,
|
|
80
|
+
iterations: int,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Run the requested benchmark types."""
|
|
83
|
+
if run_tests:
|
|
84
|
+
report.test_benchmarks = self._benchmark_test_suite(iterations)
|
|
85
|
+
|
|
86
|
+
if run_hooks:
|
|
87
|
+
report.hook_performance = self._benchmark_hooks(iterations)
|
|
88
|
+
|
|
89
|
+
report.workflow_benchmarks = self._benchmark_workflow_components(iterations)
|
|
90
|
+
report.file_operation_stats = self._benchmark_file_operations()
|
|
91
|
+
|
|
92
|
+
def _finalize_performance_report(
|
|
93
|
+
self,
|
|
94
|
+
report: PerformanceReport,
|
|
95
|
+
start_time: float,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Finalize performance report with analysis and history."""
|
|
98
|
+
report.total_duration = time.time() - start_time
|
|
99
|
+
report.recommendations = self._generate_performance_recommendations(report)
|
|
100
|
+
report.baseline_comparison = self._compare_with_baseline(report)
|
|
101
|
+
self._save_performance_history(report)
|
|
102
|
+
|
|
103
|
+
def _benchmark_test_suite(self, iterations: int = 1) -> dict[str, Any]:
|
|
104
|
+
self.console.print("[dim]📊 Benchmarking test suite...[/dim]")
|
|
105
|
+
|
|
106
|
+
benchmark_results = {}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
for i in range(iterations):
|
|
110
|
+
start_time = time.time()
|
|
111
|
+
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
[
|
|
114
|
+
"uv",
|
|
115
|
+
"run",
|
|
116
|
+
"pytest",
|
|
117
|
+
"--benchmark-only",
|
|
118
|
+
"--benchmark-json=.benchmarks/test_benchmark.json",
|
|
119
|
+
"--tb=no",
|
|
120
|
+
"-q",
|
|
121
|
+
],
|
|
122
|
+
check=False,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
timeout=300,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
duration = time.time() - start_time
|
|
129
|
+
|
|
130
|
+
benchmark_file = self.benchmarks_dir / "test_benchmark.json"
|
|
131
|
+
if benchmark_file.exists():
|
|
132
|
+
with benchmark_file.open() as f:
|
|
133
|
+
benchmark_data = json.load(f)
|
|
134
|
+
|
|
135
|
+
benchmark_results[f"iteration_{i + 1}"] = {
|
|
136
|
+
"total_duration": duration,
|
|
137
|
+
"benchmark_data": benchmark_data,
|
|
138
|
+
"success": result.returncode == 0,
|
|
139
|
+
}
|
|
140
|
+
else:
|
|
141
|
+
benchmark_results[f"iteration_{i + 1}"] = {
|
|
142
|
+
"total_duration": duration,
|
|
143
|
+
"success": result.returncode == 0,
|
|
144
|
+
"note": "No benchmark tests found",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
except subprocess.TimeoutExpired:
|
|
148
|
+
benchmark_results["error"] = "Test benchmarking timed out"
|
|
149
|
+
except Exception as e:
|
|
150
|
+
benchmark_results["error"] = f"Test benchmarking failed: {e}"
|
|
151
|
+
|
|
152
|
+
return benchmark_results
|
|
153
|
+
|
|
154
|
+
def _benchmark_hooks(self, iterations: int = 1) -> dict[str, float]:
|
|
155
|
+
self.console.print("[dim]🔧 Benchmarking hooks performance...[/dim]")
|
|
156
|
+
|
|
157
|
+
hook_performance = {}
|
|
158
|
+
|
|
159
|
+
hooks_to_test = [
|
|
160
|
+
"trailing-whitespace",
|
|
161
|
+
"end-of-file-fixer",
|
|
162
|
+
"ruff-format",
|
|
163
|
+
"ruff-check",
|
|
164
|
+
"gitleaks",
|
|
165
|
+
"pyright",
|
|
166
|
+
"bandit",
|
|
167
|
+
"vulture",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
for hook_name in hooks_to_test:
|
|
171
|
+
durations: list[float] = []
|
|
172
|
+
|
|
173
|
+
for _i in range(iterations):
|
|
174
|
+
try:
|
|
175
|
+
start_time = time.time()
|
|
176
|
+
subprocess.run(
|
|
177
|
+
[
|
|
178
|
+
"uv",
|
|
179
|
+
"run",
|
|
180
|
+
"pre-commit",
|
|
181
|
+
"run",
|
|
182
|
+
hook_name,
|
|
183
|
+
"--all-files",
|
|
184
|
+
],
|
|
185
|
+
check=False,
|
|
186
|
+
capture_output=True,
|
|
187
|
+
text=True,
|
|
188
|
+
timeout=120,
|
|
189
|
+
)
|
|
190
|
+
duration = time.time() - start_time
|
|
191
|
+
durations.append(duration)
|
|
192
|
+
except subprocess.TimeoutExpired:
|
|
193
|
+
durations.append(120.0)
|
|
194
|
+
except Exception:
|
|
195
|
+
durations.append(float("inf"))
|
|
196
|
+
|
|
197
|
+
if durations and all(d != float("inf") for d in durations):
|
|
198
|
+
hook_performance[hook_name] = {
|
|
199
|
+
"mean_duration": statistics.mean(durations),
|
|
200
|
+
"min_duration": min(durations),
|
|
201
|
+
"max_duration": max(durations),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return hook_performance
|
|
205
|
+
|
|
206
|
+
def _benchmark_workflow_components(
|
|
207
|
+
self,
|
|
208
|
+
iterations: int = 1,
|
|
209
|
+
) -> list[BenchmarkResult]:
|
|
210
|
+
self.console.print("[dim]⚙️ Benchmarking workflow components...[/dim]")
|
|
211
|
+
|
|
212
|
+
results = []
|
|
213
|
+
|
|
214
|
+
start_time = time.time()
|
|
215
|
+
python_files = list(self.project_root.rglob("*.py"))
|
|
216
|
+
file_discovery_duration = time.time() - start_time
|
|
217
|
+
|
|
218
|
+
results.append(
|
|
219
|
+
BenchmarkResult(
|
|
220
|
+
name="file_discovery",
|
|
221
|
+
duration_seconds=file_discovery_duration,
|
|
222
|
+
metadata={"files_found": len(python_files)},
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
start_time = time.time()
|
|
227
|
+
pyproject_path = self.project_root / "pyproject.toml"
|
|
228
|
+
if pyproject_path.exists():
|
|
229
|
+
with suppress(Exception):
|
|
230
|
+
import tomllib
|
|
231
|
+
|
|
232
|
+
with pyproject_path.open("rb") as f:
|
|
233
|
+
tomllib.load(f)
|
|
234
|
+
config_load_duration = time.time() - start_time
|
|
235
|
+
|
|
236
|
+
results.append(
|
|
237
|
+
BenchmarkResult(
|
|
238
|
+
name="config_loading",
|
|
239
|
+
duration_seconds=config_load_duration,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return results
|
|
244
|
+
|
|
245
|
+
def _benchmark_file_operations(self) -> dict[str, float]:
|
|
246
|
+
stats = {}
|
|
247
|
+
|
|
248
|
+
test_files = list(self.project_root.glob("*.py"))[:10]
|
|
249
|
+
if test_files:
|
|
250
|
+
start_time = time.time()
|
|
251
|
+
for file_path in test_files:
|
|
252
|
+
with suppress(Exception):
|
|
253
|
+
file_path.read_text(encoding="utf-8")
|
|
254
|
+
read_duration = time.time() - start_time
|
|
255
|
+
stats["file_read_ops"] = read_duration / len(test_files)
|
|
256
|
+
|
|
257
|
+
return stats
|
|
258
|
+
|
|
259
|
+
def _generate_performance_recommendations(
|
|
260
|
+
self,
|
|
261
|
+
report: PerformanceReport,
|
|
262
|
+
) -> list[str]:
|
|
263
|
+
"""Generate performance recommendations based on benchmark results."""
|
|
264
|
+
recommendations = []
|
|
265
|
+
|
|
266
|
+
self._add_test_suite_recommendations(report, recommendations)
|
|
267
|
+
self._add_hook_performance_recommendations(report, recommendations)
|
|
268
|
+
self._add_component_performance_recommendations(report, recommendations)
|
|
269
|
+
self._add_overall_performance_recommendations(report, recommendations)
|
|
270
|
+
|
|
271
|
+
return recommendations
|
|
272
|
+
|
|
273
|
+
def _add_test_suite_recommendations(
|
|
274
|
+
self,
|
|
275
|
+
report: PerformanceReport,
|
|
276
|
+
recommendations: list[str],
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Add recommendations for test suite performance."""
|
|
279
|
+
if not report.test_benchmarks:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
for iteration_data in report.test_benchmarks.values():
|
|
283
|
+
if self._is_slow_test_iteration(iteration_data):
|
|
284
|
+
recommendations.append(
|
|
285
|
+
"Consider optimizing test suite - execution time exceeds 1 minute",
|
|
286
|
+
)
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
def _is_slow_test_iteration(self, iteration_data: Any) -> bool:
|
|
290
|
+
"""Check if test iteration is slow."""
|
|
291
|
+
return (
|
|
292
|
+
isinstance(iteration_data, dict)
|
|
293
|
+
and iteration_data.get("total_duration", 0) > 60
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _add_hook_performance_recommendations(
|
|
297
|
+
self,
|
|
298
|
+
report: PerformanceReport,
|
|
299
|
+
recommendations: list[str],
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Add recommendations for hook performance."""
|
|
302
|
+
slow_hooks = self._identify_slow_hooks(report.hook_performance)
|
|
303
|
+
if slow_hooks:
|
|
304
|
+
recommendations.append(self._format_slow_hooks_message(slow_hooks))
|
|
305
|
+
|
|
306
|
+
def _identify_slow_hooks(
|
|
307
|
+
self,
|
|
308
|
+
hook_performance: dict[str, float],
|
|
309
|
+
) -> list[tuple[str, float]]:
|
|
310
|
+
"""Identify hooks with slow performance."""
|
|
311
|
+
slow_hooks = []
|
|
312
|
+
for hook_name, perf_data in hook_performance.items():
|
|
313
|
+
if isinstance(perf_data, dict):
|
|
314
|
+
mean_duration = perf_data.get("mean_duration", 0)
|
|
315
|
+
if mean_duration > 30:
|
|
316
|
+
slow_hooks.append((hook_name, mean_duration))
|
|
317
|
+
return slow_hooks
|
|
318
|
+
|
|
319
|
+
def _format_slow_hooks_message(self, slow_hooks: list[tuple[str, float]]) -> str:
|
|
320
|
+
"""Format message for slow hooks recommendation."""
|
|
321
|
+
hooks_info = ", ".join(f"{h}({d:.1f}s)" for h, d in slow_hooks[:3])
|
|
322
|
+
return (
|
|
323
|
+
f"Slow hooks detected: {hooks_info}. "
|
|
324
|
+
"Consider hook optimization or selective execution."
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _add_component_performance_recommendations(
|
|
328
|
+
self,
|
|
329
|
+
report: PerformanceReport,
|
|
330
|
+
recommendations: list[str],
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Add recommendations for component performance."""
|
|
333
|
+
slow_components = self._identify_slow_components(report.workflow_benchmarks)
|
|
334
|
+
if slow_components:
|
|
335
|
+
components_names = ", ".join(c.name for c in slow_components)
|
|
336
|
+
recommendations.append(
|
|
337
|
+
f"Slow workflow components: {components_names}. "
|
|
338
|
+
"Consider caching or optimization.",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _identify_slow_components(
|
|
342
|
+
self,
|
|
343
|
+
workflow_benchmarks: list[BenchmarkResult],
|
|
344
|
+
) -> list[BenchmarkResult]:
|
|
345
|
+
"""Identify slow workflow components."""
|
|
346
|
+
return [b for b in workflow_benchmarks if b.duration_seconds > 5]
|
|
347
|
+
|
|
348
|
+
def _add_overall_performance_recommendations(
|
|
349
|
+
self,
|
|
350
|
+
report: PerformanceReport,
|
|
351
|
+
recommendations: list[str],
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Add recommendations for overall performance."""
|
|
354
|
+
if report.total_duration > 300:
|
|
355
|
+
recommendations.append(
|
|
356
|
+
"Overall workflow execution is slow. Consider enabling --skip-hooks "
|
|
357
|
+
"during development iterations.",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _compare_with_baseline(
|
|
361
|
+
self,
|
|
362
|
+
current_report: PerformanceReport,
|
|
363
|
+
) -> dict[str, float]:
|
|
364
|
+
"""Compare current performance with historical baseline."""
|
|
365
|
+
baseline_comparison = {}
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
history = self._load_performance_history()
|
|
369
|
+
if not history:
|
|
370
|
+
return baseline_comparison
|
|
371
|
+
|
|
372
|
+
self._add_overall_performance_comparison(
|
|
373
|
+
current_report,
|
|
374
|
+
history,
|
|
375
|
+
baseline_comparison,
|
|
376
|
+
)
|
|
377
|
+
self._add_component_performance_comparison(
|
|
378
|
+
current_report,
|
|
379
|
+
history,
|
|
380
|
+
baseline_comparison,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
baseline_comparison["error"] = f"Could not load baseline: {e}"
|
|
385
|
+
|
|
386
|
+
return baseline_comparison
|
|
387
|
+
|
|
388
|
+
def _load_performance_history(self) -> list[dict[str, Any]] | None:
|
|
389
|
+
"""Load performance history from file."""
|
|
390
|
+
if not self.history_file.exists():
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
with self.history_file.open() as f:
|
|
394
|
+
history = json.load(f)
|
|
395
|
+
|
|
396
|
+
return history if history and len(history) > 1 else None
|
|
397
|
+
|
|
398
|
+
def _add_overall_performance_comparison(
|
|
399
|
+
self,
|
|
400
|
+
current_report: PerformanceReport,
|
|
401
|
+
history: list[dict[str, Any]],
|
|
402
|
+
comparison: dict[str, Any],
|
|
403
|
+
) -> None:
|
|
404
|
+
"""Add overall performance comparison to baseline."""
|
|
405
|
+
recent_runs = history[-5:]
|
|
406
|
+
baseline_duration = statistics.median(
|
|
407
|
+
[r["total_duration"] for r in recent_runs],
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
performance_change = (
|
|
411
|
+
(current_report.total_duration - baseline_duration) / baseline_duration
|
|
412
|
+
) * 100
|
|
413
|
+
comparison["overall_performance_change_percent"] = performance_change
|
|
414
|
+
|
|
415
|
+
def _add_component_performance_comparison(
|
|
416
|
+
self,
|
|
417
|
+
current_report: PerformanceReport,
|
|
418
|
+
history: list[dict[str, Any]],
|
|
419
|
+
comparison: dict[str, Any],
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Add component-level performance comparison."""
|
|
422
|
+
recent_runs = history[-5:]
|
|
423
|
+
if not recent_runs:
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
component_durations = recent_runs[-1].get("component_durations", {})
|
|
427
|
+
|
|
428
|
+
for component in current_report.workflow_benchmarks:
|
|
429
|
+
if component.name in component_durations:
|
|
430
|
+
old_duration = component_durations[component.name]
|
|
431
|
+
change = self._calculate_performance_change(
|
|
432
|
+
component.duration_seconds,
|
|
433
|
+
old_duration,
|
|
434
|
+
)
|
|
435
|
+
comparison[f"{component.name}_change_percent"] = change
|
|
436
|
+
|
|
437
|
+
def _calculate_performance_change(
|
|
438
|
+
self,
|
|
439
|
+
current_duration: float,
|
|
440
|
+
old_duration: float,
|
|
441
|
+
) -> float:
|
|
442
|
+
"""Calculate performance change percentage."""
|
|
443
|
+
return ((current_duration - old_duration) / old_duration) * 100
|
|
444
|
+
|
|
445
|
+
def _save_performance_history(self, report: PerformanceReport) -> None:
|
|
446
|
+
try:
|
|
447
|
+
history = []
|
|
448
|
+
if self.history_file.exists():
|
|
449
|
+
with self.history_file.open() as f:
|
|
450
|
+
history = json.load(f)
|
|
451
|
+
|
|
452
|
+
record = {
|
|
453
|
+
"timestamp": time.time(),
|
|
454
|
+
"total_duration": report.total_duration,
|
|
455
|
+
"component_durations": {
|
|
456
|
+
c.name: c.duration_seconds for c in report.workflow_benchmarks
|
|
457
|
+
},
|
|
458
|
+
"hook_durations": {
|
|
459
|
+
hook: (perf["mean_duration"] if isinstance(perf, dict) else perf)
|
|
460
|
+
for hook, perf in report.hook_performance.items()
|
|
461
|
+
},
|
|
462
|
+
"recommendations_count": len(report.recommendations),
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
history.append(record)
|
|
466
|
+
|
|
467
|
+
history = history[-50:]
|
|
468
|
+
|
|
469
|
+
with self.history_file.open("w") as f:
|
|
470
|
+
json.dump(history, f, indent=2)
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
self.console.print(
|
|
474
|
+
f"[yellow]⚠️[/yellow] Could not save performance history: {e}",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def display_performance_report(self, report: PerformanceReport) -> None:
|
|
478
|
+
self.console.print("\n[bold cyan]🚀 Performance Benchmark Report[/bold cyan]\n")
|
|
479
|
+
|
|
480
|
+
self._display_overall_stats(report)
|
|
481
|
+
self._display_workflow_components(report)
|
|
482
|
+
self._display_hook_performance(report)
|
|
483
|
+
self._display_baseline_comparison(report)
|
|
484
|
+
self._display_recommendations(report)
|
|
485
|
+
|
|
486
|
+
self.console.print(
|
|
487
|
+
f"\n[dim]📁 Benchmark data saved to: {self.benchmarks_dir}[/dim]",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def _display_overall_stats(self, report: PerformanceReport) -> None:
|
|
491
|
+
self.console.print(
|
|
492
|
+
f"[green]⏱️ Total Duration: {report.total_duration:.2f}s[/green]",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def _display_workflow_components(self, report: PerformanceReport) -> None:
|
|
496
|
+
if not report.workflow_benchmarks:
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
table = Table(title="Workflow Component Performance")
|
|
500
|
+
table.add_column("Component", style="cyan")
|
|
501
|
+
table.add_column("Duration (s)", style="yellow", justify="right")
|
|
502
|
+
table.add_column("Metadata", style="dim")
|
|
503
|
+
|
|
504
|
+
for benchmark in report.workflow_benchmarks:
|
|
505
|
+
metadata_str = ", ".join(f"{k}={v}" for k, v in benchmark.metadata.items())
|
|
506
|
+
table.add_row(
|
|
507
|
+
benchmark.name,
|
|
508
|
+
f"{benchmark.duration_seconds:.3f}",
|
|
509
|
+
metadata_str,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
self.console.print(table)
|
|
513
|
+
self.console.print()
|
|
514
|
+
|
|
515
|
+
def _display_hook_performance(self, report: PerformanceReport) -> None:
|
|
516
|
+
if not report.hook_performance:
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
table = Table(title="Hook Performance Analysis")
|
|
520
|
+
table.add_column("Hook", style="cyan")
|
|
521
|
+
table.add_column("Mean (s)", style="yellow", justify="right")
|
|
522
|
+
table.add_column("Min (s)", style="green", justify="right")
|
|
523
|
+
table.add_column("Max (s)", style="red", justify="right")
|
|
524
|
+
|
|
525
|
+
for hook_name, perf_data in report.hook_performance.items():
|
|
526
|
+
if isinstance(perf_data, dict):
|
|
527
|
+
table.add_row(
|
|
528
|
+
hook_name,
|
|
529
|
+
f"{perf_data.get('mean_duration', 0):.2f}",
|
|
530
|
+
f"{perf_data.get('min_duration', 0):.2f}",
|
|
531
|
+
f"{perf_data.get('max_duration', 0):.2f}",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
self.console.print(table)
|
|
535
|
+
self.console.print()
|
|
536
|
+
|
|
537
|
+
def _display_baseline_comparison(self, report: PerformanceReport) -> None:
|
|
538
|
+
if not report.baseline_comparison:
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
self._print_comparison_header()
|
|
542
|
+
self._print_comparison_metrics(report.baseline_comparison)
|
|
543
|
+
self.console.print()
|
|
544
|
+
|
|
545
|
+
def _print_comparison_header(self) -> None:
|
|
546
|
+
"""Print performance comparison header."""
|
|
547
|
+
self.console.print("[bold]📊 Performance Comparison[/bold]")
|
|
548
|
+
|
|
549
|
+
def _print_comparison_metrics(self, baseline_comparison: dict[str, t.Any]) -> None:
|
|
550
|
+
"""Print individual comparison metrics with appropriate colors."""
|
|
551
|
+
for metric, value in baseline_comparison.items():
|
|
552
|
+
if isinstance(value, float | int) and "percent" in metric:
|
|
553
|
+
color = "green" if value < 0 else "red" if value > 10 else "yellow"
|
|
554
|
+
direction = "faster" if value < 0 else "slower"
|
|
555
|
+
self.console.print(
|
|
556
|
+
f" {metric}: [{color}]{abs(value):.1f}% {direction}[/{color}]",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def _display_recommendations(self, report: PerformanceReport) -> None:
|
|
560
|
+
if report.recommendations:
|
|
561
|
+
self.console.print(
|
|
562
|
+
"[bold yellow]💡 Performance Recommendations[/bold yellow]",
|
|
563
|
+
)
|
|
564
|
+
for i, rec in enumerate(report.recommendations, 1):
|
|
565
|
+
self.console.print(f" {i}. {rec}")
|
|
566
|
+
else:
|
|
567
|
+
self.console.print("[green]✨ No performance issues detected![/green]")
|
|
568
|
+
|
|
569
|
+
def get_performance_trends(self, days: int = 7) -> dict[str, Any]:
|
|
570
|
+
"""Get performance trends over specified time period."""
|
|
571
|
+
try:
|
|
572
|
+
recent_history = self._get_recent_history(days)
|
|
573
|
+
if not recent_history:
|
|
574
|
+
return self._handle_insufficient_trend_data()
|
|
575
|
+
|
|
576
|
+
trends = {}
|
|
577
|
+
self._add_duration_trends(recent_history, trends)
|
|
578
|
+
self._add_component_trends(recent_history, trends)
|
|
579
|
+
trends["data_points"] = len(recent_history)
|
|
580
|
+
|
|
581
|
+
return trends
|
|
582
|
+
|
|
583
|
+
except Exception as e:
|
|
584
|
+
return {"error": f"Could not analyze trends: {e}"}
|
|
585
|
+
|
|
586
|
+
def _get_recent_history(self, days: int) -> list[dict[str, Any]] | None:
|
|
587
|
+
"""Get recent performance history within specified days."""
|
|
588
|
+
if not self.history_file.exists():
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
with self.history_file.open() as f:
|
|
592
|
+
history = json.load(f)
|
|
593
|
+
|
|
594
|
+
cutoff_time = time.time() - (days * 86400)
|
|
595
|
+
recent_history = [r for r in history if r.get("timestamp", 0) > cutoff_time]
|
|
596
|
+
|
|
597
|
+
return recent_history if len(recent_history) >= 2 else None
|
|
598
|
+
|
|
599
|
+
def _handle_insufficient_trend_data(self) -> dict[str, str]:
|
|
600
|
+
"""Handle cases where insufficient data is available for trend analysis."""
|
|
601
|
+
if not self.history_file.exists():
|
|
602
|
+
return {"error": "No performance history available"}
|
|
603
|
+
return {"error": "Insufficient data for trend analysis"}
|
|
604
|
+
|
|
605
|
+
def _add_duration_trends(
|
|
606
|
+
self, recent_history: list[dict[str, Any]], trends: dict[str, Any]
|
|
607
|
+
) -> None:
|
|
608
|
+
"""Add overall duration trends to results."""
|
|
609
|
+
durations = [r["total_duration"] for r in recent_history]
|
|
610
|
+
trends["duration_trend"] = {
|
|
611
|
+
"current": durations[-1],
|
|
612
|
+
"average": statistics.mean(durations),
|
|
613
|
+
"trend": self._determine_trend_direction(durations),
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
def _add_component_trends(
|
|
617
|
+
self, recent_history: list[dict[str, Any]], trends: dict[str, Any]
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Add component-level trends to results."""
|
|
620
|
+
component_trends = {}
|
|
621
|
+
latest_components = recent_history[-1].get("component_durations", {})
|
|
622
|
+
|
|
623
|
+
for component in latest_components:
|
|
624
|
+
component_durations = self._extract_component_durations(
|
|
625
|
+
recent_history,
|
|
626
|
+
component,
|
|
627
|
+
)
|
|
628
|
+
if len(component_durations) >= 2:
|
|
629
|
+
component_trends[component] = {
|
|
630
|
+
"current": component_durations[-1],
|
|
631
|
+
"average": statistics.mean(component_durations),
|
|
632
|
+
"trend": self._determine_trend_direction(component_durations),
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
trends["component_trends"] = component_trends
|
|
636
|
+
|
|
637
|
+
def _extract_component_durations(
|
|
638
|
+
self,
|
|
639
|
+
recent_history: list[dict[str, Any]],
|
|
640
|
+
component: str,
|
|
641
|
+
) -> list[float]:
|
|
642
|
+
"""Extract duration data for a specific component."""
|
|
643
|
+
return [
|
|
644
|
+
r.get("component_durations", {}).get(component)
|
|
645
|
+
for r in recent_history
|
|
646
|
+
if component in r.get("component_durations", {})
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
def _determine_trend_direction(self, durations: list[float]) -> str:
|
|
650
|
+
"""Determine if trend is improving or degrading."""
|
|
651
|
+
current = durations[-1]
|
|
652
|
+
historical_average = statistics.mean(durations[:-1])
|
|
653
|
+
return "improving" if current < historical_average else "degrading"
|