
# Тестування продуктивності

Інструментарій для тестування продуктивності записує стабільні JSON і звіт для читання. Він міститься в `bench/` у репозиторії [storh та ](https://github.com/inline0/storh), його не включали до пакета Composer після 0.5.2, тож наведені нижче команди `composer bench*` можна виконати лише з клонованого репозиторію після `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
```

## Еталонні результати

Медіанні значення трьох повних запусків інструментарію, по 50,000 записів на рушій, на Apple M1 Pro (16 GB, macOS 26.5, APFS) із PHP 8.5 CLI, opcache і JIT вимкнено:

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

Кожен запис скидається на диск і синхронізується через fsync, перш ніж storh повідомляє, що його збережено, тому швидкість запису обмежена файловою системою. Читання виконується з кешу сторінок ОС. Швидкості обчислено за медіанними значеннями; запустіть `composer bench` із клону репозиторію на власному обладнанні.

| DocStore, 50k записів                         | медіана         |
| ---------------------------------------------- | --------------- |
| `put()`, один довговічний файл на запис         | 3.3k записів/с  |
| `putStream()`, масове завантаження                    | 3.7k записів/с  |
| `importJsonl()`                                          | 3.6k записів/с  |
| `get()`, читання за ключем із перевіркою STAT    | 5.6 µs          |
| повторне відкриття наявного сховища             | 0.26 ms         |
| індексований запит на рівність, `limit(100)`         | 1.1 ms          |
| індексований `count()` по всій колекції           | 41 µs           |
| побудова індексу: 2 поля рівності + 1 діапазону | 52k записів/с   |
| повне `stream()` із повторною перевіркою STAT       | 103k записів/с  |
| `exportJsonl()`                                          | 104k записів/с  |

| SegmentedLog, 50k записів, сегменти по 16 KB   | медіана         |
| ---------------------------------------------- | --------------- |
| `put()`, fsync після кожного додавання           | 16k додавань/с  |
| `appendStream()`, масове завантаження                    | 56k записів/с   |
| читання курсором, 100 записів із середини    | 2.1 ms          |
| читання за діапазоном часу                      | 1.1 ms          |
| запит на рівність `count()`                        | 11 µs           |
| `compact()` усіх закритих сегментів                 | 69k записів/с   |
| повторне відкриття з відновленням обрізаного кінця | 218k записів/с |

Під час тестування сегмент закривається кожні 16 KB, щоб перевірити перемикання між сегментами; розмір сегмента за замовчуванням — 1 MiB.

| Queue, 50k завдань                          | медіана         |
| ---------------------------------------------- | --------------- |
| `enqueue()`, fsync після кожної події                | 22k завдань/с   |
| `claim()`                                          | 23k завдань/с   |
| `complete()`                                          | 23k завдань/с   |
| `enqueueMany()`                                          | 209k завдань/с  |
| `claimMany()`                                          | 467k завдань/с  |
| `completeMany()`                                          | 512k завдань/с  |

| SQL Mirror, SQLite, 50k записів              | медіана         |
| ---------------------------------------------- | --------------- |
| початкове `push()`                               | 61k рядків/с    |
| `push()` без змін                                 | 86k записів/с   |
| `flush()`, ідентифікатори 100                   | 7.1 ms          |
| індексований SQL `COUNT` у дзеркалі           | 6.9 ms          |
| `rebuild()`                                          | 65k рядків/с    |
| відновлення `pull()`, один довговічний файл на запис | 3.2k записів/с |

| Мікро                                          | медіана          |
| ---------------------------------------------- | --------------- |
| кешований `get()`, холодний, потім теплий (MemoryCache, STAT) | 83 µs / 6.3 µs |
| UUIDv7 генерація                                | 1.3 µs          |
| UUIDv7 перевірка                                | 0.30 µs         |
| фільтрація предикатів у пам’яті                  | 4.4M рядків/с     |

Охоплені цілі:

- DocStore: запис, потоковий запис, експорт/імпорт JSONL, отримання, видалення, читання потоком і побудова індексу
- запити DocStore з індексами рівності, складеними індексами, індексами діапазонів, упорядкованими діапазонами за зростанням і спаданням, підрахунком за індексом, пошуком за ID та без індексів
- SegmentedLog: додавання, потокове додавання, читання курсором, читання за часовим діапазоном, читання стисненого часового діапазону, пошук курсором і за ID через QueryBuilder, підрахунки, підрахунки пакетного додавання, статистика, стиснення
- Queue: додавання в чергу, пакетне додавання, отримання, пакетне отримання, завершення, пакетне завершення, повторне додавання
- фільтрація предикатів RecordQuery і QueryCondition для рядків у пам’яті
- монотонна генерація UUIDv7, генерація з розподілом часових позначок, перевірка та отримання часових позначок
- відновлення після обірваного останнього рядка
- холодне та тепле читання кешу з перевіркою за хешем, статистикою або довіреною перевіркою

Шлях виводу за замовчуванням:

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

Запуски діапазонів записують один файл JSON для кожного набору даних і рушія в
`build/bench-ranges/`.

`bench:gate` порівнює два файли бенчмарку JSON і завершується з ненульовим кодом, якщо будь-який
відстежуваний показник повільніший за налаштований поріг. Якщо значення `--metric` не передано,
перевіряються всі показники, наявні у файлі базового рівня.

`bench:ci` — це перевірка регресій у швидкому режимі GitHub Actions. Вона запускає бенчмарк усіх рушіїв на 1k,
порівнює вибрані показники запису/читання/кешу/UUID з відстежуваним базовим рівнем `bench/baselines/ci-1k-all.json` і використовує широкий поріг за замовчуванням,
щоб уникнути звичайних коливань середовища запуску, але виявляти значні регресії продуктивності.

`bench:jit` запускає той самий тестовий комплекс із увімкненими CLI opcache і трасувальним JIT,
а для процесу бенчмарку вимикає Xdebug, щоб PHP міг активувати JIT.
Згенерований JSON містить прапорці середовища виконання, які показують, чи були активні opcache і JIT під час цього запуску.
