Benchmarks
O harness de benchmark grava JSON estáveis e um relatório legível. Ele fica
em bench/ no repositório storh e não faz parte do pacote Composer desde 0.5.2; por isso, os comandos composer bench* abaixo só podem ser executados a partir de um clone, após 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
Resultados de referência
Medianas de três execuções completas do harness, com 50,000 registros por mecanismo, em um Apple M1 Pro (16 GB, macOS 26.5, APFS) com PHP 8.5 CLI, opcache e JIT desativados:
composer bench:repeat -- --dataset=50000 --engine=all --repeat=3 --memory-limit=512M
Cada gravação é descarregada e sincronizada com fsync antes de storh informar que foi armazenada, portanto as taxas de gravação são limitadas pelo sistema de arquivos. As leituras usam o cache de páginas do sistema operacional. As taxas são
calculadas a partir das medianas; execute composer bench a partir de um clone do repositório
no seu próprio hardware.
| DocStore, 50 mil registros | mediana |
|---|---|
put(), um arquivo durável por registro |
3.3k registros/s |
ingestão em lote putStream() |
3.7k registros/s |
importJsonl() |
3.6k registros/s |
leitura pontual get(), validada por STAT |
5.6 µs |
| reabertura de um armazenamento existente | 0.26 ms |
consulta por igualdade indexada, limit(100) |
1.1 ms |
count() indexada na coleção |
41 µs |
| criação de índice, 2 campos de igualdade + 1 de intervalo | 52k registros/s |
stream() completo com revalidação STAT |
103k registros/s |
exportJsonl() |
104k registros/s |
| SegmentedLog, 50 mil registros, segmentos de 16 KB | mediana |
|---|---|
put(), fsync por acréscimo |
16k acréscimos/s |
ingestão em lote appendStream() |
56k registros/s |
| leitura com cursor, 100 registros do meio | 2.1 ms |
| leitura por intervalo de tempo | 1.1 ms |
count() por igualdade |
11 µs |
compact() de todos os segmentos selados |
69k registros/s |
| reabertura com recuperação de final incompleto | 218k registros/s |
O benchmark sela um segmento a cada 16 KB para testar a rotação de segmentos; o tamanho padrão do segmento é 1 MiB.
| Fila, 50 mil tarefas | mediana |
|---|---|
enqueue(), fsync por evento |
22k tarefas/s |
claim() |
23k tarefas/s |
complete() |
23k tarefas/s |
enqueueMany() |
209k tarefas/s |
claimMany() |
467k tarefas/s |
completeMany() |
512k tarefas/s |
| SQL Mirror, SQLite, 50 mil registros | mediana |
|---|---|
push() inicial |
61k linhas/s |
push() sem alterações |
86k registros/s |
flush(), IDs 100 |
7.1 ms |
SQL COUNT indexada na mirror |
6.9 ms |
rebuild() |
65k linhas/s |
restauração pull(), um arquivo durável por registro |
3.2k registros/s |
| Micro | mediana |
|---|---|
get() em cache, frio e depois aquecido (MemoryCache, STAT) |
83 µs / 6.3 µs |
| geração UUIDv7 | 1.3 µs |
| validação UUIDv7 | 0.30 µs |
| filtragem de predicados em memória | 4.4M linhas/s |
Alvos cobertos:
- DocStore put, streaming put, exportação/importação JSONL, get, delete, stream e criação de índice
- consultas do DocStore por igualdade, índice composto, intervalo, intervalo ordenado crescente e decrescente, contagem indexada, busca por ID e sem índice
- acréscimo no SegmentedLog, acréscimo em fluxo, leituras com cursor, leituras por intervalo de tempo, leituras compactadas por intervalo de tempo, buscas com cursor e por ID no QueryBuilder, contagens, contagens de acréscimos em lote, estatísticas e compactação
- enfileirar, enfileirar em lote, reivindicar, reivindicar em lote, concluir, concluir em lote e recolocar na fila
- filtragem de predicados de RecordQuery e QueryCondition em linhas na memória
- geração monotônica UUIDv7, geração com distribuição de timestamps, validação e extração de timestamp
- recuperação de linha final incompleta
- leituras de cache frio e aquecido com validação por hash, stat ou confiável
Caminho de saída padrão:
build/bench-current.json
As execuções de intervalo gravam um arquivo JSON por conjunto de dados e mecanismo em
build/bench-ranges/.
bench:gate compara dois arquivos de JSON de benchmark e termina com código diferente de zero quando qualquer
métrica monitorada é mais lenta que o limite configurado. Se nenhum valor --metric for passado,
o controle se aplica a todas as métricas presentes no arquivo de referência.
bench:ci é o controle rápido de regressão GitHub Actions. Executa um benchmark de 1k com todos os mecanismos,
compara métricas selecionadas de gravação/leitura/cache/UUID com a referência bench/baselines/ci-1k-all.json monitorada e usa um limite padrão amplo para
ignorar oscilações normais do executor, mas ainda detectar regressões grandes de desempenho.
bench:jit executa o mesmo harness com opcache CLI e JIT de rastreamento ativados,
e desativa Xdebug para o processo de benchmark, para que PHP possa realmente ativar o JIT.
O JSON gerado inclui flags de execução que mostram se opcache e JIT
estavam ativos nessa execução.