pounce/PERFORMANCE_ARCHITECTURE_REPORT.md

213 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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. 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