Fuites de mémoire avec WPF 4.0 et de grosses listes d’images

15. mars 2011

Dans le cadre d’UrzaGatherer (http://urzagatherer.codeplex.com), j’ai découvert un problème bien embêtant avec une liste d’images en haute définition.

En effet j’ai une ListBox qui me présente les cartes d’une collection dans un WrapPanel (qui porte une jolie animation):

image

Or, l’utilisateur en se promenant de collections en collections va faire changer de nombreuses fois la source de la liste et va donc déclencher de nombreuses fois le chargement de ces grosses images.

Et j’ai constaté que lorsque je réaffecte le DataContext de ma liste à une nouvelle source, les anciennes données semblent ne pas être nettoyées de la mémoire.

Ainsi, UrzaGatherer peut allègrement occuper 2Go de RAM après avoir visité une dizaine de collections (chaque collection peut peser jusqu’à 300 Mo).

Je me suis donc mis en quête de la fuite.

Mais avant d’en venir à cette dernière regardons déjà comment le binding est fait et comment la liste est construite:

 

1 - Notre ListBox


La liste est donc une ListBox avec comme ItemPanel un WrapPanel dans lequel j’ai glissé un behavior en provenance de Blend pour faire un effet sympa d’animations:

            <ListBox ItemsSource="{Binding}" x:Name="imagesList">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Image Source="{Binding Bitmap, Mode=OneWay}"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal" ItemHeight="300">
                            <Interactivity:Interaction.Behaviors>
                                <Layout:FluidMoveBehavior Duration="00:00:00.5">
                                </Layout:FluidMoveBehavior>
                            </Interactivity:Interaction.Behaviors>
                        </WrapPanel>
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
            </ListBox>

 

On peut voir que l’ItemTemplate se branche sur la propriété Bitmap de notre source.

2 - La source de données


Cette dernière est effectivement composée d’une liste de cartes qui portent une propriété Bitmap que je construis ainsi:

public BitmapImage Bitmap
{
    get
    {
        if (CompletePath == null)
            return null;

        BitmapImage bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.CreateOptions = BitmapCreateOptions.None;
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.UriSource = new Uri(CompletePath);
        bitmapImage.EndInit();
        bitmapImage.Freeze();

        return bitmapImage;
    }
}

A partir du chemin CompletePath, je produis une BitmapImage. L’objectif étant de contrôler le cache et de pouvoir créer les images dans un thread séparé pour les affecter par la suite. Toutefois, dans notre exemple, je simplifie cette étape en branchant directement le contrôle Image sur mon Bitmap sans passer par la création asynchrone.

 

3 – La fuite (les fuites?)


A partir de ce code relativement simple, il me suffit de brancher 3 ou 4 collections de cartes pour voir que la mémoire n’est jamais libérée.

Mes investigations m’ont permis de voir deux soucis:

  • Le behavior de Blend garde des références sur les images et de ce fait bloque le Garbage Collector et donc laisse les images en mémoire
  • L’utilisation d’une BitmapImage semble également laisser des traces en mémoire et malgré mes efforts je n’ai pas pu libérer ces ressources. La seule solution : faire un binding vers le chemin (CompletePath) plutôt que vers ma Bitmap.

 

Je me suis donc résigner à ne plus utiliser ni le behavior ni le chargement asynchrone. Toutefois pour ce dernier point, j’ai pu faire un contournement ainsi:

  • Créer une variable BindPath sur mes cartes
  • L’initialiser à null et mettre le binding du contrôle Image dessus
  • Binder la liste (donc pour le moment aucune image ne s’affiche puisque BindPath = null)
  • Faire une boucle dans une Task qui toutes les 20 millisecondes prend une carte et met sa propriété BindPath à CompletePath. De ce fait via le fait que mes cartes sont INotityPropertyChanged, la ListBox se met à jour progressivement et donne un effet progressif agréable (plutot qu’un bon vieux figeage de l’interface à l’ancienne)

 

Si vous aussi vous trouvez des fuites dans l’utilisation de WPF 4.0, n’hésitez pas à me le signaler Sourire

WPF, .Net, UrzaGatherer

De l’utilisation intelligente d’Entity Framework 4.0

31. mai 2010

Un framework a beau être super puissant, il ne peut pas empêcher l'a mauvaise utilisation. Il peut tenter par de nombreux moyens de la réduire mais il ne peut l’empêcher.

Du moins pas sans se brider par la suite.

J’en veux pour preuve l’utilisation d’Entity Framework 4.0. Ce framework permet de manière extrêmement élégante de requêter une source de données. Et, bien utilisé, il permet aussi d’être très performant.

Voici un exemple issu d’UrzaGatherer: Mon modèle possède une collection de Card qui, entre autres, contient une propriété Check qui permet de définir si l’utilisateur détient complètement la carte en question. Dans le cadre de la treeview qui affiche les collections de cartes, je voulais modifier le texte de chaque collection en y ajoutant le nombre de cartes manquantes le cas échéant.

Pour se faire et comme tout est issu du binding dans UrzaGatherer, j’ai développé un ValueConverter utilisé ainsi:

<TextBlock Text="{Binding Converter={StaticResource CountConverter}, Mode=OneWay
, IsAsync=True}"
/>

Le code du converter était le suivant:

    public class CountConverter : IValueConverter
    {
    public object Convert(object value, Type targetType, object parameter, 
CultureInfo culture) { Expansion expansion = (Expansion) value; var query = from card in MainWindow.Entities.Cards where card.ExpansionID == expansion.ID select card; int check = 0; List<Card> cards = query.ToList(); int count = cards.Count; foreach (Card card in cards) { if (card.Check) check++; } int missing = count - check; if (missing == 0) return string.Format("- {0} cards", count); return string.Format("({0}/{1} - Missing : {2})", check, count, missing); } public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture) { return value; } }

Dans ce converter, on voit donc une requête LINQ sur les cartes en question, puis une transformation en liste pour faire notre comptage.

Et c’est là qu’apparait la mauvaise utilisation. En effet, la conversion en liste va effectivement faire la requête sur la base de données et créer les entités en mémoire. Ces entités qui peuvent être lourdes (c’est le cas ici ou chaque carte porte plusieurs images).

Tout ça pour juste obtenir un comptage.

Il faut garder à l’esprit que EF4 est efficace tant qu’on reste dans le monde des expressions LINQ, c’est à dire tant qu’on reste au final dans le monde SQL.

Ici par exemple, la solution pour être bien plus efficace est simple : Il ne faut pas utiliser de listes locales mais tout traiter en LINQ:

public object Convert(object value, Type targetType, object parameter, 
CultureInfo culture) { Expansion expansion = (Expansion) value; var query = from card in MainWindow.Entities.Cards where card.ExpansionID == expansion.ID select card; int check = query.Count(c => c.Check); int count = query.Count(); int missing = count - check; if (missing == 0) return string.Format("- {0} cards", count); return string.Format("({0}/{1} - Missing : {2})", check, count, missing); }

La différence est subtile mais ici tout se passera sur SQL Server. Aucune entité ne sera créée ni ramenée côté client. Les méthodes Count() issues de LINQ vont générer des requêtes efficaces côté serveur contrairement à la méthode ToList() qui va faire un bon gros SELECT des familles pour tout reconstruire en mémoire.

Donc en conclusion et en ce qui concerne Entity Framework 4.0 : Retardez toujours au plus tard la récupération des entités!!

.Net, WPF ,