Aller au contenu
getnextpdf.com

Dépannage : mémoire et performance

Ces entrées couvrent deux familles de défaillances que tu rencontres sous charge : PHP qui manque de mémoire pendant un rendu, et un débit qui s’effondre d’un coup une fois qu’un processus est chaud ou saturé. Chaque entrée nomme un symptôme, la cause la plus probable, et un correctif qui utilise une vraie surface NextPDF ou des contrôles PHP-FPM standard. Pour le modèle de flux sous-jacent et un tutoriel de worker, lis Flux et mémoire ; cette page est son compagnon côté incident.

Mesure d’abord. Échantillonne memory_get_peak_usage(true) avant et après un rendu et appelle memory_reset_peak_usage() entre les itérations, comme le benchmark du moteur isole le coût par rendu. Régler sans baseline déplace la chute plutôt que de la supprimer.

Entrée : « Allowed memory size exhausted » pendant la génération

Section intitulée « Entrée : « Allowed memory size exhausted » pendant la génération »
  • Symptôme. Un rendu avorte avec un fatal Allowed memory size of <n> bytes exhausted du runtime PHP, souvent sur un document volumineux ou riche en images.
  • Cause probable. Le chemin d’écriture par défaut compose tout le document, puis le sérialise, donc le pic mémoire suit la taille totale de la sortie. Un grand document, de grosses images embarquées ou une grande fonte embarquée peuvent pousser la requête au-delà de memory_limit.
  • Résolution.
    1. Borne le cache d’images. NextPDF\Core\Config expose imageCacheBytes (par défaut 52428800, soit 50 Mo). Abaisse-le avec le wither d’instance $config->withImageCacheBytes($bytes) (signature withImageCacheBytes(int $bytes): self) pour qu’un build qui embarque beaucoup d’images échoue tôt sur un plafond connu plutôt que de swapper. Cela plafonne le cache d’images en mémoire ; il ne ré-échantillonne ni ne ré-encode les images elles-mêmes.
    2. Réduis les entrées avant l’embarquement. Le Core ne réduit ni ne ré-encode les images. Redimensionne et ré-encode l’illustration matricielle surdimensionnée avant de l’embarquer, et embarque les polices que tu utilises réellement pour que le sous-ensemblage ait un petit jeu de glyphes à conserver (voir Réduire la taille de fichier d’un PDF).
    3. Garde la compression active. Un Config neuf a compress à true. Laisse-le actif pour les builds normaux ; withCompress(false) n’est pas une optimisation de taille (elle augmente généralement la sortie). Recours-y pour déboguer ou profiler le pipeline — elle déplace le compromis CPU/mémoire (en sautant l’étape de compression) plutôt que de réduire la mémoire.
    4. Augmente memory_limit délibérément, par worker. C’est un réglage PHP standard, pas une clé NextPDF. Définis-le dans la config du pool ou avec ini_set('memory_limit', '256M') pour le processus CLI/file d’attente, et dimensionne-le contre un pic profilé, pas une supposition.
  • Lié. Flux et mémoire.

Entrée : la mémoire croît avec le nombre de pages sur de très grands documents

Section intitulée « Entrée : la mémoire croît avec le nombre de pages sur de très grands documents »
  • Symptôme. Un document de plusieurs milliers de pages épuise la mémoire même si chaque page est petite, et le pic monte à peu près au rythme du nombre de pages.
  • Cause probable. L’écriveur tamponné garde tout le document sérialisé dans le tas. Pour de très grands documents, c’est le coût dominant.
  • Résolution.
    1. Préfère le chemin d’écriture en flux. Utilise le chemin d’écriture en flux documenté décrit dans Flux et mémoire : il sérialise chaque page au fur et à mesure de sa composition et libère le tampon, ce qui réduit la croissance du tampon de pages/de la sortie ; les petites métadonnées par objet (décalages, arbre de pages) peuvent encore croître avec le nombre de pages/d’objets. Suis le point d’entrée documenté plutôt que de copier les classes internes — le moteur de flux sous-jacent est de palier experimental et ses symboles ne sont pas la surface publique stable.
    2. Pour l’analyseur writeHtml() natif, souviens-toi que la mémoire côté entrée est bornée à la fois par les garde-fous de profondeur d’imbrication et de nombre d’éléments : ADR-001 plafonne l’imbrication à MAX_NESTING_DEPTH = 100 et rejette les documents au-delà de MAX_ELEMENT_COUNT = 50000. Un document qui atteint le plafond d’éléments en est informé explicitement plutôt que d’épuiser silencieusement la mémoire. Ces plafonds ADR-001 ne régissent que l’analyseur natif ; le pont Chrome optionnel (writeHtmlChrome()) rend hors processus et a ses propres limites mémoire/entrée séparées, pas ces plafonds.
  • Lié. Flux et mémoire.

Entrée : un worker de longue durée épuise la mémoire après de nombreuses tâches

Section intitulée « Entrée : un worker de longue durée épuise la mémoire après de nombreuses tâches »
  • Symptôme. Les rendus uniques réussissent, mais un worker de file d’attente qui rend de nombreux PDF d’affilée épuise la mémoire après des minutes ou des heures.
  • Cause probable. Un processus PHP de longue durée accumule des allocations à travers les tâches. Une croissance lente invisible dans une seule requête s’aggrave sur des milliers.
  • Résolution.
    1. Partage les registres, recrée les documents. Construis le FontRegistry et l’ImageRegistry une fois au démarrage et passe-les à un DocumentFactory ; crée un Document neuf par tâche avec $factory->create($config). L’analyse des polices et des images se produit alors une fois pour le processus, pas une fois par tâche, et l’arbre de document par tâche est collecté quand il sort de portée. Suis examples/14-worker-factory.php.
    2. Borne le cache d’images partagé avec new ImageRegistry(maxCacheBytes: ...) pour qu’il ne puisse pas croître sans limite à travers les tâches.
    3. Recycle le worker — contrôle de processus, pas une garantie du moteur. En PHP-FPM, définis pm.max_requests pour que chaque enfant se relance après un nombre fixe de requêtes. Dans les files d’attente Laravel, utilise queue:work --max-jobs / --max-time / --memory ; dans Symfony Messenger, utilise messenger:consume --limit / --time-limit / --memory-limit.
  • Lié. Flux et mémoire.

Entrée : chute de débit sur un processus froid ou insuffisamment chauffé

Section intitulée « Entrée : chute de débit sur un processus froid ou insuffisamment chauffé »
  • Symptôme. Les premiers rendus dans un processus neuf sont lents, ou chaque requête paie un coût d’analyse que les requêtes chaudes ne devraient pas payer.
  • Cause probable. Deux coûts de démarrage à froid s’empilent. PHP sans opcache recompile chaque fichier à chaque requête, et un FontRegistry non chauffé analyse chaque fonte la première fois qu’elle est utilisée.
  • Résolution.
    1. Active opcache (et JIT là où il aide). Définis opcache.enable=1 et un opcache.memory_consumption généreux ; en production, définis opcache.validate_timestamps=0 pour que le cache ne soit pas re-vérifié par requête. Ce réglage exige un processus de déploiement qui redémarre ou recharge PHP-FPM (ou réinitialise autrement opcache, par ex. opcache_reset() / cachetool) à chaque livraison — sinon opcache continue de servir l’ancien bytecode et du code périmé tourne après un déploiement. Ce sont des réglages PHP ini standard, pas des clés NextPDF.
    2. Chauffe et verrouille le registre de polices au démarrage. Sur une instance FontRegistry, $fontRegistry->warmup($fontFiles) analyse les fontes une fois pendant le démarrage, et $fontRegistry->lock() gèle le registre pour que le code à l’exécution ne puisse pas muter l’état partagé ; $fontRegistry->isLocked() rapporte l’état. Dans un worker ou serveur d’application véritablement de longue durée — un consommateur de file d’attente ou un worker RoadRunner/Swoole/Octane qui garde le même processus PHP vivant à travers de nombreuses requêtes — un registre chauffé et verrouillé persiste ses fontes analysées dans l’état de l’objet, transformant l’analyse des polices par requête en un coût unique de démarrage de processus. Sous le modèle de requête PHP-FPM standard, cet état d’objet chauffé ne survit pas entre requêtes : opcache met en cache les classes compilées et le bytecode, pas l’état d’objet userland chauffé, donc un FontRegistry chauffé est reconstruit par requête (réexécuté à chaque requête depuis le bootstrap de l’enfant), pas gardé chaud entre requêtes au sein d’un enfant. Sur du PHP-FPM simple, opcache amortit surtout le coût de recompilation du bytecode ; accepte que l’analyse des polices soit payée par requête, non éliminée. L’amortissement entre requêtes — analyser chaque fonte une fois pour la durée de vie du processus — ne s’applique que dans un processus véritablement de longue durée tel qu’un worker RoadRunner/Swoole/Octane ou un consommateur de file d’attente qui garde le même processus PHP vivant à travers de nombreuses requêtes.
    3. Ne ré-analyse pas le même modèle par requête. Résous les polices et les ressources réutilisables une fois au démarrage via les registres partagés ; seul le Document par tâche devrait être créé dans la requête.
  • Lié. Flux et mémoire.

Entrée : le serveur sature et la latence flambe sous concurrence

Section intitulée « Entrée : le serveur sature et la latence flambe sous concurrence »
  • Symptôme. La latence par rendu est correcte en isolation, mais sous charge la machine swappe, le CPU sature, ou les requêtes s’accumulent et expirent.
  • Cause probable. Trop de workers PHP-FPM pour la RAM disponible, donc la somme des pics des workers dépasse la mémoire physique et l’hôte swappe ; ou trop peu de workers, donc les requêtes se sérialisent derrière un petit pool.
  • Résolution.
    1. Dimensionne pm.max_children à partir d’un pic profilé. Utilise la formule standard :

      pm.max_children = (total RAM - OS/other overhead) / per-worker peak memory

      Mesure le vrai pic d’un worker avec un document représentatif (voir la note de profilage dans Portée), réserve de la marge pour l’OS et tout service colocalisé, et divise. Laisse une marge ; ne dimensionne pas à 100 % de la RAM.

    2. Épingle le coût de compression dans ton budget. La compression Flate peut être un coût CPU significatif de l’écriture d’un flux et croît avec le volume d’octets de flux compressibles, donc le nombre de pages et le volume de polices embarquées influencent le CPU par rendu ; le traitement des images, le sous-ensemblage des polices et l’analyse des entrées peuvent aussi dominer. Mesure avec des documents représentatifs, et tiens compte du vrai facteur déterminant quand tu choisis le nombre de workers et le CPU.

    3. Définis pm.max_requests aux côtés de pm.max_children pour que les enfants se recyclent et récupèrent toute croissance lente, comme dans l’entrée worker ci-dessus.

  • Lié. Flux et mémoire.

Entrée : une grosse entrée non fiable est lente ou coûteuse à analyser

Section intitulée « Entrée : une grosse entrée non fiable est lente ou coûteuse à analyser »
  • Symptôme. Un rendu est lent ou gourmand en mémoire sur une entrée grande ou profondément imbriquée, en particulier du HTML ou une police que tu n’as pas produite.
  • Cause probable. Le coût d’analyse croît avec la taille et la structure de l’entrée. Une entrée pathologique (imbrication profonde, un nombre d’éléments énorme, ou une police mal formée) peut dominer le budget.
  • Résolution.
    1. Appuie-toi sur les bornes du moteur. L’analyseur HTML writeHtml() natif impose MAX_NESTING_DEPTH = 100 et MAX_ELEMENT_COUNT = 50000 (ADR-001) ; les entrées au-delà de ces plafonds sont rejetées plutôt que d’être autorisées à épuiser le processus. (Le pont Chrome optionnel, writeHtmlChrome(), est hors du cadre de ces plafonds ADR-001 et impose ses propres limites mémoire/entrée séparées.)
    2. Traite les polices fournies par l’appelant comme non fiables. Une police mal formée lève une NextPDF\Exception\FontParsingException plutôt que de corrompre la sortie, donc intercepte l’exception précise et rejette l’entrée au lieu de réessayer.
    3. Valide et dimensionne les entrées à ta frontière, et applique des limites au niveau de la requête sur la taille des documents pour le contenu influencé par l’appelant.
  • Lié. Dépannage : polices et balisage.
SymptômeLevier le plus probable
Allowed memory size … exhausted sur un rendu uniqueAbaisser $config->withImageCacheBytes() ; réduire les images avant l’embarquement ; augmenter memory_limit par worker
Le pic mémoire monte avec le nombre de pagesUtiliser le chemin d’écriture en flux documenté
La mémoire du worker grimpe au fil de nombreuses tâchesPartager FontRegistry/ImageRegistry via DocumentFactory ; définir pm.max_requests / --max-jobs
Premières requêtes lentes, coût d’analyse par requêteActiver opcache ; $fontRegistry->warmup() puis ->lock() au démarrage
L’hôte swappe / pics de latence sous chargeDimensionner pm.max_children = (RAM − surcoût) / pic par worker
Lent ou lourd sur une entrée grande/non fiableS’appuyer sur les plafonds ADR-001 ; rejeter les polices mal formées sur FontParsingException
  • imageCacheBytes est un plafond mémoire, pas un bouton de taille. L’abaisser plafonne le cache pour qu’un build échoue tôt ; il ne ré-échantillonne ni ne ré-encode jamais les images que tu embarques. Le Core n’a aucun contrôle de qualité d’image.
  • withCompress(false) rend les fichiers plus gros et est une aide au débogage/profilage. Ce n’est pas une optimisation de taille ; il déplace le compromis CPU/mémoire (il saute l’étape de compression) plutôt que de réduire la mémoire.
  • Le profil mémoire exact du moteur de flux est une propriété de palier experimental et peut changer entre versions mineures. Traite toute mesure unique comme une observation, pas comme une constante portable.
  • memory_limit, opcache.*, pm.max_children et pm.max_requests sont des réglages PHP / PHP-FPM standard. NextPDF n’expose pas ses propres clés pour eux ; configure-les dans ton runtime, pas dans Config.

Glossaire : écriveur en flux · sous-ensemblage de polices