Un client s'est récemment plaint d'un gros memory leak dans la version Linux de Yocto-Visualization. On a d'abord trouvé ça un peu étonnant dans la mesure où, chez Yoctopuce, il y a plusieurs expériences qui sont surveillées H24 7/7 par des instances de Yocto-Visualization installées sur des Raspberry Pi. Mais vérification faite, il y a effectivement un gros problème avec la version actuelle de Mono 6.
Plus exactement, le problème vient de la librairie libgdiplus qui est installée avec Mono 6: elle contient un vilain bug qui cause ce memory leak. Il se trouve que la façon de régler ce problème est suffisamment subtile pour qu'on se dise que ça valait la peine d'en parler dans un article.
Mise en évidence
Il ne nous a pas fallu longtemps pour déterminer que le memory leak se manifestait dans la partie "renderer" de Yocto-Visualization. Plus exactement, le problème apparaît dans le code de double-buffering. Pour le mettre en évidence, on a écrit une petite application C# qui crée une animation dans un élément "picturebox". Chaque frame est dessiné par une fonction appelée 25 fois par seconde grâce à un timer. Voici le code de la fonction en question:
{
int w = pictureBox1.Size.Width;
int h = pictureBox1.Size.Height;
int centerX = w >> 1;
int centerY = h >> 1;
Bitmap DrawArea = new Bitmap(w, h);
pictureBox1.Image = DrawArea;
Graphics g = Graphics.FromImage(DrawArea);
g.FillRectangle(whiteBrush, new Rectangle(0, 0, w, h));
double radius = Math.Min(0.9 * w / 2, 0.9 * h / 2);
for (int i=0;i<5;i++)
{ double angle1 = Math.PI * (i * 72 + ofset) / 180;
double angle2 = Math.PI * ((i+2) * 72 + ofset) / 180;
double x1 = centerX + radius * Math.Cos(angle1);
double y1 = centerY + radius * Math.Sin(angle1);
double x2 = centerX + radius * Math.Cos(angle2);
double y2 = centerY + radius * Math.Sin(angle2);
g.DrawLine(blackPen,(float) x1, (float)y1, (float)x2, (float)y2);
}
g.Dispose();
ofset++;
}
Voici le résultat:
Une petite animation toute simple
Si on lance cette application sous Windows, ça fonctionne indéfiniment, en dents de scie certes, mais ça fonctionne.
Utilisation de la mémoire sous Windows (Process Explorer)
Par contre si on la lance avec Mono 6 sous Linux, elle accapare toute la mémoire et finit par planter au bout de quelques minutes. Plus la fenêtre est grande, plus ça crashe vite.
Utilisation de la mémoire sous Linux (System Monitor)
Le problème vient exactement de la ligne
Cette ligne sert à affecter le nouveau buffer bitmap lors du calcul de l'animation. L'ancien buffer n'étant plus référencé, le ramasseur de miette (aka garbage collector) est censé s'en rendre compte au bout d'un moment, et libérer la mémoire correspondante. Sauf qu'avec Mono 6, ça ne marche pas. On a eu l'idée de rajouter un peu de code pour signaler explicitement au ramasseur de pièce qu'on n'a plus besoin l'ancien buffer:
pictureBox1.Image = DrawArea;
if (previous!=null) previous.Dispose();
Assez logiquement, cela permet de lisser et de diminuer la consommation mémoire sous Windows.
La consommation mémoire sous Windows est lissée
Malheureusement, ce changement de code n'a strictement aucun effet dans la version Linux qui continue de planter lamentablement au bout de quelques dizaines de secondes.
En fait, ce memory leak a été plus ou moins corrigé en avril dernier dans la version 6.0.5 de libgdiplus. Fâcheusement, à l'heure où on écrit ces lignes, cette version corrigée n'est disponible que sous la forme de code source. Il faudra donc les télécharger et les compiler vous-même.
Corriger le problème
Vérifier la version actuelle
Si votre installation Linux de Yocto-Visualization crashe au bout de quelques minutes, la première chose à faire consiste à vérifier que vous avez la version incriminée de Mono avec la commande mono -v (pour rappel la commande pour installer Mono est "sudo apt install mono-complete"). Dans la capture d'écran ci-dessous on voit qu'on a la version 6.
On a la version 6 de Mono
Il s'agit ensuite de vérifier la version de libgdiplus qui a été installée en même temps que "mono-complete" dans "/usr/lib". Normalement le numéro de version est explicitement donné dans le nom de fichier, mais ici on le fichier est bêtement nommé libgdiplus.so.0.0.0. On peut cependant utiliser une méthode non conventionnelle pour trouver la version du fichier en cherchant la chaîne de caractères "6.0." dans la fichier avec la commande "strings /ust/lib/lingdiplus.so | grep "6\.0\.". Dans la capture d'écran ci-dessous, on voit qu'on a la version "6.0.4" alors qu'il nous faudrait au moins la 6.0.5
On a la version 6.0.4
Télécharger les sources de libgdiplus
Vous trouverez les sources de libgdiplus sur le site de Mono. N'utilisez surtout pas la version disponible sur GitHub, elle est incomplète: si vous essayez de la compiler vous allez vous retrouver avec le message d'erreur "Error: Couldn't find the required submodules. This usually happens when using an archive from GitHub".
Téléchargez donc l'archive libgdiplus-6.0.5.tar.gz, décompressez-la où bon vous semble, vous devriez vous retrouver avec un répertoire libgdiplus-6.0.5.
Avant de compiler, vous avez aussi besoin d'installer les dépendances de cette librairie, pour cela, tapez la ligne de commande suivante:
sudo apt-get install libgif-dev autoconf libtool automake build-essential gettext libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
Compiler les sources de libgdiplus
Placez-vous maintenant dans le répertoire libgdiplus-6.0.5 et tapez la commande suivante:
./configure
ensuite tapez :
make
Normalement votre librairie devrait être prête.
Installation
Pour installer votre librairie fraîchement compilée, vous avez à taper
sudo make install
Votre librairie est désormais installée, mais dans le répertoire /usr/local/lib alors que Mono va la chercher dans /usr/lib grâce à deux liens symboliques. Il faut donc effacer ces deux liens et les remplacer par des nouveaux pointant sur la nouvelle version:
cd /usr/lib sudo rm libgdiplus.so sudo rm libgdiplus.so.0 sudo ln -s /usr/local/lib/libgdiplus.so.0.0.0 libgdiplus.so sudo ln -s /usr/local/lib/libgdiplus.so.0.0.0 libgdiplus.so.0
Enfin vous pouvez vérifier que ça marche en retapant la commande
strings /usr/lib/libgdiplus.so | grep "6\.0\."
qui cette fois devait vous rendre "?6.0.5"
Test
Maintenant, si on relance notre animation de test, ça ne monopolise plus toute la mémoire, mais seulement à condition de lancer la version qui indique explicitement au ramasseur de miettes qu'il doit libérer la mémoire des bitmaps non utilisés. Apparemment il n'est toujours pas capable de s'en rendre compte tout seul.
Tadaaa! plus de memory leak!
Conclusion
Si vous avez ce problème de memory leak sous Linux, vous avez faire deux choses à faire pour régler le problème:
- Installer la librairie libgdiplus 6.0.5, quitte à la compiler vous-même
- Installer la version 43172 de Yocto-Visualization qui contient le code pour demander aux ramasseur de miettes d'arrêter de procrastiner.
Sous Windows, installer la dernière version de Yocto-Visualization vous sera aussi bénéfique: la consommation de mémoire est fortement diminuée grâce à l'élimination de ces dents de scie.