documentation
Kohinos v3 Doc générée le commit 8d60b1904

Performance — sobriété d'abord

Kohinos n'est pas Twitter. Une instance type vise quelques centaines à quelques milliers d'adhérents — charge faible mais financière. La règle locale : préférer la simplicité robuste à la performance bricolée. Toute optimisation commence par une mesure, jamais par une intuition. La performance vient après l'intégrité financière, la souveraineté, la sobriété et la maintenabilité (cf. .claude/rules/global.md).

⚠ Hiérarchie des priorités projet

1. Intégrité financière · 2. Souveraineté (self-host) · 3. Sobriété (Raspberry Pi) · 4. Maintenabilité · 5. Performance. Un patch qui améliore la perf mais complexifie la logique monétaire n'est pas un bon patch pour Kohinos.

Cibles matérielles

Le projet est conçu pour tourner sur du matériel modeste. Ces cibles couvrent 99 % des MLCC françaises.

CibleRAM / vCPUAdhérents max conseillésNotes
Raspberry Pi 4 4 Go / 4 cœurs ARMv8 < 1 000 SD card en lecture seule + SSD USB3 pour var/ et MariaDB.
Raspberry Pi 5 8 Go / 4 cœurs Cortex-A76 < 5 000 NVMe via HAT recommandé. Confortable jusqu'à ~3 fédérations colocataires.
VPS minimal 2 vCPU / 4 Go ~10 000 Cible production type Fédération Kohinos. Au-delà : passer à 4 vCPU / 8 Go avant Redis.
Serveur dédié 4+ vCPU / 8+ Go > 10 000 Probablement sur-dimensionné : questionner la vraie charge avant d'investir.
Ordre de grandeur réaliste

Une MLCC active = quelques dizaines de transactions par jour, un pic mensuel sur les prélèvements SEPA et SSA (cron de nuit). Le besoin perf est dominé par les batch jobs, pas par le trafic HTTP.

Mesurer avant d'optimiser

Aucune optimisation sans une mesure préalable. Trois outils légaux dans ce projet :

# Activer le slow query log MariaDB
sudo mysql -e "SET GLOBAL slow_query_log = 'ON';
               SET GLOBAL long_query_time = 1;
               SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';"

# Inspecter une requête repérée
EXPLAIN SELECT * FROM transaction WHERE solidoume_parameter_id = 4
        AND reference LIKE 'Solidoume%' AND DATE(created_at) >= '2026-01-01';

Patterns Doctrine performants

⚠ Pièges Doctrine récurrents sur Kohinos
  • N+1 queries : une boucle qui accède à une relation lazy = autant de requêtes que d'éléments. Utiliser l'agent n-plus-one-detector sur les services suspects (ex. listings SSA, recap mensuel SEPA).
  • JOIN FETCH quand on sait que la relation sera utilisée : SELECT t, m FROM Transaction t JOIN FETCH t.mandate m WHERE ....
  • Pagination obligatoire dès qu'une liste peut dépasser 50 éléments — utiliser KnpPaginator ou Doctrine\ORM\Tools\Pagination\Paginator.
  • $em->clear() ou iterateBatched() dans les batchs longs (cron SEPA / SSA) tous les 100 objets, sinon l'UnitOfWork explose la RAM du Pi.
  • Ne pas faire de QueryBuilder dans un contrôleur ou un service — toujours dans le repository, méthode find*() bien nommée (cf. règles projet).
// Exemple batch SEPA — vider l'UnitOfWork tous les 100 mandats
$batchSize = 100;
$i = 0;
foreach ($mandateRepo->iterateActiveDue($month) as $mandate) {
    $this->processOne($mandate);
    if ((++$i % $batchSize) === 0) {
        $this->em->flush();
        $this->em->clear(); // libère la RAM, indispensable sur Pi
    }
}
$this->em->flush();
$this->em->clear();

Cache

Symfony et Doctrine fournissent plusieurs couches de cache. Sur Kohinos elles sont activées progressivement : filesystem suffit jusqu'à 1 000 adhérents, Redis local au-delà.

CoucheAdapter recommandéNote
Symfony cache app cache.adapter.filesystem (dev/petit prod) · cache.adapter.redis (Redis local) APCu déconseillé sur Kohinos (workload random-write, fragmentation mémoire).
Doctrine metadata cache Pool cache.system (PHP array en prod via cache:warmup) Voir config/packages/doctrine.yaml — déjà branché.
Doctrine query cache Même pool — généré au warmup Met en cache le SQL des DQL, pas les résultats.
Doctrine result cache À activer ponctuellement (ex. GlobalParameter) Penser invalidation manuelle quand on modifie le paramètre.
HTTP cache Symfony HttpCache (reverse-proxy intégré) ou Varnish Pages publiques CMS uniquement — jamais les vues comptes.
# Bascule vers Redis (prod > 1000 adhérents) — config/packages/cache.yaml
framework:
    cache:
        app: cache.adapter.redis
        default_redis_provider: redis://127.0.0.1:6379

OPcache & JIT

OPcache (extension opcache déjà listée dans installation.html) est obligatoire en prod : on parle d'un facteur ×3 à ×8 sans aucune autre modification.

; /etc/php/8.4/fpm/conf.d/10-opcache.ini (prod)
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256       ; Mo (128 pour un Pi 4)
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000  ; 6000 pour un Pi 4
opcache.validate_timestamps=0        ; PROD seulement : rebuild au déploiement
opcache.save_comments=1              ; nécessaire pour Doctrine attributes

; JIT (PHP 8.4) — gain marginal sur Symfony, à activer après mesure
opcache.jit=tracing
opcache.jit_buffer_size=128M         ; 64M sur un Pi
⚠ opcache.validate_timestamps=0 en prod

Cette option fige le bytecode en mémoire. Un git pull seul ne suffit pas à appliquer le nouveau code : il faut recharger php-fpm (systemctl reload php8.4-fpm) au déploiement. Voir déploiement.html.

JIT : PHP 8.4 propose tracing et function. Le gain pour une appli Symfony classique (I/O DB dominant) est de l'ordre de 5–15 % seulement. À activer après avoir mesuré l'impact sur votre charge réelle.

Frontend

Vanilla JS + Bootstrap 5.2 = empreinte modeste. Mais Webpack Encore et le navigateur laissent beaucoup à gagner :

Indexation BDD

Sur DBAL 3, Doctrine ne crée pas automatiquement d'index sur les clés étrangères : c'est au développeur d'ajouter #[ORM\Index]. Cibles à vérifier en priorité :

Table / colonneRaison
transaction.solidoume_parameter_idFiltré dans tous les recap SSA.
transaction.referenceLIKE 'Solidoume%', 'Cotisation MLC%'… (préfixe → index utilisable).
sepa_mandate.mandate_referenceLookup UMR pour notifications + webhooks.
sepa_mandate.last_processed_monthÉvite le double prélèvement.
sepa_recurring_request.last_month_payedIdem.
payment.helloassoidRecherche webhook HelloAsso.
Toute FKJOIN sans index = scan complet.
// Exemple : index sur une référence souvent filtrée
#[ORM\Entity]
#[ORM\Index(name: 'idx_tx_solidoume_param', columns: ['solidoume_parameter_id'])]
#[ORM\Index(name: 'idx_tx_reference', columns: ['reference'])]
class Transaction
{
    // ...
}
Limiter la longueur des colonnes indexées

Un index sur VARCHAR(255) coûte beaucoup plus cher qu'un index sur VARCHAR(64). Pour les références techniques (UMR, IDs externes), réduire à la longueur réelle utile. Pour les recherches sur reference en LIKE préfixé, un index simple suffit ; pour du LIKE médian, envisager un index FULLTEXT (ou de re-modéliser).

Toujours EXPLAIN une nouvelle requête métier avant la mise en prod, particulièrement les batchs SEPA/SSA qui scannent l'historique.

Anti-patterns à refuser

⛔ Optimisations interdites sur Kohinos
  • Cacher un solde de compte. Le solde temps-réel se calcule depuis les opérations (cf. AccountableObject). Un cache de solde = risque d'écart financier = refus immédiat.
  • Pré-fetcher des données financières. Une transaction lue doit l'être au moment où on en a besoin. Pas de batch nocturne qui pré-agrège un solde dans un champ balance_cached.
  • Microservices pour 100 adhérents. Le monolithe Symfony tient sur un Pi 4. Découper en services réseau ajoute des modes de panne sans bénéfice mesurable.
  • Queue lourde (RabbitMQ, Kafka) pour des cron mensuels. symfony/messenger + transport doctrine:// suffit amplement pour les volumes Kohinos.
  • CDN propriétaire (Cloudflare, AWS CloudFront) pour des assets publics. HTTP/2 + cache-control immutable = même résultat sans intermédiaire qui voit le trafic des adhérents.
  • Cacher pour masquer un bug. Si une requête est lente, corriger la requête, pas l'envelopper dans un cache à TTL court.

Sobriété — performance ≠ scale

Le Raspberry Pi est une contrainte de design : il force à se demander si une feature mérite vraiment sa complexité. Une instance Kohinos qui tient sur 5 W sans Redis, sans queue externe et sans CDN est plus simple à maintenir, plus simple à auto-héberger, et plus difficile à censurer. C'est cohérent avec la roadmap Reticulum / LoRa : moins on dépend de l'infrastructure, plus on est libre.

Simpler than faster. Avant d'ajouter une couche, demandez-vous : cette optimisation économise combien de Wh par mois ? Combien de minutes de RAM libérée ? Et combien de minutes de dette technique je signe pour la maintenir ?

Check-list avant d'ajouter du cache ou un service
  • Ai-je mesuré (profiler + EXPLAIN) ?
  • Le gain est-il > 100 ms / requête utilisateur ?
  • Le service à ajouter est-il self-hostable ?
  • Ce cache touche-t-il une donnée financière (interdit) ?
  • Le code reste-t-il lisible à 6 mois pour un futur contributeur solo ?

Si la réponse à une seule question est non : refuser le patch.

Voir aussi : installation.html · monitoring.html.