From fd30d99fd60d5fb6f82d6db5f7fce81e46588171 Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Fri, 12 Dec 2025 11:22:50 +0100 Subject: [PATCH] docs: performance & architecture improvement report --- PERFORMANCE_ARCHITECTURE_REPORT.md | 206 +++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 PERFORMANCE_ARCHITECTURE_REPORT.md diff --git a/PERFORMANCE_ARCHITECTURE_REPORT.md b/PERFORMANCE_ARCHITECTURE_REPORT.md new file mode 100644 index 0000000..b726ccd --- /dev/null +++ b/PERFORMANCE_ARCHITECTURE_REPORT.md @@ -0,0 +1,206 @@ +# Performance & Architektur Report (Pounce) + +**Stand (Codebase):** `ceb4484a3dd2dd8a9289fef623e7395d7e26274b` +**Datum:** 2025-12-12 +**Scope:** `frontend/` (Next.js 14 App Router) + `backend/` (FastAPI + async SQLAlchemy + APScheduler) + DB + Docker/Deploy. + +--- + +## 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 + +