# Performance & Architektur Report (Pounce) **Stand (Codebase):** `d08ca33fe3c88b3b2d716f0bdf22b71f989a5eb9` **Datum:** 2025-12-12 **Scope:** `frontend/` (Next.js 14 App Router) + `backend/` (FastAPI + async SQLAlchemy + APScheduler) + DB + Docker/Deploy. ## Status (umgesetzt) - ✅ **Phase 0**: Scheduler split, Market-Feed bounded paging, Health cache-first, PriceTracker N+1 fix (`2e8ff50`) - ✅ **Phase 1**: DB migrations (indexes + optional columns), persisted `pounce_score`, Admin N+1 removal, Radar summary endpoint (`ee4266d`) - ✅ **Phase 2**: Redis + ARQ worker scaffolding, Prometheus metrics (`/metrics`), load test scaffolding, Docker hardening (`5d23d34`) --- ## Executive Summary (die 5 größten Hebel) 1. **Scheduler aus dem API-Prozess herauslösen** Aktuell startet der Scheduler in `backend/app/main.py` im App-Lifespan. Bei mehreren Uvicorn/Gunicorn-Workern laufen Jobs **mehrfach parallel** → doppelte Scrapes/Checks, DB-Last, E-Mail-Spam, inkonsistente Zustände. 2. **Market Feed Endpoint (`/api/v1/auctions/feed`) DB-seitig paginieren/sortieren** `backend/app/api/auctions.py` lädt derzeit **alle aktiven Auktionen + alle aktiven Listings** in Python, berechnet Score, sortiert, und paginiert erst am Ende. Das skaliert schlecht sobald ihr > ein paar hundert Auktionen habt. 3. **Price Tracker N+1 eliminieren** `backend/app/services/price_tracker.py::detect_price_changes()` macht aktuell: *distinct(tld, registrar) → pro Paar query(limit 2)*. Das ist ein klassischer N+1 und wird bei 800+ TLDs schnell sehr langsam. 4. **Health-Cache wirklich nutzen** Es gibt `DomainHealthCache`, und der Scheduler schreibt Status/Score. Aber `GET /domains/{id}/health` macht immer einen **Live-Check** (`domain_health.py` mit HTTP/DNS/SSL). Für UI/Performance besser: default **cached**, live nur “Refresh”. 5. **Valuation im Request-Path reduzieren (Auctions)** `backend/app/api/auctions.py` berechnet pro Auction im Response optional valuation, und `valuation_service` fragt pro Domain auch DB-Daten ab (TLD cost). Das ist pro Request potenziell **sehr teuer**. --- ## Messwerte (Frontend Build) Aus `frontend/` → `npm run build` (Next.js 14.0.4): - **First Load JS (shared):** ~81.9 kB - **Größte Pages (First Load):** - `/terminal/watchlist`: ~120 kB - `/terminal/radar`: ~120 kB - `/terminal/intel/[tld]`: ~115 kB - `/terminal/market`: ~113 kB - **Warnings:** Einige Routen “deopted into client-side rendering” (z.B. `/terminal/radar`, `/terminal/listing`, `/unsubscribe`, `/terminal/welcome`). Das ist nicht zwingend schlimm, aber ein Hinweis: dort wird kein echtes SSR/Static genutzt. **Interpretation:** Das Frontend ist vom Bundle her bereits recht schlank. Die größten Performance-Risiken liegen aktuell eher im **Backend (Queries, Jobs, N+1, Caching)**. --- ## Backend – konkrete Hotspots & Fixes ### 1) Scheduler: Architektur & Skalierung **Ist-Zustand** - `backend/app/main.py`: `start_scheduler()` im `lifespan()` → Scheduler läuft im selben Prozess wie die API. - `backend/app/scheduler.py`: viele Jobs (Domain Checks, Health Checks, TLD Scrape, Auction Scrape, Cleanup, Sniper Matching). **Probleme** - Multi-worker Deployment (Gunicorn/Uvicorn) → Scheduler läuft pro Worker → Job-Duplikate. - Jobs sind teils sequentiell (Domain Checks), teils N+1 (Health Cache, Digests, Sniper Matching). **Empfehlung (Best Practice)** - **Scheduler als separater Service/Container** laufen lassen (z.B. eigener Docker Service `scheduler`, oder systemd/cron job, oder Celery Worker + Beat). - Wenn Scheduler im selben Code bleiben soll: **Leader-Lock** (Redis/DB advisory lock) einbauen, sodass nur ein Prozess Jobs ausführt. --- ### 2) Market Feed (`backend/app/api/auctions.py::get_market_feed`) **Ist-Zustand** - Holt Listings und Auktionen ohne DB-Limit/Offset, baut `items` in Python, sortiert in Python, paginiert erst am Ende. **Warum das weh tut** - Bei z.B. 10’000 aktiven Auktionen ist jeder Request an `/feed` ein “Full table scan + Python sort + JSON build”. **Fix-Strategie** - **Score persistieren**: `pounce_score` in `DomainAuction` und `DomainListing` speichern/aktualisieren (beim Scrape bzw. beim Listing Create/Update). Dann kann man DB-seitig `WHERE pounce_score >= :min_score` und `ORDER BY pounce_score DESC` machen. - **DB-Pagination**: `LIMIT/OFFSET` in SQL, nicht in Python. - **Filter DB-seitig**: `keyword`, `tld`, `price range`, `ending_within` in SQL. - **Response caching**: Für public feed (oder häufige Filterkombos) Redis TTL 15–60s. --- ### 3) Auction Search (`backend/app/api/auctions.py::search_auctions`) **Ist-Zustand** - Nach Query werden Auktionen in Python gefiltert (Vanity Filter) und dann pro Auction in einer Schleife `valuation_service.estimate_value(...)` aufgerufen. **Probleme** - Valuation kann DB-Queries pro Item auslösen (TLD cost avg), und läuft seriell. **Fix-Strategie** - Valuations **vorberechnen** (Background Job) und in einer Tabelle/Spalte cachen. - Alternativ: Valuation nur **für Top-N** (z.B. 20) berechnen und für den Rest weglassen. - TLD-Cost als **in-memory cache** (LRU/TTL) oder einmal pro Request prefetchen. --- ### 4) Price Tracker (`backend/app/services/price_tracker.py`) **Ist-Zustand** - N+1 Queries: distinct(tld, registrar) → pro Paar 1 Query für die letzten 2 Preise. **Fix-Strategie** - SQL Window Function (Postgres & SQLite können das): - `ROW_NUMBER() OVER (PARTITION BY tld, registrar ORDER BY recorded_at DESC)` - dann self-join oder `LAG()` für vorherigen Preis. - Zusätzlich DB-Index: `tld_prices(tld, registrar, recorded_at DESC)` --- ### 5) Domain Health (`backend/app/services/domain_health.py` + `backend/app/api/domains.py`) **Ist-Zustand** - Live Health Check macht pro Request echte DNS/HTTP/SSL Checks. - Scheduler schreibt `DomainHealthCache`, aber Endpoint nutzt ihn nicht. **Fix-Strategie** - Neue Endpoints: - `GET /domains/health-cache` → cached health für alle Domains eines Users (1 Request für UI) - `POST /domains/{id}/health/refresh` → live refresh (asynchron, job queue) - `DomainHealthCache` auch mit `dns_data/http_data/ssl_data` befüllen (ist im Model vorgesehen). --- ## Datenbank – Indexing & Query Patterns ### Empfohlene Indizes (High Impact) - **Domain Checks** - `domain_checks(domain_id, checked_at DESC)` für `/domains/{id}/history` - **TLD Prices** - `tld_prices(tld, registrar, recorded_at DESC)` für “latest two prices” und history queries - **Health Cache** - `domain_health_cache(domain_id)` (unique ist vorhanden), optional `checked_at` ### Query-Patterns (Quick Wins) - In `backend/app/api/domains.py::add_domain()` wird aktuell `len(current_user.domains)` genutzt → lädt potenziell viele Rows. Besser: `SELECT COUNT(*) FROM domains WHERE user_id = ...`. - Admin “Users list”: vermeidet N+1 (Subscription + Domain Count pro User) → `JOIN` + `GROUP BY`. --- ## Frontend – Verbesserungen (gezielt, nicht “blind refactor”) ### 1) Reduziere API-Calls pro Screen (Dashboard/Watchlist) Aktuell holen manche Screens mehrere Endpoints und rechnen Stats client-side: - `/terminal/radar`: holt u.a. Auctions und `GET /listings/my` nur um Stats zu zählen. **Empfehlung** - Ein Endpoint: `GET /dashboard/summary` (counts + small previews) → 1 Request statt 3–5. ### 2) Tabellen/Listen skalieren - Für sehr große Listen (Market Feed / TLDs / Admin Users) mittelfristig: - Pagination + “infinite scroll” - ggf. Virtualisierung (`react-window`) falls 1000+ Rows. ### 3) Kleine Code-Health Fixes (auch Performance) - Achtung bei `.sort()` auf State-Arrays: `.sort()` mutiert. Immer vorher kopieren (`[...arr].sort(...)`), sonst entstehen subtile Bugs und unnötige Re-Renders. --- ## Deployment/Infra – “Production grade” Performance ### Backend - **Gunicorn + Uvicorn Workers** (oder Uvicorn `--workers`) ist gut für CPU/IO – aber **nur wenn Scheduler separat läuft**. - **DB Pooling**: `create_async_engine(..., pool_size=..., max_overflow=...)` für Postgres (nicht bei SQLite). - **slowapi**: in Production Redis storage verwenden (sonst pro Worker eigener limiter state). ### Frontend - Dockerfile erwartet `.next/standalone`. Dafür in `frontend/next.config.js` `output: 'standalone'` aktivieren (oder Dockerfile anpassen). --- ## Priorisierte Roadmap ### Phase 0 (0–1 Tag, Quick Wins) - Scheduler entkoppeln ODER Leader-Lock einbauen - `/auctions/feed`: DB-limit + offset + order_by (keine full scans) - `PriceTracker.detect_price_changes`: Window-Query statt N+1 - Cached Health Endpoint für Watchlist ### Phase 1 (1–2 Wochen) - Precompute `pounce_score` + valuations (Background Jobs), persistieren & cachen - Admin N+1 entfernen (Users list) - DB Indizes ergänzen (DomainCheck, TLDPrice) - “Dashboard summary” Endpoint + Frontend umstellen ### Phase 2 (2–6 Wochen) - Background-Job System (Celery/RQ/Dramatiq) + Redis - Observability: Request timing, DB query timing, Prometheus metrics, tracing - Load testing + Performance budgets (API p95, page LCP/TTFB) --- ## Mess-/Monitoring Plan (damit wir nicht im Dunkeln optimieren) - **Backend** - Log: Request duration + endpoint + status - DB: slow query logging / EXPLAIN ANALYZE (prod-like) - Metrics: p50/p95 latency pro endpoint, queue depth, job runtime - **Frontend** - Core Web Vitals Tracking (ist bereits angelegt in `frontend/src/lib/analytics.ts`) - “API Timing” (TTFB + payload size) für Market/Watchlist