Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-docker-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
version: "0.11.29"
version: "0.13.5"
enable-cache: false

- name: Install pinned CVE scanners
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
with:
# Pin jak w tests.yml (job `lock-check`): bez tego setup-uv bierze
# "latest", a `uv export` zaczyna zalezec od daty przebiegu.
version: "0.11.29"
version: "0.13.5"

- name: Export uv.lock -> requirements.txt (prod-only)
run: |
Expand Down Expand Up @@ -142,7 +142,7 @@ jobs:
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
# Pin jak wyzej — SBOM ma byc funkcja tresci repo, nie daty runu.
version: "0.11.29"
version: "0.13.5"

- name: Generate SBOM (CycloneDX, prod-only)
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
with:
# Pin jak w tests.yml (job `lock-check`) — sciezka wydawnicza tym
# bardziej nie moze zalezec od plywajacej wersji narzedzia.
version: "0.11.29"
version: "0.13.5"
enable-cache: true

- name: Wykryj gałąź wydania + odczytaj RC
Expand Down Expand Up @@ -153,7 +153,7 @@ jobs:
# na spojnosc locka z pyproject.toml — wiec jedyna zmiana, jaka
# moze wniesc, to ta jedna linia z wersja root-pakietu.
#
# uv jest przypiete na 0.11.29 w kroku setup-uv wyzej. Wersja ma
# uv jest przypiete na 0.13.5 w kroku setup-uv wyzej. Wersja ma
# znaczenie: `uv lock --check` na dev biegnie na tym samym pinie,
# a rozne uv potrafia wygenerowac rozny (choc rownowazny) lock.
uv lock
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/refresh-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
# Pin jak w tests.yml (job `lock-check`). Tu szczegolnie wazny:
# baseline commitowany do repo nie moze zalezec od tego, ktora
# wersja uv akurat byla "latest" w dniu przebiegu.
version: "0.11.29"
version: "0.13.5"

- name: Install system build deps
# uv sync builds several wheels from source (pycairo via
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
#
# Pin wersji jak w tests.yml (job `lock-check`): bez `version`
# setup-uv bierze "latest", czyniac przebieg funkcja daty.
version: "0.11.29"
version: "0.13.5"
enable-cache: false

- name: Bump RC (nowy cykl lub kolejne -rcN)
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
with:
# Patrz komentarz przy jobie `lock-check` — bez `version` setup-uv
# bierze "latest", co czyni CI niedeterministycznym.
version: "0.11.29"
version: "0.13.5"
enable-cache: true

- name: Determine diff base
Expand Down Expand Up @@ -157,8 +157,9 @@ jobs:
# repo: ten sam, bit-w-bit identyczny uv.lock przechodzil na uv
# 0.11.29 i padal na 0.11.31 ("lockfile needs to be updated"),
# wywracajac gate na branchach, ktore deps w ogole nie ruszaly.
# Wersje podnosimy SWIADOMIE, razem z przegenerowanym uv.lock.
version: "0.11.29"
# Wersje podnosimy SWIADOMIE, razem z przegenerowanym uv.lock
# (0.13.5: GHSA-hq85-5cpj-mr62, zaufany root CA emSign).
version: "0.13.5"
enable-cache: true
- name: Assert uv.lock matches pyproject.toml
run: uv lock --check
Expand Down Expand Up @@ -195,7 +196,7 @@ jobs:
# Patrz komentarz przy jobie `lock-check`. Tu dochodzi drugi powod:
# `uv sync --frozen` czyta uv.lock, wiec plywajaca wersja uv czyni
# srodowisko testowe niereprodukowalnym miedzy przebiegami.
version: "0.11.29"
version: "0.13.5"
enable-cache: false

- name: Install project deps
Expand Down
302 changes: 302 additions & 0 deletions HANDOFF-pomiar-wydajnosci-2026-08-07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# HANDOFF: pomiar czasów wydajności — ZAMKNIĘTY

**Data zlecenia:** 2026-08-07
**Data wykonania:** 2026-08-07 (MacBook Pro, cichy host)
**Gałąź:** `django-6.1`
**Status:** **ZROBIONE.** Czasy zmierzone, kryterium kontrolera spełnione.
Wynik niżej; oryginalna treść handoffu zostaje jako zapis procedury.

---

## WYNIK

Warunki: kopia bazy produkcyjnej (`db-backup-20260603-023000`, 68 355
autorów, 491 jednostek), wariant `bench_prod` (23 produkcyjne reguły
`CACHEOPS`), 25 powtórzeń × dwa niezależne przebiegi na stronę, sekwencja
`po → przed → po → przed`, `FLUSHDB` przed każdym przebiegiem. Dev-owy
stack `docker compose` zatrzymany na czas pomiaru.

### Kontroler — kryterium ważności SPEŁNIONE

`admin: źródło (changelist)`, 16 zapytań we **wszystkich czterech**
przebiegach; mediany 284,2 / 281,7 / 284,6 / 281,4 ms. Rozrzut między
przebiegami **3,2 ms = 1,1 %** mediany, **zero flag `SZUM`**. (Na
`mac-mini` ten sam kontroler skakał 320 → 420 ms, +31 %.)

Podłoga szumu wyznaczona dodatkowo z **dziesięciu** niezmienionych
scenariuszy: dla stron > 100 ms żaden nie odchylił się o więcej niż
**3,5 %**; na stronach kilkudziesięciomilisekundowych sięga 9 %.

### Czasy, para `ee5e81a35` → `ccb9756ff`

| scenariusz | zapytań | przed [ms] | po [ms] | różnica |
|---|---|---|---|---|
| publiczne: lista jednostek | 514→3 | 196,2 / 182,0 | 26,7 / 29,1 | **−161 ms (−85 %)** |
| admin: autor (changelist) | 27→27 | 788,4 / 698,7 | 237,2 / 240,3 | **−505 ms (−68 %)** |
| admin: jednostka (changelist) | 66→17 | 240,2 / 199,1 | 131,7 / 123,7 | **−92 ms (−42 %)** |
| admin: wyd. zwarte (changelist) | 220→36 | 246,6 / 249,8 | 210,8 / 208,0 | **−39 ms (−16 %)** |
| admin: wyd. ciągłe (changelist) | 190→34 | 233,4 / 233,1 | 219,5 / 207,4 | −20 ms (−8,5 %) |

Wszystkie powyżej progu 3,5 %; `wyd. ciągłe` najbliżej i najmniej pewna.
Liczby zapytań odtworzyły się co do jednego względem tych z handoffu.

### Ile z tego wymaga Django 6.1 — prawie nic

Trzeci punkt pomiarowy (czubek `dev`, Django 5.2.16; zbiory migracji
`dev` i `django-6.1` są identyczne, więc obsłużyła go ta sama baza):

| scenariusz | `ee5e81a35` | `dev` 5.2 | 6.1 + PEERS | `dev` + PR #738 (5.2) |
|---|---|---|---|---|
| publiczne: lista jednostek | 514 | **3** | 3 | 3 |
| admin: wyd. ciągłe | 190 | **34** | 34 | 34 |
| admin: wyd. zwarte | 220 | 70 | 36 | **35** |
| admin: jednostka | 66 | 66 | 17 | **16** |
| admin: źródło (kontroler) | 16 | 16 | 16 | 16 |

Wnioski:

1. Dwie wygrane (`lista jednostek`, `wyd. ciągłe`) były **już na 5.2** —
dają je poprawki z `dev`, `FETCH_PEERS` nie dokłada tam nic.
2. Spadek czasu `admin: autor` 788 → ~250 ms daje commit `5ffc83f50`
z `dev`; `FETCH_PEERS` dokłada już tylko ~250 → 238 ms. Czyli i ta
wygrana jest **w praktyce cała na 5.2**.
3. Pozostałe dwie (`jednostka`, `wyd. zwarte`) da się na 5.2 zrobić
jawnym `select_related` — i wychodzi **o jedno zapytanie lepiej** niż
`FETCH_PEERS`, bo ten musi dorzucić zapytanie hurtowe na relację.
Zrobione w **PR #738** (do `dev`).

Czyli: **sam upgrade do 6.1 nie kupuje wydajności, a `FETCH_PEERS` daje
mniej, niż wyglądało.** Jego wartością zostaje bycie siatką bezpieczeństwa
na relacje, których nikt nie zadeklarował, oraz `FETCH_RAISE` jako
detektor N+1 (PR #736).

### Znalezisko poboczne

Na każdej changelistcie admina siedzi stałe **13 zapytań** aplikacji
`dynamic_admin_columns` (8 × `dynamic_columns_modeladmin` + 5 ×
`dynamic_columns_modeladmincolumn`), w 5.2 i w 6.1 jednakowo.
`FETCH_PEERS` tego nie rusza. Przy changelistcie autorów to prawie połowa
z 27 zapytań — jedno miejsce, zysk na wszystkich changelistach.

### PUŁAPKA, której nie było w oryginalnym handoffie

`bench.py` ustawia porty kontenerów przez `os.environ.setdefault()`:

```python
os.environ.setdefault("DJANGO_BPP_DB_PORT", "55432")
os.environ.setdefault("DJANGO_BPP_REDIS_PORT", "55379")
```

`setdefault` **nie nadpisuje** zmiennej już ustawionej. Jeśli profil
shella eksportuje `DJANGO_BPP_DB_PORT=5432` / `DJANGO_BPP_REDIS_PORT=6379`
(typowe przy dev-owym `docker compose`), benchmark **po cichu uderza
w dev-owe kontenery** zamiast w odtworzony dump. Bez błędu, bez
ostrzeżenia, z wiarygodnie wyglądającym wynikiem. To samo dotyczy
`manage.py migrate` uruchomionego z tym settings-modułem — domigruje
cudzą bazę.

Kontrola: przed pomiarem wypisz `settings.DATABASES["default"]["PORT"]` —
ma być `55432`. Druga kontrola: liczby zapytań muszą odtworzyć `514`,
`190`, `220`, `66` (przed) i `3`, `34`, `36`, `17` (po); na dev-owej bazie
się nie odtworzą. Obejście: **eksportuj** porty zamiast polegać na
`setdefault`.

---

## Co jest ZROBIONE (nie powtarzaj)

Audyt Django 6.1 pod kątem wydajności + cztery naprawione defekty.
Pełny raport: `AUDYT-DJANGO-6.1-WYDAJNOSC.md` (nietrackowany, w korzeniu
worktree `bpp-django-6.1`).

### Ustalenia, które są już pewne

1. **Sam upgrade do Django 6.1 nie przyspiesza niczego.** Liczby zapytań
bit w bit identyczne jak na 5.2 we wszystkich 15 scenariuszach, w obu
wariantach konfiguracji cache. Zero regresji — i zero zysku.
2. Zysk wymaga jawnego opt-inu w *fetch modes*
(`QuerySet.fetch_mode(FETCH_PEERS)`).
3. Najcenniejsze w 6.1 jest `FETCH_RAISE`/własny `FetchMode` jako
**detektor N+1**. Tym mechanizmem znalazłem cztery defekty, których nie
widać w kodzie.

### Wdrożone poprawki

Na `dev` (Django 5.2, wypchnięte):

| commit | co |
|---|---|
| `f509c7bdc` | filtr „Wydział" na `/admin/bpp/autor/` listował **504 jednostki zamiast 7** (bug UI, nie tylko koszt) |
| `5ffc83f50` | `JednostkaFilter` bez `select_related("uczelnia")`; `LogEntryFilterBase.only()` bez `last_name`/`first_name` |
| `6416e2ac0` | indeks jednostek: `.count()` na relacji odwrotnej 3–4× na wiersz → `annotate()` |

Na `django-6.1`:

| commit | co |
|---|---|
| `ccb9756ff` | PR #733 (squash): `FETCH_PEERS` w `BaseBppAdminMixin` — **wymaga 6.1** |

### Zmierzone liczby zapytań (pewne, powtórzone w 5 przebiegach)

Konfiguracja z produkcyjnymi regułami `CACHEOPS`:

```
scenariusz przed po
publiczne: lista jednostek 514 3
admin: wyd. ciągłe (changelist) 190 34
admin: wyd. zwarte (changelist) 220 36
admin: jednostka (changelist) 66 17
admin: autor (changelist) 27 27 (spadły round-tripy do Redisa, nie SQL)
admin: źródło (changelist) 16 16 ← KONTROLER, nietknięty
pozostałe 9 scenariuszy bez zmian
```

### Testy

* `django-6.1` (po mergu #733): `1016 passed, 1 skipped`
* `dev` (Django 5.2): `1012 passed, 1 skipped`
(różnica = 4 testy `test_fetch_peers.py`, które żyją tylko na 6.1)

Zakres: `src/bpp/tests/test_admin/` + `src/bpp/tests/test_views/`.

---

## Co ZOSTAŁO do zrobienia

**Jedno zadanie: zmierzyć czasy odpowiedzi na cichym hoście.**

Dotychczasowe pomiary czasu są niewiarygodne i **nie wolno ich cytować jako
wyniku**. Dowód: scenariusz kontrolny `admin: źródło (changelist)` ma te
same 16 zapytań przed i po wszystkich zmianach (żadna go nie dotyka), a
zmierzony czas skakał **320 → 420 ms**. Skoro niezmieniona ścieżka kodu daje
+100 ms, to różnice czasowe pochodzą z obciążenia hosta, nie z kodu. Prawie
każdy pomiar dostawał flagę `SZUM` (odchylenie > 10% mediany).

Liczby czasowe, które trafiły do wiadomości commitów i do opisu PR #733
(np. „185 → 120 ms"), pochodzą ze spokojniejszego okna i są **rzędem
wielkości, nie pomiarem** — tak też je tam opisałem. Po czystym pomiarze
warto je zastąpić albo potwierdzić.

### Kryterium sukcesu

Dla każdego scenariusza podać medianę i odchylenie z >= 25 powtórzeń, przy
czym:

* **kontroler `admin: źródło (changelist)` musi wypaść stabilnie** (rozrzut
< 10% mediany, brak flagi `SZUM`) — inaczej cały przebieg jest do
wyrzucenia,
* różnicę uznajemy za realną tylko wtedy, gdy jest **większa niż rozrzut
kontrolera**.

---

## Warunki wstępne na drugim komputerze

1. **Docker** (testcontainers + kontenery benchmarkowe).
2. **Cichy host** — żadnych równoległych suit testowych, `run-site` ani
innych sesji agenta. To jest cały powód tego handoffu.
3. **Dump bazy produkcyjnej.** To jedyna rzecz, której NIE ma w repo:
`db-backup-20260603-023000.tar.gz`, ~772 MB, leży na
`/Volumes/SSD/` na `mac-mini` (symlink z `~`). Trzeba go przekopiować,
np. przez udział SMB `mpasternak` albo `rsync`. Bez niego nie ma pomiaru
— świeża baza nie pokaże N+1 (ma pojedyncze wiersze zamiast 504
jednostek i 68 tys. autorów).
Alternatywa: mniejszy `db-backup-20260428-093811.pg_dump` (~48 MB),
ale ma inny wolumen danych, więc liczby nie będą porównywalne z tymi
wyżej.

---

## Procedura

Cała mechanika jest opisana w `docs/deweloper/benchmark-orm.md` — poniżej
skrót właściwy dla tego zadania.

```bash
git clone git@github.com:iplweb/bpp.git && cd bpp
git checkout django-6.1
uv sync

# 1. Kontenery (nietypowe porty, żeby nie kolidować z niczym)
docker run -d --name bpp-bench-pg -p 55432:5432 \
-e POSTGRES_USER=bpp -e POSTGRES_PASSWORD=password -e POSTGRES_DB=bpp \
--shm-size=1g iplweb/bpp_dbserver:psql-16.13 \
-c shared_buffers=1GB -c work_mem=64MB -c maintenance_work_mem=512MB
docker run -d --name bpp-bench-redis -p 55379:6379 redis:7-alpine

# 2. Baza (dump to format KATALOGOWY pg_dump -Fd w tarze, nie .sql!)
gzip -dc db-backup-20260603-023000.tar.gz | tar -xf -
PGPASSWORD=password pg_restore -h localhost -p 55432 -U bpp -d bpp \
--no-owner --no-privileges --no-comments -j 6 db-backup-20260603-023000/
DJANGO_SETTINGS_MODULE=django_bpp.settings.bench \
uv run python src/manage.py migrate --noinput

# 3. Pomiar PO zmianach (czubek django-6.1)
docker exec bpp-bench-redis redis-cli -n 7 FLUSHDB
DJANGO_SETTINGS_MODULE=django_bpp.settings.bench_prod \
uv run python bench/bench_orm.py --pomiar --powtorzenia 25 > /tmp/po.txt

# 4. Pomiar PRZED zmianami — commit-rodzic squasha #733.
# Harness trzeba przenieść, bo w ee5e81a35 jeszcze nie istniał:
git checkout ee5e81a35
git checkout django-6.1 -- bench/ src/django_bpp/settings/bench.py \
src/django_bpp/settings/bench_prod.py
docker exec bpp-bench-redis redis-cli -n 7 FLUSHDB
DJANGO_SETTINGS_MODULE=django_bpp.settings.bench_prod \
uv run python bench/bench_orm.py --pomiar --powtorzenia 25 > /tmp/przed.txt
git checkout . && git checkout django-6.1
```

**UWAGA na `ee5e81a35`:** to stan przed CAŁĄ pracą wydajnościową na linii
6.1, czyli bez `FETCH_PEERS` — ale też bez poprawek z `dev` (A/C/D/B),
bo #733 wszedł jako squash i wciągnął je razem ze sobą. Czyli para
`ee5e81a35` vs `ccb9756ff` mierzy **efekt łączny wszystkich czterech
poprawek + FETCH_PEERS**, co jest dokładnie tym, co chcemy pokazać.

Jeśli chcesz rozbić wkład `FETCH_PEERS` osobno od poprawek A/C/D/B, użyj
`--tryb peers` na czubku `dev` (Django 5.2 nie ma fetch modes, więc to
trzeba zrobić na 6.1 — zob. „Ograniczenia" w dokumentacji harnessu).

### Czyszczenie po pomiarze

```bash
docker rm -f bpp-bench-pg bpp-bench-redis
```

---

## Pułapki, w które już wpadłem (nie powtarzaj)

1. **`grep -c` bez mianownika kłamie.** `grep -c "^\[ \]"` zwrócił `0`
(„brak zaległych migracji"), a komenda po prostu **wywaliła się
tracebackiem** i nie wypisała nic. Zawsze licz też mianownik.
2. **`cmd > log 2>&1; echo "exit=$?"` w potoku mierzy exit ostatniego
członu.** Dwa razy uwierzyłem w „exit 0", gdy `migrate` padał.
Zapisuj kod wyjścia JAWNIE do pliku z logiem.
3. **`pytest ... | tail -25` gubi podsumowanie**, bo logi zamykanych
kontenerów testcontainers wychodzą PO nim. Zapisuj pełny output do
pliku i grepuj.
4. **Cacheops pikluje instancje modeli.** Pomiar 6.1 czytający wpisy
zapisane przez 5.2 daje `RuntimeWarning` i nieważne wyniki. `FLUSHDB`
przy każdej zmianie wersji.
5. **`constance` odrzuca LocMemCache** (`ImproperlyConfigured`) — dlatego
`bench.py` trzyma `constance_cache` na Redisie.
6. **Nie wyrzucaj `easyaudit` z `INSTALLED_APPS`** — `bpp.0470` deklaruje
zależność migracji od `easyaudit.0001_initial`, więc graf migracji się
nie zbuduje. Wystarczy zgasić middleware i hooki sygnałów (tak robi
`bench.py`).
7. **Nie ubijaj kontenerów wzorcem na współdzielonym hoście.** Przy
sprzątaniu dwa razy trafiły mi na listę „sierot" CUDZE, żywe przebiegi
pytest — bo w trakcie detekcji startował kolejny Ryuk. Selekcjonuj po
nazwie/ID i wypisz listę PRZED usunięciem.

---

## Stan repozytorium

* `dev` — wypchnięte, `6416e2ac0`
* `django-6.1` — wypchnięte, `ccb9756ff` (+ ten handoff i `bench/`)
* PR #733 — **zmergowany**
* PR #731 (Django 6.1 → dev) — otwarty
* `AUDYT-DJANGO-6.1-WYDAJNOSC.md` — nietrackowany, tylko na `mac-mini`;
jeśli potrzebny na drugiej maszynie, skopiuj razem z dumpem
Loading
Loading