"""Performance monitoring utilities for pyqt-reactor.
Provides decorators and context managers for timing operations and
logging performance metrics.
"""
import time
import functools
import logging
import os
from contextlib import contextmanager
from typing import Optional, Callable
from pathlib import Path
from pyqt_reactive.protocols import get_form_config
# Create performance logger
_config = get_form_config()
perf_logger = logging.getLogger(_config.performance_logger_name)
perf_logger.setLevel(logging.WARNING)
# Add file handler for performance logs
_data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
_log_dir = Path(_config.log_dir) if _config.log_dir else _data_home / "pyqt_reactive" / "logs"
perf_log_file = _log_dir / _config.performance_log_filename
perf_log_file.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(perf_log_file)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
perf_logger.addHandler(file_handler)
# Also log to console
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(logging.Formatter(
'⏱️ %(message)s'
))
perf_logger.addHandler(console_handler)
[docs]
@contextmanager
def timer(operation_name: str, threshold_ms: float = 0.0, log_args: bool = False, **kwargs):
"""Context manager for timing operations.
Args:
operation_name: Name of the operation being timed
threshold_ms: Only log if operation takes longer than this (in milliseconds)
log_args: Whether to log kwargs in the message
**kwargs: Additional context to include in log message
Example:
with timer("Loading config", threshold_ms=10.0, config_type="GlobalPipelineConfig"):
config = load_config()
"""
start = time.perf_counter()
try:
yield
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
if elapsed_ms >= threshold_ms:
msg = f"{operation_name}: {elapsed_ms:.2f}ms"
if log_args and kwargs:
args_str = ", ".join(f"{k}={v}" for k, v in kwargs.items())
msg += f" ({args_str})"
perf_logger.debug(msg)
[docs]
def timed(operation_name: Optional[str] = None, threshold_ms: float = 0.0):
"""Decorator for timing function calls.
Args:
operation_name: Name for the operation (defaults to function name)
threshold_ms: Only log if operation takes longer than this (in milliseconds)
Example:
@timed("Config loading", threshold_ms=10.0)
def load_config():
...
"""
def decorator(func: Callable) -> Callable:
nonlocal operation_name
if operation_name is None:
operation_name = f"{func.__module__}.{func.__qualname__}"
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
if elapsed_ms >= threshold_ms:
perf_logger.debug(f"{operation_name}: {elapsed_ms:.2f}ms")
return wrapper
return decorator
# Global monitors for common operations
_monitors = {}
[docs]
def get_monitor(operation_name: str) -> PerformanceMonitor:
"""Get or create a global monitor for an operation.
Example:
monitor = get_monitor("Placeholder resolution")
with monitor.measure():
resolve_placeholder(field)
"""
if operation_name not in _monitors:
_monitors[operation_name] = PerformanceMonitor(operation_name)
return _monitors[operation_name]
[docs]
def report_all_monitors():
"""Report statistics for all global monitors."""
if not _monitors:
perf_logger.debug("No performance monitors active")
return
perf_logger.debug("=" * 60)
perf_logger.debug("PERFORMANCE SUMMARY")
perf_logger.debug("=" * 60)
for monitor in _monitors.values():
monitor.report()
perf_logger.debug("=" * 60)
[docs]
def reset_all_monitors():
"""Reset all global monitors."""
for monitor in _monitors.values():
monitor.reset()
# Convenience function to enable/disable performance logging
_enabled = True