
# Tests de performance

Le banc d'essai écrit des JSON stables et un rapport lisible. Il se trouve
dans `bench/` du dépôt [storh](https://github.com/inline0/storh) et ne fait plus partie du paquet Composer depuis 0.5.2. Les commandes `composer bench*` ci-dessous s'exécutent donc depuis un clone, après `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
```

## Résultats de référence

Médianes de trois exécutions complètes du banc d'essai, avec 50,000 enregistrements par moteur, sur un Apple M1 Pro (16 GB, macOS 26.5, APFS) avec PHP 8.5 CLI, opcache et JIT désactivés :

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

Chaque écriture est vidée et synchronisée avec fsync avant que storh ne la signale comme stockée : les débits d'écriture dépendent donc du système de fichiers. Les lectures s'effectuent dans le cache de pages du système d'exploitation. Les débits sont calculés à partir des médianes ; exécutez `composer bench` depuis un clone du dépôt sur votre propre matériel.

| DocStore, 50k enregistrements                  | médiane         |
| ---------------------------------------------- | --------------- |
| `put()`, un fichier durable par enregistrement  | 3.3k enregistrements/s |
| `putStream()` ingestion en lot                        | 3.7k enregistrements/s |
| `importJsonl()`                                          | 3.6k enregistrements/s |
| `get()` lecture ponctuelle, validée par STAT     | 5.6 µs          |
| réouverture d'un magasin existant               | 0.26 ms         |
| requête d'égalité indexée, `limit(100)`               | 1.1 ms          |
| `count()` indexée dans toute la collection         | 41 µs           |
| création d'index, 2 champs d'égalité + 1 de plage | 52k enregistrements/s |
| `stream()` complet avec revalidation STAT           | 103k enregistrements/s |
| `exportJsonl()`                                          | 104k enregistrements/s |

| SegmentedLog, 50k enregistrements, segments de 16 KB | médiane       |
| ---------------------------------------------- | --------------- |
| `put()`, fsync à chaque ajout                    | 16k ajouts/s    |
| `appendStream()` ingestion en lot                         | 56k enregistrements/s |
| lecture par curseur, 100 enregistrements depuis le milieu | 2.1 ms |
| lecture par plage temporelle                    | 1.1 ms          |
| `count()` d'égalité                                 | 11 µs           |
| `compact()` tous les segments scellés                 | 69k enregistrements/s |
| réouverture avec récupération de fin tronquée    | 218k enregistrements/s |

Le banc d'essai scelle un segment tous les 16 KB afin de provoquer des rotations ; la taille de segment par défaut est de 1 MiB.

| Queue, 50k tâches                              | médiane         |
| ---------------------------------------------- | --------------- |
| `enqueue()`, fsync à chaque événement               | 22k tâches/s    |
| `claim()`                                          | 23k tâches/s    |
| `complete()`                                          | 23k tâches/s    |
| `enqueueMany()`                                          | 209k tâches/s   |
| `claimMany()`                                          | 467k tâches/s   |
| `completeMany()`                                          | 512k tâches/s   |

| Miroir SQL, SQLite, 50k enregistrements      | médiane         |
| ---------------------------------------------- | --------------- |
| `push()` initiale                                 | 61k lignes/s    |
| `push()` sans changement                          | 86k enregistrements/s |
| `flush()`, 100 identifiants                      | 7.1 ms          |
| SQL indexée `COUNT` sur le miroir              | 6.9 ms          |
| `rebuild()`                                          | 65k lignes/s    |
| restauration `pull()`, un fichier durable par enregistrement | 3.2k enregistrements/s |

| Micro                                           | médiane         |
| ---------------------------------------------- | --------------- |
| `get()` en cache, à froid puis à chaud (MemoryCache, STAT) | 83 µs / 6.3 µs |
| génération UUIDv7                              | 1.3 µs          |
| validation UUIDv7                               | 0.30 µs         |
| filtrage de prédicats en mémoire                | 4.4M lignes/s   |

Cibles couvertes :

- DocStore put, put en continu, export/import JSONL, get, delete, stream et création d'index
- requêtes DocStore avec index d'égalité, index composé, index de plage, plages ordonnées croissantes et décroissantes, décompte indexé, recherche par identifiant et sans index
- ajout à SegmentedLog, ajout en continu, lectures par curseur, lectures par plage temporelle, lectures de plages temporelles compactées, lectures par curseur et recherche d'identifiant avec QueryBuilder, décomptes, décomptes d'ajouts en lot, statistiques, compactage
- mise en file d'attente, ajout en lot, revendication, revendication en lot, finalisation, finalisation en lot, remise en file d'attente avec Queue
- filtrage par prédicats RecordQuery et QueryCondition sur des lignes en mémoire
- génération monotone UUIDv7, génération avec horodatages répartis, validation et extraction d'horodatage
- récupération après une ligne finale incomplète
- lectures du cache à froid et à chaud avec validation par hachage, statut ou confiance

Chemin de sortie par défaut :

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

Les exécutions par plage écrivent un fichier JSON par jeu de données et moteur sous
`build/bench-ranges/`.

`bench:gate` compare deux fichiers JSON de banc d'essai et renvoie un code de sortie non nul si une métrique suivie est plus lente que le seuil configuré. Si aucune valeur `--metric` n'est fournie, toutes les métriques présentes dans le fichier de référence sont vérifiées.

`bench:ci` est la vérification rapide des régressions de GitHub Actions. Elle exécute un banc d'essai de 1k sur tous les moteurs, compare les métriques sélectionnées d'écriture, de lecture, de cache et de UUID avec la référence `bench/baselines/ci-1k-all.json` suivie, et applique un seuil par défaut large pour éviter le bruit normal du système d'exécution tout en détectant les baisses de performances importantes.

`bench:jit` exécute le même banc d'essai avec CLI opcache et le JIT de traçage activés, et désactive Xdebug pour le processus du banc d'essai afin que PHP puisse réellement activer le JIT. Le JSON généré inclut des options d'exécution qui indiquent si opcache et le JIT étaient actifs lors de cette exécution.
