193 lines
5.8 KiB
Python
193 lines
5.8 KiB
Python
"""
|
|
EU-Utility - Startup Profiler
|
|
|
|
Profiles application startup time and identifies bottlenecks.
|
|
"""
|
|
|
|
import time
|
|
import sys
|
|
from functools import wraps
|
|
from typing import Dict, List, Callable, Optional
|
|
from dataclasses import dataclass, field
|
|
from collections import defaultdict
|
|
|
|
|
|
@dataclass
|
|
class ProfileResult:
|
|
"""Result of profiling a component."""
|
|
name: str
|
|
start_time: float
|
|
end_time: float
|
|
duration_ms: float
|
|
parent: Optional[str] = None
|
|
children: List[str] = field(default_factory=list)
|
|
|
|
|
|
class StartupProfiler:
|
|
"""
|
|
Profiles application startup time.
|
|
|
|
Usage:
|
|
profiler = StartupProfiler()
|
|
|
|
with profiler.profile("database_init"):
|
|
init_database()
|
|
|
|
profiler.print_report()
|
|
"""
|
|
|
|
_instance = None
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
def __init__(self):
|
|
if self._initialized:
|
|
return
|
|
|
|
self.results: Dict[str, ProfileResult] = {}
|
|
self._stack: List[str] = []
|
|
self._start_times: Dict[str, float] = {}
|
|
self.enabled = True
|
|
self.app_start_time = time.perf_counter()
|
|
self._initialized = True
|
|
|
|
def profile(self, name: str):
|
|
"""Context manager for profiling a code block."""
|
|
return self._ProfileContext(self, name)
|
|
|
|
class _ProfileContext:
|
|
def __init__(self, profiler, name):
|
|
self.profiler = profiler
|
|
self.name = name
|
|
|
|
def __enter__(self):
|
|
if self.profiler.enabled:
|
|
self.profiler._start(self.name)
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if self.profiler.enabled:
|
|
self.profiler._end(self.name)
|
|
return False
|
|
|
|
def _start(self, name: str):
|
|
"""Start profiling a component."""
|
|
self._start_times[name] = time.perf_counter()
|
|
|
|
parent = self._stack[-1] if self._stack else None
|
|
self._stack.append(name)
|
|
|
|
if parent:
|
|
if parent not in self.results:
|
|
self.results[parent] = ProfileResult(
|
|
name=parent,
|
|
start_time=0,
|
|
end_time=0,
|
|
duration_ms=0,
|
|
children=[]
|
|
)
|
|
self.results[parent].children.append(name)
|
|
|
|
def _end(self, name: str):
|
|
"""End profiling a component."""
|
|
end_time = time.perf_counter()
|
|
start_time = self._start_times.pop(name, end_time)
|
|
duration = (end_time - start_time) * 1000
|
|
|
|
if name in self._stack:
|
|
self._stack.remove(name)
|
|
|
|
self.results[name] = ProfileResult(
|
|
name=name,
|
|
start_time=start_time - self.app_start_time,
|
|
end_time=end_time - self.app_start_time,
|
|
duration_ms=duration,
|
|
parent=self._stack[-1] if self._stack else None
|
|
)
|
|
|
|
def print_report(self):
|
|
"""Print startup profiling report."""
|
|
total_time = (time.perf_counter() - self.app_start_time) * 1000
|
|
|
|
print("\n" + "=" * 80)
|
|
print("🚀 STARTUP PERFORMANCE REPORT")
|
|
print("=" * 80)
|
|
print(f"{'Component':<40} {'Time (ms)':<12} {'% of Total':<12} {'Status'}")
|
|
print("-" * 80)
|
|
|
|
# Sort by duration
|
|
sorted_results = sorted(
|
|
self.results.items(),
|
|
key=lambda x: x[1].duration_ms,
|
|
reverse=True
|
|
)
|
|
|
|
for name, result in sorted_results:
|
|
pct = (result.duration_ms / total_time * 100) if total_time > 0 else 0
|
|
status = "⚠️ SLOW" if result.duration_ms > 1000 else "✓ OK" if result.duration_ms > 100 else "✓ FAST"
|
|
indent = " " * (len(name.split(".")) - 1)
|
|
print(f"{indent}{name:<38} {result.duration_ms:>10.2f} {pct:>10.1f}% {status}")
|
|
|
|
print("-" * 80)
|
|
print(f"{'TOTAL STARTUP TIME':<40} {total_time:>10.2f} ms")
|
|
print("=" * 80)
|
|
|
|
# Identify bottlenecks
|
|
slow_components = [r for r in self.results.values() if r.duration_ms > 500]
|
|
if slow_components:
|
|
print("\n⚠️ BOTTLENECKS DETECTED:")
|
|
for r in sorted(slow_components, key=lambda x: x.duration_ms, reverse=True):
|
|
print(f" • {r.name}: {r.duration_ms:.2f} ms")
|
|
print()
|
|
|
|
def get_bottlenecks(self, threshold_ms: float = 500) -> List[ProfileResult]:
|
|
"""Get components that took longer than threshold."""
|
|
return [
|
|
r for r in self.results.values()
|
|
if r.duration_ms > threshold_ms
|
|
]
|
|
|
|
def reset(self):
|
|
"""Reset profiler."""
|
|
self.results.clear()
|
|
self._stack.clear()
|
|
self._start_times.clear()
|
|
self.app_start_time = time.perf_counter()
|
|
|
|
|
|
def profile_startup(name: str):
|
|
"""Decorator for profiling function startup time."""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
profiler = StartupProfiler()
|
|
with profiler.profile(name):
|
|
return func(*args, **kwargs)
|
|
return wrapper
|
|
return decorator
|
|
|
|
|
|
# Convenience function
|
|
def get_startup_profiler() -> StartupProfiler:
|
|
"""Get global StartupProfiler instance."""
|
|
return StartupProfiler()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test profiler
|
|
profiler = StartupProfiler()
|
|
|
|
with profiler.profile("test_component"):
|
|
time.sleep(0.1)
|
|
with profiler.profile("nested_component"):
|
|
time.sleep(0.05)
|
|
|
|
with profiler.profile("another_component"):
|
|
time.sleep(0.2)
|
|
|
|
profiler.print_report()
|