La fuite GDI (ou simplement l'utilisation d'un trop grand nombre d'objets GDI) est l'un des problèmes les plus courants. Cela finit par entraîner des problèmes de rendu, des erreurs et/ou des problèmes de performances. L'article décrit comment nous déboguons ce problème.
En 2016, alors que la plupart des programmes sont exécutés dans des bacs à sable d'où même le développeur le plus incompétent ne peut pas nuire au système, je suis étonné de faire face au problème dont je vais parler dans cet article. Franchement, j'espérais que ce problème avait disparu pour toujours avec Win32Api. Néanmoins, j'y ai fait face. Avant cela, j'ai juste entendu des histoires d'horreur à ce sujet de la part d'anciens développeurs plus expérimentés.
Le problème
Fuite ou utilisation de l'énorme quantité d'objets GDI.
Symptômes
- La colonne Objets GDI de l'onglet Détails du Gestionnaire des tâches affiche la valeur critique 10 000 (si cette colonne est absente, vous pouvez l'ajouter en cliquant avec le bouton droit sur l'en-tête du tableau et en sélectionnant Sélectionner les colonnes).
- Lorsque vous développez en C# ou dans d'autres langages exécutés par CLR, l'erreur peu informative suivante se produit :
Message :Une erreur générique s'est produite dans GDI+.
Source :System.Drawing
Site cible :IntPtr GetHbitmap(System.Drawing.Color)
Type :System.Runtime.InteropServices.ExternalException
L'erreur peut ne pas se produire avec certains paramètres ou dans certaines versions du système, mais votre application ne pourra pas afficher un seul objet : - Pendant le développement dans С/С++, toutes les méthodes GDI, comme Create%SOME_GDI_OBJECT%, ont commencé à renvoyer NULL.
Pourquoi ?
Les systèmes Windows ne permettent pas de créer plus de 65 535 Objets GDI. Ce nombre, en fait, est impressionnant et je peux difficilement imaginer un scénario normal nécessitant une telle quantité d'objets. Il y a une limite pour les processus - 10000 par processus qui peuvent être modifiés (en changeant le HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota valeur comprise entre 256 et 65535), mais Microsoft ne recommande pas d'augmenter cette limite. Si vous continuez à le faire, un processus pourra geler le système afin qu'il ne puisse même pas afficher le message d'erreur. Dans ce cas, le système ne peut être relancé qu'après le redémarrage.
Comment réparer ?
Si vous vivez dans un monde CLR confortable et géré, il y a de fortes chances que vous ayez une fuite de mémoire habituelle dans votre application. Le problème est désagréable, mais c'est un cas tout à fait ordinaire. Il existe au moins une douzaine d'excellents outils pour détecter cela. Vous devrez utiliser n'importe quel profileur pour voir si le nombre d'objets qui encapsulent les ressources GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) augmente. Si c'est le cas, vous pouvez arrêter de lire cet article. Si la fuite d'objets wrapper n'a pas été détectée, votre code utilise directement l'API GDI et il existe un scénario lorsqu'ils ne sont pas supprimés
Que recommandent les autres ?
Les conseils officiels de Microsoft ou d'autres articles sur ce sujet vous recommanderont quelque chose comme ceci :
Trouver tout Créer %SOME_GDI_OBJECT% et détecter si le DeleteObject correspondant (ou ReleaseDC pour les objets HDC) existe. Si tel DeleteObject existe, il peut y avoir un scénario qui ne l'appelle pas.
Il existe une version légèrement améliorée de cette méthode qui contient une étape supplémentaire :
Téléchargez l'utilitaire GDIView. Il peut afficher le nombre exact d'objets GDI par type. Notez que le nombre total d'objets ne correspond pas à la valeur de la dernière colonne. Mais nous pouvons fermer les yeux sur cela si cela aide à réduire le champ de recherche.
Le projet sur lequel je travaille a la base de code de 9 millions d'enregistrements, environ le même nombre d'enregistrements se trouve dans les bibliothèques tierces, des centaines d'appels de la fonction GDI qui sont répartis sur des dizaines de fichiers. J'avais perdu beaucoup de temps et d'énergie avant de comprendre qu'une analyse manuelle sans fautes est impossible.
Que puis-je offrir ?
Si cette méthode vous paraît trop longue et fastidieuse, vous n'avez pas franchi toutes les étapes du désespoir avec la précédente. Vous pouvez essayer de suivre les étapes précédentes, mais si cela ne vous aide pas, n'oubliez pas cette solution.
À la poursuite de la fuite, je me suis demandé :Où sont créés les objets qui fuient ? Il était impossible de définir des points d'arrêt à tous les endroits où la fonction API est appelée. De plus, je n'étais pas sûr que cela ne se produise pas dans le .NET Framework ou dans l'une des bibliothèques tierces que nous utilisons. Quelques minutes de recherche sur Google m'ont conduit à l'utilitaire API Monitor qui permettait de consigner et de tracer les appels à toutes les fonctions du système. J'ai facilement trouvé la liste de toutes les fonctions qui génèrent des objets GDI, je les ai localisées et sélectionnées dans API Monitor. Ensuite, je fixe des points d'arrêt.
Après cela, j'ai exécuté le processus de débogage dans Visual Studio et l'avez sélectionné dans l'arborescence des processus. Le cinquième point d'arrêt a fonctionné immédiatement :
J'ai compris que j'allais me noyer dans ce torrent et qu'il me fallait autre chose. J'ai supprimé les points d'arrêt des fonctions et j'ai décidé d'afficher le journal. Il a montré des milliers d'appels. Il est devenu clair que je ne pourrai pas les analyser manuellement.
La tâche consiste à Rechercher les appels des fonctions GDI qui ne provoquent pas la suppression . Le journal contenait tout ce dont j'avais besoin :la liste des appels de fonction dans l'ordre chronologique, leurs valeurs renvoyées et les paramètres. Par conséquent, j'avais besoin d'obtenir une valeur renvoyée de la fonction Create%SOME_GDI_OBJECT% et de trouver l'appel de DeleteObject avec cette valeur comme argument. J'ai sélectionné tous les enregistrements dans API Monitor, je les ai insérés dans un fichier texte et j'ai obtenu quelque chose comme CSV avec le délimiteur TAB. J'ai exécuté VS, où j'avais l'intention d'écrire un petit programme pour l'analyse, mais avant qu'il ne puisse se charger, une meilleure idée m'est venue à l'esprit :exporter des données dans une base de données et écrire une requête pour trouver ce dont j'ai besoin. C'était le bon choix car cela m'a permis de poser rapidement des questions et d'obtenir des réponses.
Il existe de nombreux outils pour importer des données de CSV vers une base de données, je ne m'attarderai donc pas sur ce sujet (mysql, mssql, sqlite).
J'ai le tableau suivant :
CREATE TABLE apicalls ( id int(11) DEFAULT NULL, `Time of Day` datetime DEFAULT NULL, Thread int(11) DEFAULT NULL, Module varchar(50) DEFAULT NULL, API varchar(200) DEFAULT NULL, `Return Value` varchar(50) DEFAULT NULL, Error varchar(100) DEFAULT NULL, Duration varchar(50) DEFAULT NULL )
J'ai écrit la fonction MySQL suivante pour obtenir le descripteur de l'objet supprimé à partir de l'appel API :
CREATE FUNCTION getHandle(api varchar(1000)) RETURNS varchar(100) CHARSET utf8 BEGIN DECLARE start int(11); DECLARE result varchar(100); SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )' IF start = 0 THEN SET start := INSTR(api, '('); END IF; SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1); RETURN TRIM(result); END
Et enfin, j'ai écrit une requête pour localiser tous les objets courants :
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates LEFT JOIN (SELECT d.id, d.API, getHandle(d.API) handle FROM apicalls d WHERE API LIKE 'DeleteObject%' OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels ON dels.handle = creates.handle WHERE creates.API LIKE 'Create%';
(En gros, il trouvera simplement tous les appels de suppression pour tous les appels de création).
Comme vous le voyez sur l'image ci-dessus, tous les appels sans une seule suppression ont été trouvés en même temps.
Donc, la dernière question a été laissée :Comment déterminer, d'où ces méthodes sont-elles appelées dans le contexte de mon code ? Et ici, une astuce fantaisiste m'a aidé :
- Exécuter l'application dans VS pour le débogage
- Recherchez-le dans Api Monitor et sélectionnez-le.
- Sélectionnez une fonction requise dans l'API et placez un point d'arrêt.
- Continuez à cliquer sur "Suivant" jusqu'à ce qu'il soit appelé avec les paramètres en question (j'ai vraiment raté les points d'arrêt conditionnels de VS)
- Lorsque vous arrivez à l'appel requis, passez à CS et cliquez sur Tout casser .
- VS Debugger sera arrêté là où l'objet qui fuit est créé et tout ce que vous avez à faire est de découvrir pourquoi il n'est pas supprimé.
Remarque :Le code est écrit à des fins d'illustration.
Résumé :
L'algorithme décrit est compliqué et nécessite de nombreux outils, mais il a donné le résultat beaucoup plus rapidement par rapport à une recherche stupide dans l'énorme base de code.
Voici un résumé de toutes les étapes :
- Recherchez les fuites de mémoire des objets wrapper GDI.
- S'ils existent, éliminez-les et répétez l'étape 1.
- S'il n'y a pas de fuites, recherchez explicitement les appels aux fonctions de l'API.
- Si leur quantité n'est pas importante, recherchez un script dans lequel un objet n'est pas supprimé.
- Si leur quantité est importante ou s'ils sont difficilement traçables, téléchargez API Monitor et configurez-le pour enregistrer les appels des fonctions GDI.
- Exécutez l'application pour le débogage dans VS.
- Reproduire la fuite (cela initialisera le programme afin de cacher les objets encaissés).
- Connectez-vous avec API Monitor.
- Reproduire la fuite.
- Copiez le journal dans un fichier texte, importez-le dans n'importe quelle base de données à portée de main (les scripts présentés dans cet article sont pour MySQL, mais ils peuvent être facilement adoptés pour n'importe quel système de gestion de base de données relationnelle).
- Comparez les méthodes Create et Delete (vous pouvez trouver le script SQL dans cet article ci-dessus) et recherchez les méthodes sans les appels Delete.
- Définissez un point d'arrêt dans API Monitor lors de l'appel de la méthode requise.
- Continuez à cliquer sur Continuer jusqu'à ce que la méthode soit appelée avec les paramètres réacquis.
- Lorsque la méthode est appelée avec les paramètres requis, cliquez sur Tout casser dans VS.
- Découvrez pourquoi cet objet n'est pas supprimé.
J'espère que cet article vous sera utile et vous aidera à gagner du temps.