| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- # -*- coding: utf-8 -*-
- """
- Timing-Decorators für Performance-Messung.
- Bietet @timed für einfache Zeitmessung und @profile für
- detailliertes Profiling von Funktionen.
- """
- from __future__ import annotations
- import asyncio
- import functools
- import time
- from dataclasses import dataclass, field
- from datetime import datetime
- from typing import Any, Callable, ParamSpec, TypeVar
- P = ParamSpec("P")
- R = TypeVar("R")
- @dataclass
- class TimingResult:
- """
- Ergebnis einer Zeitmessung.
- Enthält alle relevanten Timing-Informationen für eine
- Funktionsausführung.
- """
- function_name: str
- duration_seconds: float
- start_time: datetime
- end_time: datetime
- args_count: int = 0
- kwargs_count: int = 0
- success: bool = True
- error: str | None = None
- @property
- def duration_ms(self) -> float:
- """Gibt die Dauer in Millisekunden zurück."""
- return self.duration_seconds * 1000
- def to_dict(self) -> dict[str, Any]:
- """Konvertiert das Ergebnis in ein Dictionary."""
- return {
- "function": self.function_name,
- "duration_seconds": self.duration_seconds,
- "duration_ms": self.duration_ms,
- "start_time": self.start_time.isoformat(),
- "end_time": self.end_time.isoformat(),
- "success": self.success,
- "error": self.error,
- }
- @dataclass
- class ProfilingStats:
- """
- Aggregierte Profiling-Statistiken für eine Funktion.
- Sammelt Statistiken über mehrere Aufrufe hinweg.
- """
- function_name: str
- call_count: int = 0
- total_time: float = 0.0
- min_time: float = float("inf")
- max_time: float = 0.0
- error_count: int = 0
- last_call: datetime | None = None
- _times: list[float] = field(default_factory=list)
- @property
- def avg_time(self) -> float:
- """Durchschnittliche Ausführungszeit."""
- return self.total_time / self.call_count if self.call_count > 0 else 0.0
- @property
- def success_rate(self) -> float:
- """Erfolgsrate in Prozent."""
- if self.call_count == 0:
- return 100.0
- return ((self.call_count - self.error_count) / self.call_count) * 100
- def record(self, duration: float, success: bool = True) -> None:
- """
- Zeichnet eine Ausführung auf.
- Args:
- duration: Dauer in Sekunden.
- success: Ob die Ausführung erfolgreich war.
- """
- self.call_count += 1
- self.total_time += duration
- self.min_time = min(self.min_time, duration)
- self.max_time = max(self.max_time, duration)
- self.last_call = datetime.now()
- if not success:
- self.error_count += 1
- # Für Percentil-Berechnung (limitiert auf letzte 1000 Werte)
- self._times.append(duration)
- if len(self._times) > 1000:
- self._times.pop(0)
- def percentile(self, p: float) -> float:
- """
- Berechnet das p-te Percentil der Ausführungszeiten.
- Args:
- p: Percentil zwischen 0 und 100.
- Returns:
- Percentil-Wert in Sekunden.
- """
- if not self._times:
- return 0.0
- sorted_times = sorted(self._times)
- index = int((p / 100) * len(sorted_times))
- index = min(index, len(sorted_times) - 1)
- return sorted_times[index]
- def reset(self) -> None:
- """Setzt alle Statistiken zurück."""
- self.call_count = 0
- self.total_time = 0.0
- self.min_time = float("inf")
- self.max_time = 0.0
- self.error_count = 0
- self.last_call = None
- self._times.clear()
- def to_dict(self) -> dict[str, Any]:
- """Konvertiert die Statistiken in ein Dictionary."""
- return {
- "function": self.function_name,
- "call_count": self.call_count,
- "total_time_seconds": self.total_time,
- "avg_time_seconds": self.avg_time,
- "min_time_seconds": self.min_time if self.min_time != float("inf") else 0,
- "max_time_seconds": self.max_time,
- "p50_seconds": self.percentile(50),
- "p95_seconds": self.percentile(95),
- "p99_seconds": self.percentile(99),
- "error_count": self.error_count,
- "success_rate": self.success_rate,
- "last_call": self.last_call.isoformat() if self.last_call else None,
- }
- # Globaler Speicher für Profiling-Statistiken
- _profiling_stats: dict[str, ProfilingStats] = {}
- def get_profiling_stats(function_name: str | None = None) -> dict[str, ProfilingStats]:
- """
- Gibt Profiling-Statistiken zurück.
- Args:
- function_name: Optional spezifischer Funktionsname.
- Returns:
- Dictionary mit Statistiken.
- """
- if function_name:
- if function_name in _profiling_stats:
- return {function_name: _profiling_stats[function_name]}
- return {}
- return dict(_profiling_stats)
- def reset_profiling_stats(function_name: str | None = None) -> None:
- """
- Setzt Profiling-Statistiken zurück.
- Args:
- function_name: Optional spezifischer Funktionsname.
- """
- if function_name:
- if function_name in _profiling_stats:
- _profiling_stats[function_name].reset()
- else:
- _profiling_stats.clear()
- def timed(
- callback: Callable[[TimingResult], None] | None = None,
- log_args: bool = False,
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
- """
- Decorator zur Zeitmessung einer Funktion.
- Misst die Ausführungszeit und ruft optional einen Callback
- mit dem Ergebnis auf.
- Args:
- callback: Optionale Funktion, die mit TimingResult aufgerufen wird.
- log_args: Ob Argument-Anzahl mitgeloggt werden soll.
- Returns:
- Decorator-Funktion.
- Example:
- @timed()
- def slow_function():
- time.sleep(1)
- @timed(callback=lambda r: print(f"Took {r.duration_ms}ms"))
- def another_function():
- pass
- """
- def decorator(func: Callable[P, R]) -> Callable[P, R]:
- @functools.wraps(func)
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
- start_time = datetime.now()
- start = time.perf_counter()
- error_msg = None
- success = True
- try:
- result = func(*args, **kwargs)
- return result
- except Exception as e:
- success = False
- error_msg = str(e)
- raise
- finally:
- end = time.perf_counter()
- end_time = datetime.now()
- duration = end - start
- timing_result = TimingResult(
- function_name=func.__qualname__,
- duration_seconds=duration,
- start_time=start_time,
- end_time=end_time,
- args_count=len(args) if log_args else 0,
- kwargs_count=len(kwargs) if log_args else 0,
- success=success,
- error=error_msg,
- )
- if callback:
- callback(timing_result)
- return wrapper
- return decorator
- def async_timed(
- callback: Callable[[TimingResult], None] | None = None,
- log_args: bool = False,
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
- """
- Async-Decorator zur Zeitmessung.
- Wie @timed, aber für async Funktionen.
- Args:
- callback: Optionale Funktion, die mit TimingResult aufgerufen wird.
- log_args: Ob Argument-Anzahl mitgeloggt werden soll.
- Returns:
- Decorator-Funktion.
- Example:
- @async_timed()
- async def slow_async_function():
- await asyncio.sleep(1)
- """
- def decorator(func: Callable[P, R]) -> Callable[P, R]:
- @functools.wraps(func)
- async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
- start_time = datetime.now()
- start = time.perf_counter()
- error_msg = None
- success = True
- try:
- result = await func(*args, **kwargs)
- return result
- except Exception as e:
- success = False
- error_msg = str(e)
- raise
- finally:
- end = time.perf_counter()
- end_time = datetime.now()
- duration = end - start
- timing_result = TimingResult(
- function_name=func.__qualname__,
- duration_seconds=duration,
- start_time=start_time,
- end_time=end_time,
- args_count=len(args) if log_args else 0,
- kwargs_count=len(kwargs) if log_args else 0,
- success=success,
- error=error_msg,
- )
- if callback:
- callback(timing_result)
- return wrapper
- return decorator
- def profile(
- name: str | None = None,
- threshold_seconds: float | None = None,
- callback: Callable[[ProfilingStats], None] | None = None,
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
- """
- Decorator für detailliertes Profiling.
- Sammelt Statistiken über mehrere Aufrufe hinweg.
- Args:
- name: Optionaler Name für die Statistiken (Default: Funktionsname).
- threshold_seconds: Optionaler Schwellenwert für Warnungen.
- callback: Optionaler Callback bei Schwellenwert-Überschreitung.
- Returns:
- Decorator-Funktion.
- Example:
- @profile(threshold_seconds=1.0)
- def database_query():
- # Langsame Abfrage
- pass
- # Statistiken abrufen
- stats = get_profiling_stats("database_query")
- """
- def decorator(func: Callable[P, R]) -> Callable[P, R]:
- stats_name = name or func.__qualname__
- # Statistik-Objekt erstellen falls nicht vorhanden
- if stats_name not in _profiling_stats:
- _profiling_stats[stats_name] = ProfilingStats(function_name=stats_name)
- stats = _profiling_stats[stats_name]
- @functools.wraps(func)
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
- start = time.perf_counter()
- success = True
- try:
- result = func(*args, **kwargs)
- return result
- except Exception:
- success = False
- raise
- finally:
- duration = time.perf_counter() - start
- stats.record(duration, success)
- # Schwellenwert-Prüfung
- if threshold_seconds and duration > threshold_seconds:
- if callback:
- callback(stats)
- # Statistik-Zugriff über die Funktion ermöglichen
- wrapper.stats = stats # type: ignore
- wrapper.reset_stats = stats.reset # type: ignore
- return wrapper
- return decorator
- def async_profile(
- name: str | None = None,
- threshold_seconds: float | None = None,
- callback: Callable[[ProfilingStats], None] | None = None,
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
- """
- Async-Decorator für detailliertes Profiling.
- Args:
- name: Optionaler Name für die Statistiken.
- threshold_seconds: Optionaler Schwellenwert für Warnungen.
- callback: Optionaler Callback bei Schwellenwert-Überschreitung.
- Returns:
- Decorator-Funktion.
- """
- def decorator(func: Callable[P, R]) -> Callable[P, R]:
- stats_name = name or func.__qualname__
- if stats_name not in _profiling_stats:
- _profiling_stats[stats_name] = ProfilingStats(function_name=stats_name)
- stats = _profiling_stats[stats_name]
- @functools.wraps(func)
- async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
- start = time.perf_counter()
- success = True
- try:
- result = await func(*args, **kwargs)
- return result
- except Exception:
- success = False
- raise
- finally:
- duration = time.perf_counter() - start
- stats.record(duration, success)
- if threshold_seconds and duration > threshold_seconds:
- if callback:
- callback(stats)
- wrapper.stats = stats # type: ignore
- wrapper.reset_stats = stats.reset # type: ignore
- return wrapper
- return decorator
|