Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Silverlight 5 – Fuite mémoire avec les images (une solution)

[new:20/04/2013]Utiliser Silverlight 5 n’est pas forcément une option puisque même si vous êtes content de la version 3 ou 4 et que vos logiciels marchent bien sous ces versions vos utilisateurs ont forcément reçus une mise à jour vers la version 5 qui ruinera peut-être ces applications qui pourtant tournaient correctement jusqu’à lors…

Bogue fantôme ?

Le code incriminé est très simple et se retrouve dans de nombreuses applications Silverlight dès lors qu’elles manipulent des images.

Les premières informations sur ce bogue ne sont pas si récentes puisque déjà en 2009, Jeff Prosise indiquait avoir rencontré le problème et fournissait des exemples (repris ici). Puis les choses semblent s’être tassées étrangement. Mais la diffusion de SL 5 a relancé le débat avec des utilisateurs rapportant le même genre de problème (out of memory) dans des applications manipulant des images. Il y a deux jours sur la liste de diffusion “Silverlight List” (liste réservées aux personnes sous NDA) quelques MVP ont rapporté des ennuis similaires arrivés à leurs clients ou contacts.

Bogue fantôme ? Fluctuations quantique du vide ? je ne saurai le dire. Quoi qu’il en soit le nombre de personnes rapportant ce problème est reparti à la hausse depuis la sortie de SL 5.

Un code simple

Comme je le disais, le code permettant de mettre le bogue en évidence est d’une grande simplicité et peut se retrouver sous une forme ou autre dans toute application qui manipule des images.

private Image CreateThumbnailImage(Stream stream, int width)
{
    BitmapImage bi = new BitmapImage();
    bi.SetSource(stream);
 
    double cx = width;
    double cy = bi.PixelHeight * (cx / bi.PixelWidth);
           
    Image image = new Image();
    image.Width = cx;
    image.Height = cy;
    image.Source = bi;
 
    return image;
}

 

Quand je vous dis que c’est un code simple, c’est vraiment simple. Cet exemple est une méthode qui créée une miniature d’une certaine largeur à partir d’un flux (stream) contenant une image. Peu importe d’où vient le flux (téléchargement internet par exemple ou lecture disque).

Le problème ?

Il est tout simple lui aussi : out of memory.

Simple comme une lame de rasoir, tranchant comme une guillotine toute neuve, incontournable comme un éléphant coincé entre les murs d’une rue trop étroite. L’application plante sans autre forme de procès.

L’exemple porte sur la création de miniatures car c’est un cas où les choses tourneront vite au vinaigre. Avec deux ou trois images manipulées par l’application le bogue passera inaperçu, mais en balayant tout un répertoire pour créer des miniatures la mémoire va rapidement exploser !

La classe BitmapImage nous rappelle que nous manipulons des images toujours plus grosses dont nous avons oublié la taille réelle… Un Jpeg de 2 Mo ne pèse pas 2 Mo, c’est son image disque compressée qui fait ce poids. Mais pour l’afficher ou la traiter il faudra bien en reconstituer chaque pixel en mémoire… et c’est là que BitmapImage nous rappelle que ces 2 Mo tout à fait raisonnables prendront peut-être 40 à 50 Mo en RAM !

Cela n’est malgré tout pas grand chose après tout. D’abord les PC modernes disposent de plusieurs Go de mémoire et .NET et son Garbage Collector font le ménage de façon efficace.

C’est vrai.

Encore faut-il que le code de l’application soit bien écrit et qu’il n’y ait pas de bogue dans le Framework. Si la première condition n’est pas toujours réalisées, même les meilleurs font des erreurs, il en va de même avec la seconde condition : le Framework lui-même malgré sa qualité n’est pas exempt de bogues.

Si vous balayez tout un répertoire de Jpeg de bonne taille (dans les 2 Mo) et que vous afficher les miniatures en utilisant le code plus haut, au bout d’une vingtaine d’images le code plantera en out of memory. Un examen de la mémoire consommée vous montrera peut-être une chiffre délirant dépassant 1 à 2 Go !

Quand on pense à une vingtaine de miniatures en 100x100 environ, une trentaine de Ko, on voit mal d’où peuvent provenir ces Go consommés !

Pourtant, en y réfléchissant on s’aperçoit que le composant Image créé par le code exemple référence le BitmapImage original. Ce dernier n’est pas collecté par le GC. Et quand on a compris qu’une image Jpeg de 2 Mo pesait en réalité 40/50 Mo en mémoire, on comprend vite comment au bout d’une vingtaine d’images on arrive et même dépasse le Go de RAM…

Une solution ?

Il existe un moyen d’éviter le problème. Il complique le code, par force :

private Image CreateThumbnailImage(Stream stream, int width)
{
    BitmapImage bi = new BitmapImage();
    bi.SetSource(stream);
 
    double cx = width;
    double cy = bi.PixelHeight * (cx / bi.PixelWidth);
 
    Image image = new Image();
    image.Source = bi;
 
    WriteableBitmap wb = new WriteableBitmap((int)cx, (int)cy);
    ScaleTransform transform = new ScaleTransform();
    transform.ScaleX = cx / bi.PixelWidth;
    transform.ScaleY = cy / bi.PixelHeight;
    wb.Render(image, transform);
    wb.Invalidate();
 
    Image thumbnail = new Image();
    thumbnail.Width = cx;
    thumbnail.Height = cy;
    thumbnail.Source = wb;
    return thumbnail;
} 

 

L’idée qui se cache derrière ces lignes est que plutôt d’assigner une grosse BitmapImage a une petite Image on passe par une WriteableBitmap avec une ScaleTransform pour créer la miniature. On assigne enfin cette miniature à l’image retournée. Dans cette version le BitmapImage n’est plus lié à l’objet Image retourné, et quand on sort du scope de la méthode le GC peut faire son boulot (au moment où il le décidera).

Ca plante toujours !

Hmm… Et oui.

Dans les deux conditions évoquées plus haut concernant la qualité du code, nous n’avons fait que résoudre la condition 1 : écrire un code plus intelligent et moins naïf.

Reste la condition 2 : un éventuel bogue du Framework lui-même.

Et hélas, cette seconde condition se réalise dans ce cas pourtant simple.

Le véritable problème est que WriteableBitmap, pour une raison qui échappe à la raison, conserve une référence sur l’objet XAML source ! Aucune méthode, aucune propriété de cette classe ne permettent de relâcher cette ressource.

L’objet WriteableBitmap conserve ainsi une référence sur l’objet Image qui lui-même référence la BitmapImage. Aucune de ces instances n’est donc supprimée de la mémoire par le GC.

Point final ?

Non, pas tout à fait.

Enfin la solution…

Avec de l’imagination, des tests, et une bonne connaissance du Framework certains ont découvert que la référence retenue par la WriteableImage vers l’objet XAML source peut être évitée dans le cas, subtile, où la méthode Render n’est pas utilisée. Mais cela est impossible, il faut bien faire un Render pour appliquer la transformation. La solution consiste alors a utiliser une seconde WriteableImage qui sera créée non plus avec Render mais par une vilaine boucle For qui copiera un à un les pixels de la première vers la seconde. Et c’est cette seconde WriteableImage qui sera utilisée comme source de l’objet Image retourné…

Dès lors tout le fatras intermédiaire (BitmapImage, WriteableImage avec référence sur la source, etc), tout cela ne sera plus référencé par personne et en sortant du scope de la méthode pourra être supprimé de la RAM !

Le code final devient alors :

private Image CreateThumbnailImage(Stream stream, int width)

{
    BitmapImage bi = new BitmapImage();
    bi.SetSource(stream);
 
    double cx = width;
    double cy = bi.PixelHeight * (cx / bi.PixelWidth);
 
    Image image = new Image();
    image.Source = bi;
 
    WriteableBitmap wb1 = new WriteableBitmap((int)cx, (int)cy);
    ScaleTransform transform = new ScaleTransform();
    transform.ScaleX = cx / bi.PixelWidth;
    transform.ScaleY = cy / bi.PixelHeight;
    wb1.Render(image, transform);
    wb1.Invalidate();
 
    WriteableBitmap wb2 = new WriteableBitmap((int)cx, (int)cy);
    for (int i = 0; i < wb2.Pixels.Length; i++)
        wb2.Pixels[i] = wb1.Pixels[i];
    wb2.Invalidate();
 
    Image thumbnail = new Image();
    thumbnail.Width = cx;
    thumbnail.Height = cy;
    thumbnail.Source = wb2;
    return thumbnail;
} 

 

Forcément cela devient beaucoup plus tordu et infiniment moins simple et moins naïf que le code exemple donné en début de billet. C’est le prix à payer pour réunir les deux conditions d’un code qui passe : écrire du bon code pas naïf, et contourner le bogue du Framework.

C’est lourd, bien plus lent que la première version, mais au lieu de planter dans les premières images, votre applications pourra créer des milliers de miniatures sans souci.

Conclusion

Ce bogue ne touche pas que les applications qui créent des miniatures, vous avez bien compris que cela n’est qu’un exemple. Toute application qui manipule des images peut rencontrer le problème d’une façon ou d’une autre. L’exemple des miniatures n’est que l’une de ces façons…

Alors si vous avez des applications Silverlight qui plantent en Out Of Memory sans raison alors qu’elles ne font rien de bien compliqué avec des images, vous avez peut-être ici la clé de vos problèmes, et un bout de code dont vous pouvez vous inspirer pour régler le problème !

Stay Tuned !

blog comments powered by Disqus