Perf: 9 Performance-Fixes — SW by-v1072
Backend: - DB: 3 neue Indizes (forum_posts thread+user, routes user) — Forum/Routen-Queries - Caching: cache.py (TTL-Cache ohne neue Dependency) für 5 statische Listen (training_exercises, pflege_tipps, wiki_stats, wiki_gruppen, help_articles) - diary.py + breeder_photos.py: Bildverarbeitung (ffmpeg/PIL/EXIF) per run_in_executor → blockiert Event-Loop nicht mehr - scheduler.py: 11 kollidierende Jobs auf 5-Min-Intervalle gestaggert, coalesce=True - social.py: ORDER BY RANDOM() ohne LIMIT in 2 Stellen gefixt - alerts.py: Haversine-Loop bekommt SQL-Bounding-Box-Vorfilter Frontend: - sw.js: Tile-Cache mit LRU-Eviction (max 500 Einträge) - admin.js: Event-Listener-Leak — Tab-Klicks per Delegation statt N Listener - api.js: compressImage() Helper — Client-seitiges Resize auf max 2000px (HEIC/Videos/<500KB unverändert), integriert in 8 Upload-Stellen (diary, dog-profile×2, walks, poison, lost, health×2) Bump APP_VER 1071 → 1072 (sw.js, app.js, main.py, index.html)
This commit is contained in:
parent
3abf974d29
commit
c03884cb81
23 changed files with 461 additions and 120 deletions
103
backend/cache.py
Normal file
103
backend/cache.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""BAN YARO — In-Memory TTL-Cache für statische DB-Daten.
|
||||
|
||||
Hintergrund:
|
||||
Routes wie /api/training/exercises, /api/help, /api/wiki/stats laden bei
|
||||
jedem Request statische Daten aus der DB. Das ist verschwendete Energie.
|
||||
Diese Daten ändern sich nur durch Admin-Aktionen.
|
||||
|
||||
Verwendung:
|
||||
from cache import ttl_cache
|
||||
|
||||
@ttl_cache(ttl=3600)
|
||||
def my_func(arg1, arg2):
|
||||
...
|
||||
|
||||
API:
|
||||
@ttl_cache(ttl=3600) Decorator – cached pro Argumenten-Signatur
|
||||
my_func.cache_clear() Komplett leeren (z.B. nach Admin-Update)
|
||||
|
||||
Hinweis:
|
||||
Diese Implementierung ist absichtlich klein und ohne externe Dependency
|
||||
(kein cachetools nötig). Thread-safe via Lock. Reicht für Read-Only-
|
||||
Listen, die sich selten ändern. Niemals für user-spezifische Daten
|
||||
verwenden!
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
def ttl_cache(ttl: int = 3600, maxsize: int = 128) -> Callable:
|
||||
"""Decorator: cached Rückgabewert pro Argumenten-Signatur für `ttl` Sek.
|
||||
|
||||
- ttl: Time-to-live in Sekunden (Default: 1 Stunde)
|
||||
- maxsize: max. Anzahl Einträge im Cache (FIFO-Eviction bei Überlauf)
|
||||
|
||||
Die dekorierte Funktion bekommt zusätzlich:
|
||||
.cache_clear() – leert den gesamten Cache
|
||||
.cache_info() – {hits, misses, size, ttl, maxsize}
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
store: dict[tuple, tuple[float, Any]] = {}
|
||||
lock = threading.Lock()
|
||||
stats = {"hits": 0, "misses": 0}
|
||||
|
||||
def _make_key(args: tuple, kwargs: dict) -> tuple:
|
||||
# kwargs als sortiertes Tuple in den Key packen
|
||||
if kwargs:
|
||||
return args + tuple(sorted(kwargs.items()))
|
||||
return args
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = _make_key(args, kwargs)
|
||||
now = time.monotonic()
|
||||
with lock:
|
||||
cached = store.get(key)
|
||||
if cached is not None:
|
||||
expires_at, value = cached
|
||||
if expires_at > now:
|
||||
stats["hits"] += 1
|
||||
return value
|
||||
# abgelaufen → raus
|
||||
del store[key]
|
||||
stats["misses"] += 1
|
||||
|
||||
# Außerhalb des Locks ausführen (kann DB-Calls machen)
|
||||
value = func(*args, **kwargs)
|
||||
|
||||
with lock:
|
||||
# FIFO-Eviction, wenn maxsize überschritten
|
||||
if len(store) >= maxsize:
|
||||
try:
|
||||
oldest_key = next(iter(store))
|
||||
del store[oldest_key]
|
||||
except StopIteration:
|
||||
pass
|
||||
store[key] = (now + ttl, value)
|
||||
return value
|
||||
|
||||
def cache_clear() -> None:
|
||||
with lock:
|
||||
store.clear()
|
||||
stats["hits"] = 0
|
||||
stats["misses"] = 0
|
||||
|
||||
def cache_info() -> dict:
|
||||
with lock:
|
||||
return {
|
||||
"hits": stats["hits"],
|
||||
"misses": stats["misses"],
|
||||
"size": len(store),
|
||||
"ttl": ttl,
|
||||
"maxsize": maxsize,
|
||||
}
|
||||
|
||||
wrapper.cache_clear = cache_clear # type: ignore[attr-defined]
|
||||
wrapper.cache_info = cache_info # type: ignore[attr-defined]
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
Loading…
Add table
Add a link
Reference in a new issue