Du thème du magazine à la pratique des projets
Pages de services et techniques pertinentes pour l'article
Quand une Delphi-application en production gonfle lentement, plante sporadiquement avec des Access Violations ou devient soudainement instable après plusieurs jours d’exécution, il ne s’agit souvent pas d’un bug isolé, mais d’un schéma : de la mémoire est allouée sans être correctement libérée — ou elle est libérée trop tôt puis réutilisée. C’est précisément là que FastMM FullDebugMode est précieux. Pas comme état permanent, mais comme outil de diagnostic ciblé qui transforme un « quelque chose est cassé dans le heap » en une cause traçable.
Le revers de la médaille : FullDebugMode génère beaucoup de sortie, impacte les performances et conduit rapidement à des interprétations erronées. Un rapport de fuites n’indique pas automatiquement l’endroit où se trouve « l’erreur ». Et une pile d’appels n’est utile que dans la mesure où la résolution des symboles est correcte (MAP-Datei, Debug-Infos, inlining). Dans cet article, je passe en revue le cas limite typique, j’explique l’approche propre et les pièges — afin qu’à la fin tu ne trouves pas seulement des fuites, mais que tu les élimines de façon durable.
Quand FastMM FullDebugMode est vraiment pertinent
FastMM est souvent déjà le gestionnaire de mémoire par défaut dans les versions modernes de Delphi ou est intégré dans de nombreux projets. Le FullDebugMode est toutefois une configuration particulière : il marque les blocs mémoire avec des motifs de contrôle supplémentaires, collecte des stacktraces d’allocation et vérifie de manière plus agressive la corruption du heap (c’est‑à‑dire les données de gestion du heap endommagées, par ex. par des dépassements de tampon).
J’utilise FullDebugMode de façon ciblée quand l’un des cas suivants se présente :
- Fuite reproductible : la consommation mémoire augmente dans un test par opération (p. ex. par requête, par import, par action UI).
- Access Violations sporadiques : en particulier celles qui surviennent « tantôt ici, tantôt là » dans la même zone (classique : use-after-free).
- Corruption du heap : messages comme « Invalid pointer operation », « Access violation in ntdll » ou plantages lors de l’arrêt/finalisation.
- Recherche de régressions : instabilité nouvelle après refactoring, mise à jour de bibliothèque ou changement de compilateur.
Le FullDebugMode n’a pas lieu d’être en « on l’active dans toutes les builds ». Le surcoût est élevé, le timing change, et des conditions de course peuvent disparaître ou se déplacer. Pour l’exploitation continue, un monitoring léger (p. ex. Working Set du processus, Private Bytes, compteurs par opération) est plus approprié — FullDebugMode est le scalpel, pas le stéthoscope.
Principe de base : le rapport de fuite est un symptôme, la stacktrace est une piste
Un rapport de fuites montre d’abord : ces blocs sont encore alloués à la fin du programme. Ce n’est automatiquement un problème que si ces blocs auraient dû être libérés. Il existe des « fuites » légitimes : singletons globaux, caches, handles OS avec durée de vie du processus ou bibliothèques tierces qui n’effectuent pas la finalisation volontairement. Tu veux connaître ces cas, mais ne pas les « corriger » aveuglément.
La stacktrace dans le rapport montre l’endroit où le bloc a été alloué. Ce n’est souvent pas l’endroit où tu as « oublié le Free ». Réalité fréquente dans les systèmes évolués :
- Allocation dans la couche UI ou service, la libération devrait avoir lieu dans une couche plus basse (ownership incertain).
- Allocation dans une factory, ownership transféré à l’appelant — mais l’appelant pense que c’est « owned ».
- Des objets sont conservés dans des collections (listes, dictionnaires), mais le modèle d’ownership n’est pas cohérent.
- Un chemin d’exception saute le nettoyage, parce que try/finally manque ou commence trop tard.
La procédure propre est donc : reproduzieren → isolieren → Stacktrace auflösen → Ownership-Fehler finden → Fix mit Regressionstest. FastMM liefert dir die Spuren, aber du musst sie in Architektur und Lebenszyklen übersetzen.
FastMM FullDebugMode sauber aktivieren (ohne Nebenwirkungen zu übersehen)
Der FullDebugMode wird in der Praxis über die FastMM-Optionen und eine passende FastMM-Konfiguration aktiviert. Entscheidend ist weniger „wie genau heißt die Include-Datei“, sondern was die Konfiguration bewirkt und unter welchen Build-Bedingungen du sie nutzt.
Empfohlene Rahmenbedingungen für den Debug-Build
- Debug DCUs und Debug-Infos: Stacktraces sind nur nützlich, wenn sie auf echte Unit/Zeile/Adresse auflösbar sind. Stelle sicher, dass Debug-Informationen erzeugt werden und eine MAP-Datei verfügbar ist.
- Optimierung bewusst wählen: Für Stacktrace-Lesbarkeit ist ein nicht-optimierter Build meist besser. Inlining und aggressive Optimierung können Stackframes „verwischen“.
- Gleiche Laufzeitbedingungen: Nutze möglichst gleiche Daten, gleiche Konfiguration, gleiche Rechte. Viele Leaks sind datenabhängig (z. B. seltene Formate, Sonderpfade).
- 64-bit vs. 32-bit trennen: Speicherverhalten, Alignment und Drittbibliotheken unterscheiden sich. Debugge in der Zielplattform, in der das Problem auftritt.
Ein Punkt, den Admins und technische Leads oft unterschätzen: FullDebugMode kann auch Timing verändern. Wenn du Threading im Spiel hast, können Race-Conditions dadurch anders auftreten. Deshalb ist es sinnvoll, parallel auch einen Lauf ohne FullDebugMode zu haben, der nur die Reproduktion bestätigt. FullDebugMode ist dann der Schritt zur Diagnose.
Vorsicht bei „ReportMemoryLeaksOnShutdown“
Delphi kann über ReportMemoryLeaksOnShutdown Leaks beim Programmende melden. Das ist praktisch, aber in komplexen Anwendungen (Services, Plug-in Host, lange Laufzeiten) kann es trügen: Beim Shutdown laufen Finalization-Abschnitte, Threads stoppen, Caches werden weggeräumt. Ein Leak, das in der Mitte der Laufzeit kritisch ist, kann bis zum Ende wieder verschwinden – oder umgekehrt: ein scheinbarer Leak entsteht erst im Shutdown, weil noch Hintergrundarbeit läuft.
Für praxistaugliches Leak-Hunting ist daher wichtiger: Leak pro Vorgang messen (z. B. nach 100 Requests), nicht nur beim Beenden. FastMM kann dabei helfen, aber der Testaufbau muss das abbilden.
Der typische Randfall: Leak-Report zeigt „irgendein Objekt“, aber die Ursache ist Ownership
Un classique des applications d’entreprise : un processus d’importation crée, par enregistrement, des objets auxiliaires (p. ex. StringLists, JSON-Parser, listes temporaires). Sur le happy path, ils sont correctement libérés. Dans de rares cas (saut lié à une validation, exception, sortie anticipée), un objet reste coincé. Après 10 000 enregistrements, cela devient visible.
FastMM FullDebugMode aide ici, car il indique le point d’allocation. Mais le « fix » n’est pas de « free à l’endroit de l’allocation ». Le correctif est un pattern d’ownership robuste :
- Celui qui crée un objet n’en est pas automatiquement l’owner.
- L’ownership doit être clairement définie dans le contrat de l’API (paramètres/retour, documentation, conventions de nommage).
- Les collections doivent être explicites : owning vs. non-owning. Les formes mixtes entraînent des problèmes.
- Les chemins d’exception exigent des blocs try/finally précoces.
Si, dans le stacktrace, tu ne vois que « TStringList.Create », l’information n’est pas dépourvue de valeur — mais elle te dit seulement : quelque chose est créé ici. La question est : où cela devrait-il se terminer ? Et c’est là que la réflexion architecturale aide plus que l’acrobatie du débogueur.
Lire correctement les stacktraces : ce que tu peux réellement en déduire
Un stacktrace produit par FastMM est en général une liste d’adresses de retour qui — avec les symboles de débogage — se mappent sur des Units, des procédures et idéalement des numéros de ligne. Quand tu lis cela, trois choses sont décisives :
- Le haut de la pile n’est pas toujours l’erreur : les frames supérieurs sont souvent le gestionnaire mémoire/RTL. C’est intéressant là où ton code commence.
- Chaîne d’appels plutôt que ligne isolée : la ligne n’est qu’un point. La chaîne te montre quel chemin a mené à l’allocation.
- Plusieurs blocs identiques : si FastMM signale plusieurs fuites de taille identique, il s’agit souvent d’un chemin récurrent. C’est positif : tu as de la reproductibilité.
Quand les numéros de ligne manquent : fichier MAP, packages, Release-DCUs
Beaucoup d’équipes butent ici : FullDebugMode est actif, le rapport de fuites arrive, mais au lieu d’unité/ligne il n’y a que des adresses ou des symboles cryptiques. Causes typiques :
- Pas de fichier MAP ou les informations de debug n’ont pas été générées.
- Tu cibles des Release-DCUs ou des DLLs tierces sans symboles.
- L’application utilise des runtime packages : alors une partie du code se trouve dans des BPL, et la résolution des symboles doit être adaptée.
- L’optimisation/Inlining a rendu le stacktrace moins lisible.
En pratique, cela signifie : pour la chasse aux fuites, tu as besoin d’un build délibérément « permettant le diagnostic ». C’est un objectif différent de « le plus rapide possible ». Les responsables techniques devraient traiter cela comme un profil de build distinct, afin qu’aucun membre de l’équipe ne modifie ad hoc les options du projet.
Évaluer les frames : « intéressant » est souvent une ligne plus haut
Un exemple tiré de la réalité (sans code client concret) : le stacktrace montre, dans le premier frame de votre code, une routine „LoadConfig“. Vous y voyez une création d’objet. Vous ajoutez un Free, la fuite disparaît – et soudain ça plante ailleurs avec Double Free. Pourquoi ? Parce que „LoadConfig“ place l’objet dans un Cache, et un autre chemin de code est déjà Owner et nettoie plus tard.
La lecture correcte aurait été : le stacktrace vous indique, où le block est créé. La correction se situe souvent dans la définition : qui possède l’objet après le Return ? Si vous ne répondez pas clairement à cette question, vous ne faites que modifier le tableau des erreurs (Leak → AV).
Corruption du tas vs. Leak : pourquoi FullDebugMode trouve souvent le véritable coupable
Beaucoup de « Leaks » sont en réalité des problèmes secondaires : un dépassement de tampon écrase les métadonnées du tas, le gestionnaire de mémoire ne peut plus libérer correctement, et au final vous observez des fuites apparemment aléatoires ou des Invalid Pointer Operations. FullDebugMode est efficace ici, car il utilise des motifs de vérification et effectue des validations supplémentaires lors des Free/Reuse.
Il est important de distinguer :
- Leak : le block a été alloué et jamais libéré. La stabilité se dégrade au fil du temps, le crash n’est pas nécessairement immédiat.
- Use-after-free : le block est libéré mais ensuite encore utilisé. Conduit à des AV sporadiques, difficiles à reproduire.
- Double Free : le block est libéré deux fois. Peut planter immédiatement ou plus tard (si le block a été réutilisé).
- Corruption du tas : quelqu’un écrit au-delà des limites d’un block. Les symptômes apparaissent souvent avec retard.
FullDebugMode est particulièrement utile lorsque vous observez des symptômes avec retard. La validation supplémentaire rend les erreurs visibles plus tôt – souvent précisément à l’endroit où l’accès erroné a lieu, et pas seulement des minutes plus tard dans un Free quelconque.
Procédure dans les projets : chasse reproductible aux fuites plutôt que « débogage dans le brouillard »
Si vous voulez traquer les fuites mémoire, vous avez besoin d’un procédé reproductible et partageable en équipe. J’aime travailler avec un cadre de diagnostic fixe :
1) Reproduction dans un scénario déterministe
Définissez une séquence de test qui montre la fuite de façon fiable : „Démarrer le Service, traiter 500 Nachrichten, arrêter le Service“ ou „Ouvrir Maske X, exécuter Aktion Y 200-mal“. Il est important de documenter la séquence avec ses paramètres (Datenbestand, Mandant, Feature-Flags), afin que d’autres puissent la reproduire.
2) Minimiser : rendre la fuite visible à chaque étape
Si la séquence dure 20 minutes, segmentez-la. L’objectif est : pouvoir comparer « avant » et « après » le plus rapidement possible. Dans les grosses applications, c’est souvent cela qui consomme le plus de temps, pas la correction elle-même.
3) Activer FullDebugMode et interpréter le rapport
C’est là que FastMM FullDebugMode entre en jeu. Rassemblez les rapports, regroupez par taille de bloc/pile d’appels et recherchez les répétitions. Un seul bloc résiduel peut être un cache légitime. 10 000 blocs identiques sont presque toujours une fuite réelle.
4) Clarification de l’ownership et correction dans la couche appropriée
Corrigez les fuites là où l’ownership est définie : Factory, contrat d’API, Collection-Wrapper. «Ajouter un Free rapide» juste à côté du Create est souvent le mauvais endroit si l’objet est transmis.
5) Régression : même séquence, même build, même rapport
Le correctif n’est valable que lorsque la séquence s’exécute à nouveau sans fuites ni nouvelles erreurs mémoire. Surtout pour les use-after-free, la disparition d’une fuite n’est pas une preuve, mais seulement un nouveau symptôme.
Pièges typiques dans le code Delphi que FastMM met en évidence
Collections et ownership (listes, dictionnaires, interfaces)
Beaucoup de fuites ne proviennent pas d’algorithmes complexes, mais de structures de données courantes. Deux cas d’erreur classiques :
- Une liste contient des objets, mais personne ne sait qui les libère. Solution : utiliser une liste owning ou vider systématiquement dans le finally.
- Un dictionnaire conserve des objets en tant que valeurs ; lors d’un Remove la valeur n’est pas libérée ou lors d’un Clear elle est oubliée.
Les interfaces sont de plus délicates : le comptage de références (semblable à ARC) est confortable, mais un fonctionnement mixte avec l’ownership d’objet peut générer des fuites en cas de références cycliques ou d’événements. FullDebugMode montre souvent le chemin d’allocation, mais la cause est un cycle de références (A retient B via interface, B retient A via callback).
Exceptions et sorties anticipées
Dans des systèmes logiciels d’entreprise évolués, les exceptions font souvent partie du flux normal de contrôle (p. ex. validation, annulation, retry). Le problème est rarement l’exception elle-même, mais le chemin qui l’entoure : un objet est créé avant le try/finally, une exception est levée, et le nettoyage est sauté. FullDebugMode fournit la stacktrace de l’allocation — il faut vérifier s’il existe un chemin de libération garanti.
Threads et durée de vie : « libération dans le mauvais thread »
Avec VCL/FMX et des services utilisant des worker-threads survient un autre cas limite : un objet est créé dans un thread mais libéré dans le thread UI (ou inversement), parce qu’on transmet « rapidement » quelque chose via Queue/Synchronize. Cela peut marcher, mais peut aussi conduire à un use-after-free si le producteur continue de travailler pendant que le consommateur libère déjà.
FastMM FullDebugMode peut aider ici, car il détecte plus tôt les erreurs retardées. Le correctif réel est cependant un modèle de durée de vie propre : règles de possession claires, transfert uniquement via des données immuables ou points de transfert d’ownership explicites.
Comment rendre les rapports exploitables : filtrer, comparer, documenter
Au sein des équipes, il vaut la peine de ne pas se contenter de « regarder » les rapports de fuites, mais de les traiter comme un artefact. Trois mesures pragmatiques qui ont fait leurs preuves :
- Baseline-Report : un « état connu » (p. ex. version produit actuelle) est exécuté une fois avec FullDebugMode et conservé comme référence. Ainsi, vous repérez immédiatement les nouvelles fuites.
- Comparaison par use-case : pour les workflows critiques (import, export, requête API, opérations de masse UI), définissez pour chacun une courte séquence reproductible régulièrement.
- Fuites « légitimes » documentées : si un cache n’est volontairement pas finalisé, documentez-le. Sinon, dans six mois quelqu’un relancera les mêmes entrées.
Ce n’est pas de la bureaucratie, mais un gain de temps : la chasse aux fuites devient sinon vite une boucle sans fin, car les mêmes schémas réapparaissent à chaque sprint.
Quand l’effort vaut-il la peine — et quand devriez-vous procéder autrement
FastMM FullDebugMode est un outil de diagnostic qui a un coût. L’effort en vaut particulièrement la peine lorsque :
- l’application fonctionne longtemps (service, client Terminalserver, système de rotation, processus 24/7).
- vous traitez des flux de données clients réels et ne couvrez pas tous les chemins en test.
- la stabilité prime sur la rapidité d’implémentation de fonctionnalités à court terme (typique des solutions logicielles proches des processus).
En revanche, si vous n’avez qu’un petit utilitaire de bureau qui se termine au bout de 30 secondes, la chasse aux fuites est souvent secondaire. De même : si vous avez un problème ponctuel de pic mémoire (p. ex. une grosse exportation), il s’agit souvent d’une question de stratégie de streaming et de charge de pointe dans le heap, et non d’une fuite.
Conclusion pratique : FullDebugMode n’est pas un interrupteur, mais un processus
FastMM FullDebugMode apporte de la structure à la recherche d’erreurs mémoire : il rend visibles les allocations, détecte plus tôt la corruption du heap et fournit des stacktraces permettant de corriger la cause plutôt que le symptôme. Le levier décisif n’est toutefois pas l’outil, mais la procédure : scénarios reproductibles, builds diagnostiques, contrats d’ownership clairs et régression par rapport à une baseline.
Si vous êtes coincé sur une fuite tenace ou une erreur sporadique du heap et que vous voulez stabiliser durablement le sujet dans un système Delphi plus vaste, un court setup de diagnostic propre avec une séquence claire et des rapports exploitables en vaut la peine. Si vous avez besoin d’assistance pour l’analyse, les profils de build ou le refactoring d’architecture : contactez Net-Base Software GmbH.
Pour ce sujet, la recherche de fuites Delphi et la lecture du Fastmm Leak Report sont aussi importantes. L’article remet ces aspects en perspective de manière compréhensible et montre ce qui importe au quotidien.
Étape suivante
Lorsque le sujet devient un projet réel, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.
Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.
- L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
- REST, l’accès aux données, les portails et le déploiement ne sont pas reportés à des phases ultérieures.
- Vous identifiez tôt quelle voie est viable économiquement et opérationnellement.