Playwright-Projekt perf (nur mit PERF_WEB_URL, nur lesend, nie in der CI): CDP-Drosselung 100 ms / 10 Mbit/s, mind. 100 Suchen, Messung im Browser vom letzten Tastendruck bis zur ersten Kachel. Ladeskript fuer synthetisches Volumen unter supabase/perf/. Messung und Aussagegrenzen in der Umsetzungsdoku. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
15 KiB
Spec #3 – KAT-01 Spiele suchen: Umsetzungs-Nachtrag PR 3a (Backend)
Ergänzt docs/specs/3-katalog-suche.md (die Spec selbst wird hier nicht geändert). Stand: 2026-11-04, Branch feature/3a-katalog-suche-backend, Migration 20261103235947_catalog_search.sql, Tests supabase/tests/database/09_catalog_search.test.sql.
1. Abweichungen und Präzisierungen zum Vertrag
nullfürp_limit/p_offsetgilt als Standardwert (30/0). Nur Leerraum (auch Tab, Zeilenumbruch) gilt als kein Suchbegriff.- Ein
smallint-Parameter außerhalb des Typbereichs scheitert beim Umwandeln mit 22003, nicht mit P0001. - Die Ähnlichkeit (zweites Sortierkriterium) ist das Maximum über Haupttitel und alle alternativen Titel. Sortierung binär (
collate "C"), dannid. - Rang 0 prüft nur
replace(ts,' ','') = qc(schließtts = qein). - Von mehreren treffenden alternativen Titeln gewinnt die beste Stufe, dann die größte Ähnlichkeit, dann die Schreibweise. Bei gleicher Stufe von Haupt- und alternativem Titel ist
matched_alt_titlenull. - Stufe 4 trifft auch ohne Verlag: Jedes Suchwort muss in einem Titel (Haupt- oder alternativer Titel) oder einem Verlagsnamen des Spiels vorkommen. Verteilen sich die Wörter auf Haupt- und alternativen Titel (kein einzelner Titel enthält alle), ist der Treffer ebenfalls Stufe 4, obwohl kein Verlag beteiligt ist. Die Spec nennt als Stufe-4-Fall nur den Verlagstreffer.
- Doppelt eingegebene Wörter zählen einfach.
- Filter einmal berechnet: Spielerzahl, Dauer, Alter, Genre und Mechanik werden zu einer ID-Menge ausgewertet, die beide Zweige (mit und ohne Suchbegriff) nutzen.
- Messskript unter
supabase/perf/stattsupabase/tests/perf/:supabase-db-tests.shundsupabase test dblaufen rekursiv übertests/und würden es mitlaufen lassen. - Testplan:
00_schema22 Tabellen;07/08tragengame_alt_titles.game_idals "bewusst ausgelassen" ein;06bekam die Prüfabfrage-ZeileInfo-Alt(statt einer zweitenInfo) und Grants für die Test-Rollecatalog_importer. - Für SAM-08: Die Import-Rolle braucht
executeaufpublic.search_normalize(text),usageauf das Schemaextensionsund Schreibrechte aufgame_alt_titles. - Performance-Fehler der ersten Fassung: korrelierte Unterabfragen je Spiel (Rang 4, Filter) brauchten 13 s bei 10 000 Spielen. Behoben durch Mengenoperationen (
word_hits/word_cover, gehashte ID-Menge,like all, Ähnlichkeit nur für Treffer). Stufen 1 und 2 (Kandidaten-CTE,title_compact) sind nicht gebaut.
2. Messung B5 (lokal, Docker auf macOS, Postgres 17.11)
Skript supabase/perf/catalog_search_perf.sql: 10 000 Katalogspiele, 20 002 alternative Titel, 2 000 Verlage, 12 919 Verlagszuordnungen, je Spiel 2 Genres und 3 Mechaniken; eine Transaktion mit Rollback (danach vacuum (analyze), weil ANALYZE nicht zurückrollbar ist); Messung als anon mit RLS, set local role anon, Generic Plans (ab der 6. Ausführung, wie bei PostgREST-Verbindungen). Das Rollen-statement_timeout (3 s) gilt bei set local role nicht und wird nicht angewandt; Aufrufe über 3 s wurden gezählt (keiner).
2.1 Zeiten je Fall (Endstand, n = 100 je Fall, 1400 Aufrufe, Host-Last 1,7 bis 2,2)
| Fall | p50 ms | p95 ms | p99 ms | max ms |
|---|---|---|---|---|
| 01 Titelteil (4 Zeichen) | 70,4 | 77,4 | 84,8 | 115,8 |
| 02 zwei Zeichen | 77,8 | 90,7 | 112,6 | 116,6 |
| 03 ganzes Wort | 70,4 | 75,8 | 78,6 | 82,8 |
| 04 zwei Wörter | 73,5 | 77,9 | 89,0 | 105,2 |
| 05 ohne Leerzeichen | 69,4 | 73,4 | 85,2 | 98,4 |
| 06 Verlag (ein Wort) | 69,8 | 73,1 | 81,9 | 91,8 |
| 06b Verlag + häufiges Wort | 76,6 | 81,3 | 86,6 | 147,7 |
| 07 alternativer Titel | 70,1 | 77,9 | 87,2 | 112,4 |
| 08 Begriff + Filter | 18,6 | 20,5 | 22,6 | 23,4 |
| 09 nur Filter (Spielerzahl, Genre) | 8,5 | 8,7 | 8,8 | 8,9 |
| 10 nur alle Filter | 10,7 | 11,3 | 12,8 | 12,8 |
| 11 ohne alles, erste Seite | 8,9 | 9,4 | 10,4 | 11,4 |
| 12 ohne alles, Versatz 5000 | 10,7 | 12,3 | 13,3 | 13,3 |
| 13 ohne Treffer | 66,3 | 72,3 | 93,7 | 101,3 |
| alle | 78,6 | 147,7 |
Budget p95 ≤ 150 ms: erfüllt. Frühere Läufe derselben Fassung (vor dem einmaligen Filter-Set): ruhiger Host gesamt 110 ms (je Fall 77 bis 131 ms); auf einem stark belasteten Host (Last 8 bis 15) je Textfall 190 bis 325 ms, gesamt 215 ms, also über Budget. Das Ergebnis hängt von der CPU-Verfügbarkeit ab. Die Fixkosten sind der Volltext-Scan über rund 30 000 Titel (Suche ohne Treffer: 66 bis 72 ms). Hetzner/Staging ist nicht gemessen.
2.2 Pläne (auto_explain, analyze, buffers, als anon mit RLS)
Das Skript gibt für alle 13 Fälle den Plan mit Zeilenzahlen und Buffers als NOTICE aus (log_timing aus, weil die Zeitmessung selbst die Zeiten stark verfälscht). Gesamtdauer der Anweisung und Buffers, sechs Fälle:
| Fall | Dauer der Anweisung | Treffer (Zeilen) | Buffers (shared hit) | Plan |
|---|---|---|---|---|
| 01 Titelteil | 47,7 ms | 30 | 10 102 | Custom Plan |
| 02 zwei Zeichen | 75,8 ms | 30 | 9 795 | Custom Plan |
| 06 Verlag | 75,1 ms | 11 | 177 098 | Generic Plan |
| 08 Begriff + Filter | 21,5 ms | 2 | 26 236 | Generic Plan |
| 10 alle Filter | 13,3 ms | 30 | 14 437 | Generic Plan |
| 13 ohne Treffer | 72,2 ms | 0 | 176 884 | Generic Plan |
Die Pläne zeigen Seq Scan auf games (View und Join) und auf game_alt_titles; die Trigramm-Indizes werden nicht genutzt (Zweizeichen-Begriffe, Regel ohne Leerzeichen, RLS-Prüfungen für anon), wie in Spec 3.2 Nr. 3 vorhergesagt. Die Generic Plans lesen mehr Buffers (RLS-Probe auf games je Verlagszeile), bleiben aber in der Zeit gleich.
3. Protokollprüfung (QUE-04)
Lokale Werte: log_min_duration_statement = -1, log_statement = ddl, log_parameter_max_length = -1, log_parameter_max_length_on_error = 0, pgaudit.log = none, pgaudit.log_parameter = off, auto_explain.log_min_duration = 10 s mit auto_explain.log_parameter_max_length = -1.
- POST über Kong: Ein Aufruf mit dem Marker
geheimerbegriff9z(Erfolg und Fehlersearch_query_too_long,search_paging_invalid) steht weder im Datenbank- noch im PostgREST-Protokoll. Fehler stehen mit dem Platzhalter$1im Protokoll (STATEMENT: WITH pgrst_source AS (SELECT $1 AS json_data) …), weillog_parameter_max_length_on_error = 0. - GET über Kong (Befund der Review, am 2026-11-04 bestätigt):
GET /rest/v1/rpc/search_catalog_games?p_query=markergetxyzliefert 200 und steht im Zugriffsprotokoll des Gateways (Kong); das POST-Gegenstück (markerpostxyz) nicht. Datenbank-Log in beiden Fällen ohne Marker. Die App nutzt POST; der GET-Weg muss im Proxy gesperrt werden (Release-Checkliste 2a). - Abhängigkeit: Suchbegriffe bleiben nur deshalb aus dem
auto_explain-Log, weil die Rollen-Zeitlimits (3 s, 8 s) kürzer als 10 s sind und abgebrochene Anweisungen keineauto_explain-Ausgabe erzeugen.log_parameter_max_length_on_error,auto_explain.log_min_durationund die Rollen-Zeitlimits müssen zusammen geprüft werden (pgTAP prüft die lokalen Annahmen; Runbookdocs/katalog-pflege.md8.2). - Hetzner/Staging nicht geprüft.
4. Mutationsprüfung
Mutationen in einer Kopie der Testdatei (Rollback), nicht am echten Schema: 103 von 107 SQL-Mutationen (RPC, search_normalize, Rechte, Tabelle, Check, Indizes) erkannt; Überlebende: ID-Tie-Break innen und außen (der Upstream-Plan liefert bereits ID-Reihenfolge), beide collate "C"-Mutationen (lokal ICU-Sortierung, für [a-z0-9 ] gleich). Fixtures: 4 von 5 (überlebt: Rohtitel-Vergleich, gleicher Endzustand durch die Semantik datenändernder CTEs). Deno-Werkzeug: 17 von 17.
5. PR 3c (E2E und vorläufige Latenzmessung), Stand 2026-11-04
Branch feature/3c-katalog-suche-e2e, Refs #3. Ergänzt § 7.2/7.3 der Spec; die Spec selbst wird nicht geändert.
5.1 Was gebaut ist
- Web-E2E
e2e/tests/web/kat01-suche.spec.ts(17 Tests, ohne Anmeldung): Einstieg über die Schaltfläche „Search games“ auf der Startseite, Titel in anderer Schreibweise (Krit. 1), Relevanzreihenfolge, ausgeblendete/zusammengeführte Spiele (KAT-05 Krit. 4), alternative Titel de/en (Krit. 2), Verlage inkl. zweitem Verlag (Krit. 3), Filter Genre/Spielerzahl/Dauer/Alter (Krit. 4), nur Filter und UND-Verknüpfung (Krit. 8), Filter-Sheet im mittleren Layout, Nachladen über Scrollen mit Prüfung der Anfragep_offset 30(Krit. 5), Leerzustände (Krit. 6, Krit. 8), URL-Zustand, Neuladen, Zurück in einem Schritt, Deep Link mit Filtern, ungültige URL-Parameter, Offline mit „Try again“. - API-E2E
e2e/tests/api/kat01-suche.spec.ts(18 Tests):search_catalog_gamesalsanonüber Kong (Vertrag, Normalisierung, Relevanzstufen 0 bis 4, alternative Titel, Verlage, Filter, Paging,total_count), nie sichtbar: ausgeblendete, zusammengeführte und private Spiele (auch nicht für den Besitzer), Fehlerfällesearch_query_too_long,search_filter_invalid,search_paging_invalid(P0001, HTTP 400), 22003 bei smallint-Überlauf, 22P02 bei falscher UUID,service_roleohne Ausführungsrecht (42501), kein Schreiben aufgame_alt_titles. - Testdaten
e2e/support/catalog.ts: je Spec-Datei ein eigenes Datenset mit eigenem UUID-Präfix und Kunstwort (e2ec0001-/zqvkweb,e2ec0002-/wlpdapi), Anlage alspostgresimbeforeAll, Aufräumen imafterAll(der API-Test prüft, dass nichts zurückbleibt);seedlöscht zuerst und ist nach einem Abbruch idempotent. Die Dateien laufen intern seriell und untereinander parallel. Eigene Genres/Mechaniken statt des Vokabulars, damit „nur Filter“-Tests unabhängig vom Startbestand sind. - Latenzmessung (Projekt
perf, nur mitPERF_WEB_URL, nie ine2e-run.sh/CI):e2e/tests/perf/kat01-suche-latenz.spec.ts, Ladeskriptsupabase/perf/catalog_search_perf_load.sql(committet, mit Präfix9e4f0000-) und…_unload.sql. Bedienung:e2e/README.md. playwright.config.ts:perfbedingt registriert; mitPERF_ONLY=1kein Web-Server und kein Import der lokalen Auth-Konstanten. Ohne die Variablen verhält sich die Konfiguration wie vorher. Die allgemeinen Flutter-Helfer liegen jetzt insupport/flutter.ts(support/web.tsexportiert sie unverändert weiter).
5.2 Messung KAT-01 Krit. 7 (vorläufig, Option A)
Lauf am 2026-11-04, lokaler Stack (Supabase CLI auf Docker/macOS), Web-Build --release, Chrome (Playwright-Chromium, Desktop), CDP-Drosselung 100 ms Latenz, 10 Mbit/s je Richtung. Daten: Startbestand-Stand der lokalen DB (1 Spiel) plus synthetisch 10 000 Katalogspiele, 20 000 alternative Titel, 2 000 Verlage, 12 951 Verlagszuordnungen, je Spiel 2 Genres und 3 Mechaniken (wie catalog_search_perf.sql).
| Wert | |
|---|---|
| Suchen gemessen | 116 (1 Erststart mit neuer Verbindung, 110 inhaltliche Suchen, 5 ohne Treffer) |
| p50 | 441 ms |
| p95 | 461 ms |
| Maximum | 637 ms (der Erststart; ohne ihn 535 ms) |
| Minimum | 404 ms |
| Grenze Krit. 7 | p95 ≤ 2000 ms: erfüllt |
Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag 454, GROSSBUCHSTABEN 441, alternativer Titel 437, ohne Treffer 447 ms. Der Wert besteht zu rund 300 ms aus der Entprellung der App; der Rest ist eine Round-Trip-Zeit (100 ms), Verarbeitung und Rendern. Gegenprobe des Gates: mit PERF_LATENCY_MS=1800 steigt der gemessene p95 auf 2157 ms (über der Grenze), das Gate reagiert also.
Aussagegrenzen (wichtig):
- Kein Staging, keine Produktion. Gemessen ist der lokale Stack auf demselben Rechner wie der Browser (Docker unter macOS); Rechenzeit von Datenbank, Gateway und Browser konkurriert miteinander und mit anderen Prozessen (auf dem Rechner lief gleichzeitig Backend-Arbeit mit eigenen Datenbank-Läufen). Die Staging-Messung bleibt Freigabebedingung in
docs/release-checkliste.md(Abschnitt 2a). - Die Drosselung ist eine Browser-Emulation (Chrome DevTools): sie verzögert Antworten und begrenzt den Durchsatz im Browser, bildet aber keine Funkstrecke, kein TLS-Handshake über echte Latenz, keine Paketverluste und keinen Wechsel zwischen Funkzellen ab. Die Latenz gilt je Anfrage; ob ein Verbindungsaufbau (TCP/TLS) mit 100 ms Round-Trip in der Emulation vollständig abgebildet ist, ist nicht geprüft.
- Nur Web (Desktop-Chrome, ungedrosselte CPU). Keine Stichprobe auf Android/iOS (Spec 7.3, Aufgabe C1, Chris), keine CPU-Drosselung für ein schwächeres Mobilgerät.
- Synthetische Daten: Wörter mit ungleicher Häufigkeit, aber kein echter Titelbestand; die Relevanz mit dem echten Startbestand bleibt C1. Der Verlag „zusammengesetzt“ entspricht nicht echten Verlagsnamen.
- Suchliste: feste Schrittweite durch die ersten 600 Spiele in alphabetischer Reihenfolge, nicht durch einen echten Nutzungsverlauf. Ein Treffer-Erwartungswert je Suche kommt aus derselben RPC (selbstprüfend, aber ohne unabhängiges Orakel).
- Kein Ratenlimit im lokalen Kong für die RPC (Entscheidung D1 vor dem Go-live): die Messung enthält keine Drosselung durch ein Gateway-Limit.
- Gemessen ist bis zur ersten sichtbaren Kachel (Textknoten im Semantics-Baum), nicht bis zum gezeichneten Frame; die Differenz ist nicht gemessen.
- Eine einzelne Messreihe (kein Vergleich über mehrere Läufe/Tageszeiten); Streuung zwischen zwei früheren Läufen mit 110 Suchen: p95 456 und 461 ms.
5.3 Befunde und offene Punkte
- Trefferkacheln im Semantics-Baum: Alle Kacheln liegen als Kinder eines einzigen Listen-Containers (
role=group); die Texte stehen inspan-Knoten. Der Container hat keinen eigenen Namen. Für Bildschirmleser ist das Verhalten nicht geprüft (NVDA/VoiceOver-Durchgang, manuell). Die Tests suchen die Kacheln als Textknoten. - Semantics-Eingabefeld: Ein direkt geladenes
/catalognimmt per Playwright getippten Text nicht verlässlich an (Feld bleibt leer; Autofokus ist mal gesetzt, mal nicht). Über die Startseite funktioniert es. Nicht untersucht, ob echte Nutzer (Mausklick) betroffen sind. Der Wert des Eingabefelds ist nach einem Auswahlmenü oder Neuladen im DOM leer, obwohl die Suche den Begriff behält (Beleg: URL und Ergebnis); die Tests prüfen deshalb die URL. - Ausgeblendete Spiele: Die Tabelle
game_alt_titlesist füranonauch für alternative Titel ausgeblendeter Katalogspiele lesbar (die RLS-Regel folgt der Sichtbarkeit ingames, nicht der Viewcatalog_games). Über die Suche erscheinen sie nicht. Ob das gewollt ist, ist fachlich zu entscheiden (nicht geraten). - Gateway ohne
apikey: Die lokale CLI beantwortet die RPC auch ohneapikey-Header alsanon(HTTP 200). Auf dem Server ist das eine Frage der Kong-Konfiguration (Release-Checkliste). - Krit. 6: „Spiel melden“ ist erst mit #13 sichtbar; der Test prüft den Hinweis, nicht die Aktion (Spec-F4).
- Nicht automatisiert: „Weitere laden“ per Tastatur (das automatische Nachladen kommt der Schaltfläche zuvor; Widget-Tests in 3b), Ansage der Trefferzahl für Bildschirmleser, Textskalierung 200 %, Tab-Reihenfolge im Browser.