Benchmarks
Das Benchmark-Harness schreibt stabile JSON und einen verständlichen Bericht. Es befindet sich in bench/ im storh-Repository und ist seit Version 0.5.2 nicht mehr Teil des Composer-Pakets. Die folgenden composer bench*-Befehle funktionieren daher nur in einem Klon nach 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
Referenzergebnisse
Medianwerte aus drei vollständigen Durchläufen des Harness mit 50.000 Datensätzen pro Engine auf einem Apple M1 Pro (16 GB, macOS 26.5, APFS) mit PHP 8.5 CLI, deaktiviertem Opcache und JIT:
composer bench:repeat -- --dataset=50000 --engine=all --repeat=3 --memory-limit=512M
Jeder Schreibvorgang wird geflusht und mit fsync gesichert, bevor storh ihn als gespeichert meldet. Daher hängt die Schreibrate vom Dateisystem ab. Lesevorgänge greifen auf den Seiten-Cache des Betriebssystems zu. Die Raten werden aus den Medianwerten abgeleitet. Führe composer bench in einem Klon des Repositorys aus, um deine eigene Hardware zu messen.
| DocStore, 50.000 Datensätze | Median |
|---|---|
put(), eine dauerhafte Datei pro Datensatz |
3.3k Datensätze/s |
putStream() Masseneinlesen |
3.7k Datensätze/s |
importJsonl() |
3.6k Datensätze/s |
get() Einzelabruf, STAT-validiert |
5.6 µs |
| Vorhandenen Store erneut öffnen | 0.26 ms |
Indizierte Gleichheitsabfrage, limit(100) |
1.1 ms |
Indizierte count() in der gesamten Sammlung |
41 µs |
| Indexaufbau, 2 Gleichheits- und 1 Bereichsfeld | 52k Datensätze/s |
Vollständiges stream() mit STAT-Neuvalidierung |
103k Datensätze/s |
exportJsonl() |
104k Datensätze/s |
| SegmentedLog, 50.000 Datensätze, 16-KB-Segmente | Median |
|---|---|
put(), fsync bei jedem Anhängen |
16k Anhänge/s |
appendStream() Masseneinlesen |
56k Datensätze/s |
| Cursor-Lesen, 100 Datensätze ab der Mitte | 2.1 ms |
| Lesen eines Zeitbereichs | 1.1 ms |
Gleichheits-count() |
11 µs |
compact() aller versiegelten Segmente |
69k Datensätze/s |
| Erneutes Öffnen mit Wiederherstellung nach unvollständigem Dateiende | 218k Datensätze/s |
Der Benchmark versiegelt alle 16 KB ein Segment, um Segmentwechsel zu testen. Die Standardgröße eines Segments beträgt 1 MiB.
| Queue, 50.000 Jobs | Median |
|---|---|
enqueue(), fsync bei jedem Ereignis |
22k Jobs/s |
claim() |
23k Jobs/s |
complete() |
23k Jobs/s |
enqueueMany() |
209k Jobs/s |
claimMany() |
467k Jobs/s |
completeMany() |
512k Jobs/s |
| SQL Mirror, SQLite, 50.000 Datensätze | Median |
|---|---|
Initiales push() |
61k Zeilen/s |
push() ohne Änderungen |
86k Datensätze/s |
flush(), 100 IDs |
7.1 ms |
Indiziertes SQL COUNT im Mirror |
6.9 ms |
rebuild() |
65k Zeilen/s |
pull() Wiederherstellung, eine dauerhafte Datei pro Datensatz |
3.2k Datensätze/s |
| Mikro | Median |
|---|---|
Gecachter get(), kalt, dann warm (MemoryCache, STAT) |
83 µs / 6.3 µs |
| UUIDv7 generieren | 1.3 µs |
| UUIDv7 validieren | 0.30 µs |
| In-Memory-Filterung nach Prädikat | 4.4M Zeilen/s |
Abgedeckte Ziele:
- DocStore put, Streaming-put, JSONL-Export/-Import, get, delete, stream und Indexaufbau
- Gleichheits-, zusammengesetzt und bereichsindizierte Abfragen, aufsteigend und absteigend sortierte Bereiche, indizierte Zählung, ID-Suche und nicht indizierte DocStore-Abfragen
- SegmentedLog append, Streaming-append, Cursor-Lesevorgänge, Zeitbereichsabfragen, Abfragen komprimierter Zeitbereiche, QueryBuilder-Cursor- und ID-Suchen, Zählungen, Zählungen beim Masseneinfügen, Statistiken und Komprimierung
- Queue enqueue, Masseneinreihung, claim, Massen-claim, complete, Massen-complete und requeue
- Filtern von In-Memory-Zeilen anhand von Prädikaten mit RecordQuery und QueryCondition
- Monotone UUIDv7-Generierung, Generierung mit zeitlich verteilten Zeitstempeln, Validierung und Extraktion des Zeitstempels
- Wiederherstellung nach unvollständiger Zeile am Dateiende
- Kalte und warme Cache-Lesevorgänge mit Hash-, Stat- oder vertrauenswürdiger Validierung
Standard-Ausgabepfad:
build/bench-current.json
Bei Bereichsläufen wird pro Datensatz und Engine eine JSON-Datei unter build/bench-ranges/ geschrieben.
bench:gate vergleicht zwei Benchmark-JSON-Dateien und gibt einen Exit-Code ungleich null zurück, wenn eine erfasste Kennzahl den konfigurierten Schwellenwert unterschreitet. Werden keine --metric-Werte übergeben, prüft es alle Kennzahlen der Baseline-Datei.
bench:ci ist die Smoke-Regression-Prüfung für GitHub Actions. Sie führt einen 1k-Benchmark mit allen Engines aus, vergleicht ausgewählte Schreib-, Lese-, Cache- und UUID-Metriken mit der erfassten bench/baselines/ci-1k-all.json-Baseline und verwendet einen großzügigen Standard-Schwellenwert, um normale Runner-Schwankungen zu vermeiden und dennoch große Leistungsregressionen zu erkennen.
bench:jit führt dasselbe Harness mit aktiviertem CLI Opcache und Tracing-JIT aus und deaktiviert Xdebug für den Benchmark-Prozess, damit PHP JIT tatsächlich aktivieren kann. Die erzeugte JSON enthält Laufzeit-Flags, anhand derer du siehst, ob Opcache und JIT bei diesem Durchlauf aktiv waren.