docs: performance & architecture improvement report

This commit is contained in:
yves.gugger
2025-12-12 11:22:50 +01:00
parent ceb4484a3d
commit fd30d99fd6

View File

@ -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. 10000 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 1560s.
---
### 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 35.
### 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 (01 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 (12 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 (26 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