
# Бенчмарки

Стенд для тестирования производительности создает стабильный 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, потоковое добавление, чтение курсором, чтение по диапазону времени, чтение по диапазону времени после уплотнения, запросы курсором QueryBuilder и поиск по ID, подсчёты, подсчёты массового добавления, статистика, уплотнение
- добавление задач в 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 во время запуска.
