pounce/PERFORMANCE_ARCHITECTURE_REPORT.md

9.3 KiB
Raw Permalink Blame History

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