fix(api): bez planow ogolnych i szybkie listy serwisow, ktore stoja
Some checks failed
Backend tests / test (push) Has been cancelled

- db: prepare_threshold=None. psycopg przygotowywal zapytanie po 5 wykonaniach na
  polaczeniu, a Postgres przechodzil potem na plan ogolny. Lista duzego tagu na takim
  planie: "pov" 44 s zamiast 0,46 s, "masturbation" 46 s zamiast 0,18 s. Stad listy
  ladowaly sie raz szybko, raz minute, zaleznie od polaczenia z puli (tez GOON-73/75).
- Lista jednego serwisu bez frazy: przejscie od najnowszych z limitem 1 s, a po nim
  start od scen serwisu (do 150 tys.). Serwis, ktory stoi, mial sceny gleboko:
  superporn 72-92 s -> 1,7 s, yespornvip 15 s -> 1,5 s, hentaiplay 9 s -> 0,06 s.

Porownanie ze starym kodem na 88 listach serwisow: wyniki identyczne przy
zapytaniach puszczonych po sobie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-23 09:45:05 +02:00
parent 3349700f72
commit 2a2c0d7e9d
2 changed files with 42 additions and 8 deletions

View file

@ -251,6 +251,16 @@ _ORIGIN_DRIVEN_MAX_SCENES = 60_000
# a przy sort=studio przejscie po studiach szuka trafien w kazdym z 33 tys. studiow # a przy sort=studio przejscie po studiach szuka trafien w kazdym z 33 tys. studiow
# ("sunny leone" 15 min), podczas gdy trigramy + sort trafien to 0,37 s. # ("sunny leone" 15 min), podczas gdy trigramy + sort trafien to 0,37 s.
_RARE_PHRASE_HITS = 4 _RARE_PHRASE_HITS = 4
# Lista jednego serwisu bez frazy (ekran serwisu w apce). Planer idzie od najnowszych scen
# calego katalogu i sprawdza, czy scena jest z tego serwisu. Dla serwisu, ktory stoi,
# jego sceny sa gleboko: superporn (37 tys., ostatnia 20.08) 92 s na strone 1, bo
# przejscie mijalo 1,1 mln nowszych scen; yespornvip 23 s (pomiar 2026-09-23). Dlatego
# przejscie dostaje limit czasu, a po nim lista zaczyna od scen serwisu. Limit 1 s, bo
# u zywych srednich serwisow przejscie trwa ok. 1 s (hdporngg), a start od scen serwisu
# kosztuje wtedy wiecej (5 s na zimno). Powyzej 150 tys. scen (porndoe 279 tys.) start
# od serwisu jest wolniejszy niz przejscie, wiec zostaje zwykly plan.
_ORIGIN_LIST_WALK_BUDGET_MS = 1000
_ORIGIN_LIST_MAX_SCENES = 150_000
_TRGM_RUN_RE = re.compile(r"[^\W_]{3}") _TRGM_RUN_RE = re.compile(r"[^\W_]{3}")
_TITLE_SAMPLE_TTL = 3600.0 _TITLE_SAMPLE_TTL = 3600.0
_TITLE_SAMPLE: dict = {"ts": 0.0, "vals": ()} _TITLE_SAMPLE: dict = {"ts": 0.0, "vals": ()}
@ -334,10 +344,7 @@ def _search_walk_rows(
if ids is None: if ids is None:
# Bez trigramu zwykly plan czyta caly indeks (44 s), wiec lepiej pusto. # Bez trigramu zwykly plan czyta caly indeks (44 s), wiec lepiej pusto.
return None if budget == _SEARCH_WALK_BUDGET_MS else [] return None if budget == _SEARCH_WALK_BUDGET_MS else []
if not ids: return _scenes_in_order(session, ids)
return []
by_id = {s.id: s for s in session.execute(select(Scene).where(Scene.id.in_(ids))).scalars()}
return [by_id[i] for i in ids if i in by_id]
def _rare_title_phrase(q: str, sample) -> bool: def _rare_title_phrase(q: str, sample) -> bool:
@ -356,7 +363,9 @@ def _origin_driven_worth(q: str, sample) -> bool:
return not _rare_title_phrase(q, sample) return not _rare_title_phrase(q, sample)
def _origin_scene_ids(session: Session, origin: str) -> list[uuid.UUID] | None: def _origin_scene_ids(
session: Session, origin: str, max_scenes: int = _ORIGIN_DRIVEN_MAX_SCENES
) -> list[uuid.UUID] | None:
"""Id scen z zywym zrodlem serwisu albo None, gdy serwis jest za duzy na te sciezke. """Id scen z zywym zrodlem serwisu albo None, gdy serwis jest za duzy na te sciezke.
Wielkosc bierzemy z source_stats (liczone co 6 h), zeby dla xvideos nie ciagnac Wielkosc bierzemy z source_stats (liczone co 6 h), zeby dla xvideos nie ciagnac
@ -365,17 +374,25 @@ def _origin_scene_ids(session: Session, origin: str) -> list[uuid.UUID] | None:
from app.models.source_stats import SourceStats from app.models.source_stats import SourceStats
size = session.execute(select(SourceStats.scenes).where(SourceStats.origin == origin)).scalar() size = session.execute(select(SourceStats.scenes).where(SourceStats.origin == origin)).scalar()
if size is not None and size > _ORIGIN_DRIVEN_MAX_SCENES: if size is not None and size > max_scenes:
return None return None
ids = list( ids = list(
session.execute( session.execute(
select(PlaybackSource.scene_id) select(PlaybackSource.scene_id)
.where(PlaybackSource.origin == origin, PlaybackSource.dead_at.is_(None)) .where(PlaybackSource.origin == origin, PlaybackSource.dead_at.is_(None))
.distinct() .distinct()
.limit(_ORIGIN_DRIVEN_MAX_SCENES + 1) .limit(max_scenes + 1)
).scalars() ).scalars()
) )
return ids if len(ids) <= _ORIGIN_DRIVEN_MAX_SCENES else None return ids if len(ids) <= max_scenes else None
def _scenes_in_order(session: Session, ids: list[uuid.UUID]) -> list[Scene]:
"""Pelne wiersze scen dla listy id, w kolejnosci tej listy."""
if not ids:
return []
by_id = {s.id: s for s in session.execute(select(Scene).where(Scene.id.in_(ids))).scalars()}
return [by_id[i] for i in ids if i in by_id]
def _split_csv(raw: str | None) -> list[str]: def _split_csv(raw: str | None) -> list[str]:
@ -1037,6 +1054,17 @@ def list_scenes(
Scene.id Scene.id
== any_(bindparam("origin_scene_ids", origin_ids, type_=ARRAY(PG_UUID(as_uuid=True)))) == any_(bindparam("origin_scene_ids", origin_ids, type_=ARRAY(PG_UUID(as_uuid=True))))
) )
if rows is None and origin and not q and sort in ("created_at", "release_date"):
walked = _search_walk_ids(session, ordered, _off, _want, _ORIGIN_LIST_WALK_BUDGET_MS)
if walked is not None:
rows = _scenes_in_order(session, walked)
else:
origin_ids = _origin_scene_ids(session, origin, _ORIGIN_LIST_MAX_SCENES)
if origin_ids is not None:
ordered = ordered.where(
Scene.id
== any_(bindparam("origin_scene_ids", origin_ids, type_=ARRAY(PG_UUID(as_uuid=True))))
)
if rows is None and origin_ids is None and q and ( if rows is None and origin_ids is None and q and (
sort == "created_at" or (sort == "release_date" and tag_sort_id is None) sort == "created_at" or (sort == "release_date" and tag_sort_id is None)
): ):

View file

@ -8,10 +8,16 @@ from app.config import get_settings
_settings = get_settings() _settings = get_settings()
# prepare_threshold=None: psycopg domyslnie przygotowuje zapytanie po 5 wykonaniach na
# polaczeniu, a Postgres po kolejnych 5 potrafi przejsc na plan ogolny, niezalezny od
# parametrow. Dla list tagu to katastrofa: plan dobry dla malego tagu na duzym ("pov")
# trwal 44 s zamiast 0,46 s (pomiar 2026-09-23), wiec lista ladowala sie raz szybko, raz
# minute, zaleznie od tego, ktore polaczenie z puli trafilo. Planowanie od nowa to kilka ms.
engine = create_engine( engine = create_engine(
_settings.database_url, _settings.database_url,
pool_pre_ping=True, pool_pre_ping=True,
future=True, future=True,
connect_args={"prepare_threshold": None},
) )
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False, future=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False, future=True)