Бенчмарки
Стенд для тестирования производительности создает стабильный JSON и понятный человеку отчет. Он находится
в bench/ в репозитории storh и
не входил в пакет Composer с 0.5.2, поэтому команды composer bench*
ниже выполняются только из клона после composer install:
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 выключены:
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, генерация с разбросом временных меток, проверка и извлечение временной метки
- восстановление после незавершённой строки в конце
- чтение из холодного и тёплого кэша с проверкой по хешу, статистике или с доверием к результату проверки
Путь сохранения по умолчанию:
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 во время запуска.