
# Testy wydajnościowe

Zestaw testów wydajności zapisuje stabilne JSON oraz czytelny dla człowieka raport. Znajduje się
w `bench/` w repozytorium [storh](https://github.com/inline0/storh) i nie jest
częścią pakietu Composer od wersji 0.5.2, dlatego poniższe
polecenia `composer bench*` można uruchomić tylko z klonu repozytorium, po `composer install`:

```bash
composer bench
composer bench -- --dataset=100000 --engine=doc
composer bench -- --dataset=100000 --engine=log
composer bench -- --dataset=100000 --engine=queue
composer bench -- --dataset=100000 --engine=cache --cache-validation=trust
composer bench -- --dataset=100000 --engine=filter
composer bench -- --dataset=1000000 --engine=uuid
composer bench:jit -- --dataset=1000000 --engine=uuid
composer bench:repeat -- --dataset=100000 --engine=doc --repeat=5
composer bench:repeat -- --dataset=1000000 --engine=filter --repeat=5 --memory-limit=512M
composer bench:range -- --datasets=1000,10000,50000,100000
composer bench:range -- --datasets=100000 --engines=cache --memory-limit=512M
composer bench:compare build/bench-main.json build/bench-current.json
composer bench:gate -- build/bench-main.json build/bench-current.json --threshold=10 --metric=doc.put --metric=log.stream
composer bench:ci
```

## Wyniki referencyjne

Mediany z trzech pełnych przebiegów zestawu testów, po 50,000 rekordów na silnik, na
Apple M1 Pro (16 GB, macOS 26.5, APFS) z PHP 8.5 CLI, opcache i JIT wyłączonymi:

```bash
composer bench:repeat -- --dataset=50000 --engine=all --repeat=3 --memory-limit=512M
```

Każdy zapis jest opróżniany z bufora i synchronizowany przez fsync, zanim storh zgłosi, że został zapisany, więc szybkość zapisu zależy od systemu plików. Odczyty korzystają z pamięci podręcznej stron systemu operacyjnego. Szybkości obliczono na podstawie median; uruchom `composer bench` z klonu repozytorium, aby sprawdzić działanie na własnym sprzęcie.

| DocStore, 50 tys. rekordów                    | mediana         |
| ---------------------------------------------- | --------------- |
| `put()`, jeden trwały plik na rekord            | 3.3 tys. rekordów/s |
| masowe pobieranie `putStream()`                      | 3.7 tys. rekordów/s |
| `importJsonl()`                                         | 3.6 tys. rekordów/s |
| odczyt punktowy `get()`, z weryfikacją STAT     | 5.6 µs          |
| ponowne otwarcie istniejącego magazynu         | 0.26 ms         |
| indeksowane zapytanie równościowe, `limit(100)`      | 1.1 ms          |
| indeksowane `count()` w całej kolekcji            | 41 µs           |
| budowanie indeksu, 2 pola równościowe + 1 zakresu | 52 tys. rekordów/s |
| pełne `stream()` z ponowną weryfikacją STAT        | 103 tys. rekordów/s |
| `exportJsonl()`                                         | 104 tys. rekordów/s |

| SegmentedLog, 50 tys. rekordów, segmenty 16 KB | mediana         |
| ---------------------------------------------- | --------------- |
| `put()`, fsync przy każdym dopisaniu             | 16 tys. dopisań/s |
| masowe pobieranie `appendStream()`                       | 56 tys. rekordów/s |
| odczyt kursorem, 100 rekordów od środka       | 2.1 ms          |
| odczyt z zakresu czasu                         | 1.1 ms          |
| indeks równościowy `count()`                      | 11 µs           |
| `compact()` wszystkich zamkniętych segmentów        | 69 tys. rekordów/s |
| ponowne otwarcie z odzyskiwaniem niekompletnego końca | 218 tys. rekordów/s |

Test wydajności zamyka segment co 16 KB, aby obciążyć rotację segmentów; domyślny rozmiar segmentu to 1 MiB.

| Queue, 50 tys. zadań                          | mediana         |
| ---------------------------------------------- | --------------- |
| `enqueue()`, fsync przy każdym zdarzeniu             | 22 tys. zadań/s |
| `claim()`                                         | 23 tys. zadań/s |
| `complete()`                                         | 23 tys. zadań/s |
| `enqueueMany()`                                         | 209 tys. zadań/s |
| `claimMany()`                                         | 467 tys. zadań/s |
| `completeMany()`                                         | 512 tys. zadań/s |

| SQL Mirror, SQLite, 50 tys. rekordów          | mediana         |
| ---------------------------------------------- | --------------- |
| początkowe `push()`                             | 61 tys. wierszy/s |
| `push()` bez żadnych zmian                       | 86 tys. rekordów/s |
| `flush()`, identyfikatory 100                  | 7.1 ms          |
| indeksowany SQL `COUNT` w całym mirrorze       | 6.9 ms          |
| `rebuild()`                                         | 65 tys. wierszy/s |
| przywracanie `pull()`, jeden trwały plik na rekord | 3.2 tys. rekordów/s |

| Micro                                           | mediana         |
| ---------------------------------------------- | --------------- |
| buforowane `get()`, zimne, a następnie ciepłe (MemoryCache, STAT) | 83 µs / 6.3 µs |
| generowanie UUIDv7                              | 1.3 µs          |
| weryfikacja UUIDv7                              | 0.30 µs         |
| filtrowanie predykatów w pamięci                | 4.4 mln wierszy/s |

Objęte testami cele:

- DocStore put, put strumieniowe, JSONL eksport/import, get, delete, stream i budowanie indeksu
- zapytania DocStore z indeksami równościowymi, złożonymi i zakresowymi, uporządkowanymi zakresami rosnącymi i malejącymi, zliczaniem indeksowanym, wyszukiwaniem po ID oraz zapytania bez indeksu
- SegmentedLog append, append strumieniowe, odczyty kursorem, odczyty zakresu czasu, odczyty skompaktowanego zakresu czasu, wyszukiwanie kursorem i po ID przez QueryBuilder, zliczanie, zliczanie masowych zapisów, statystyki, kompaktowanie
- Queue enqueue, masowe enqueue, claim, masowe claim, complete, masowe complete, requeue
- filtrowanie predykatami RecordQuery i QueryCondition wierszy przechowywanych w pamięci
- UUIDv7 generowanie monotoniczne, generowanie z rozłożeniem znaczników czasu, walidacja i wyodrębnianie znaczników czasu
- odzyskiwanie po urwanej końcowej linii
- odczyty z zimnej i ciepłej pamięci podręcznej z walidacją przez hash, stat lub zaufaną walidację

Domyślna ścieżka wyjściowa:

```text
build/bench-current.json
```

Uruchomienia zakresowe zapisują jeden plik JSON dla każdego zbioru danych i silnika w
`build/bench-ranges/`.

`bench:gate` porównuje dwa pliki JSON z benchmarkami i kończy działanie kodem różnym od zera, gdy dowolna śledzona metryka jest wolniejsza niż skonfigurowany próg. Jeśli nie przekazano wartości `--metric`, sprawdza każdą metrykę z pliku bazowego.

`bench:ci` to bramka regresji dymnej GitHub Actions. Uruchamia benchmark 1k dla wszystkich silników, porównuje wybrane metryki zapisu/odczytu/pamięci podręcznej/UUID z śledzonym punktem odniesienia `bench/baselines/ci-1k-all.json` i stosuje szeroki domyślny próg, aby uniknąć typowych wahań środowiska uruchomieniowego, a zarazem wykrywać duże regresje wydajności.

`bench:jit` uruchamia ten sam zestaw testowy z włączonym CLI opcache i JIT śledzącym, a dla procesu benchmarku wyłącza Xdebug, aby PHP mógł rzeczywiście włączyć JIT.
Wygenerowany JSON zawiera flagi środowiska uruchomieniowego pokazujące, czy opcache i JIT były aktywne podczas tego uruchomienia.
