Comme d’habitude vous trouverez le code source liée à cet article ici : http://aka.ms/oitxqi
Je ne sais pas si vous avez pu le remarquer, mais la fluidité des écrans et de la navigation, n’est pas réellement au rendez-vous,et est d’autant plus exacerbé lorsqu’on affiche un nombre de carte importantedans la vue Expansions.
Il faut attendre que l’intégralité des cartes visibles à l’écran soient chargées par le GridView.En résumé plus il y a de cartes visibles (70 dans notre exemple plus haut), plus le temps de d’affichage sera long, car il faudra que ce même GridView fasse un traitement sur chaqueimage et ceci de manière synchrone.La conséquence intrinsèque de ce phénomène est que, je ne peux pas naviguer d’une vue à une autre tant que le GridView charge les images
Bien évidemment, avant de commencer à "essayer" d’optimiser le code, il faut d’abord vérifier que ce quiest fait ne satisfait pas à vos besoins et donc mesurer les performances globales de l’application.
Si vous n’affichez que 20 cartes à l’écran le résultat est sans doute satisfaisant, mais pour ma part, je vise non seulement lesperformances, mais également que l’interface soit plus réactive aux sollicitations de l’utilisateur, c’est-à-dire que je puisse naviguer dans lesdifférentes vues sans attendre que l’intégralité des images ou du contenu en général soit chargé.
Sur cette figure, non seulement le chargement des images est asynchrone (elles n’arrivent plus d’un seul bloc), mais tous les élémentsde la GridView sont déjà actif. De plus le scrolling vertical est plus fluide, comme vous pourrez le constater dans l’exemple fournit.
Alors comment rendre les écrans plus réactifs ?
Rappelez-vous, j’avais développé un contrôle Image personnalisé qui permettait je me site "Couplé avec la Virtualisation de la GridView, qui ne monte en mémoireque les contrôles visibles à l'écran, on gagne ainsi en performance et en fluidité, en évitant que toutes les images soient téléchargées d'unseul bloc lors de l'activation de la vue".
Ce qui était bon hier, ne l’est plus aujourd’hui. En effet comme je le disais si le nombre de carte visible est important, cettedernière stratégie n’est plus réellement efficace, et le scrolling reste saccadé. De plus même si le progressRingindique un téléchargement en cours et que cela me paraissait une bonne idée de départ,il est plus consommateur de ressources qu’autre chose, je l’ai donc remplacé par un seul progressRing au niveau de la vue.
L’idée de base pour améliorer l’application est très simple et j’aurai du y penser plutôt !!. Je vais déférer leBinding des images dans nos collections de données et m’appuyer sur INotifyPropertyChanged pour notifier legestionnaire de Binding que l’image est disponible. La GridView, affichera dans un premier temps un contrôle Image vide, puis sera notifiée que l’image estdisponible, d’une simplicité !!
Remarque : Je prends comme exemple, la vue Expansions,mais cela s’applique bien évidement aux autres vues également.
1) La vue Expansions, est liée à une collection de la classe URZACard(ObservableCollection<URZACard>), j’ai donc rajouté dans la classe URZACard, la propriété Picture de type BitmapImage quime permettra de faire du Binding avec un contrôle Image.
public class URZACard : UrzaGatherer.Common.BindableBase, IURZACommon
{
public URZACard()
}
private BitmapImage _pictureCard;
public BitmapImage Picture
get { return _pictureCard; }
set
this.SetProperty(ref this._pictureCard, value);
Remarque : Cette classe dérive de BindableBase qui implémente INotifyPropertyChanged.
2) Nous allons "Binder" dans le XAML, cette nouvelle propriété avec un contrôle Image, ou directement avec le contrôle URZAImage auquel j’ai ajouté une propriété BitmapSource.<UrzaControl:UrzaImage BitmapSource="{Binding Picture}" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
</UrzaControl:UrzaImage>
Remarque : Dans cet exemple nous ne déferons que les images, mais rien n’empêche de fairede même pour d’autres champs.
3) Passons au code !!, l’idée est donc d’itérer sur la collection ObservableCollection<URZACard>,afin d’alimenter, la propriété Picture de chaque instance de la classe URZACard.Un code classique serait donc le suivant :
On démarre un nouveau thread, et on itère sur la collection
Task.Run(() =>
var lcards = this.CurrentExpansion.cards;
foreach (var card in lcards)
this.Dispatcher.RunAsync(CoreDispatcherPriority.Low, new DispatchedHandler(() =>
card.Picture = new BitmapImage(new Uri(card.ImageInfo.RemotePath));
})); }
});
A noter que comme nous sommes dans un thread , j’utilise le Dispatcher, pour se synchroniser avec le thread qui a créé l’objet Picture. Sinon => WRONG_THREAD
Notez que la priorité du Dispatcher est à Low, permettant ainsi une meilleure réactivité de l’interface, si l’utilisateur,veut par exemple fermer l’application, ou la mettre en mode snap. Si vous mettez à High, les messages souris ou touch ne sont pas traités en priorités
une alternative serait d’utiliser le pendant parallèle du foreach, la méthode Parallel.ForEach
Parallel.ForEach(blocks, async (block, loopState) =>
}));
Néanmoins, utiliser la version parallèle, peut sembler moins rapide pour l’utilisateur, car elle partitionne les données en npartitions (n correspondant au nombre de processeurs virtuelles), et n’affiche pas forcement les cartes dans un ordre bien défini alors que la versionséquentielle les affiches les unes après les autres ce qui peut engendrer une sensation que le code séquentiel est plus rapide. Ce qui d’ailleurs peut êtrele cas, en fonction du nombre de cartes à télécharger. En effet il peut dans certain cas, être plus judicieux de rester en séquentiel si le nombre de cartesest insuffisante. La version parallèle pouvant engendré des surcharges du à son fonctionnement interne. Il est donc important de mesurer et de choisir unscénario en fonction d’un volume de données, ou de s’adapter dynamiquement en fonction de ce même volume.
Mais le point ou la version parallèle est plus rapide, c’est quand dans URZAGatherer on décide de sauvegarder les images en locale.
En effet, sur un volume de 350 cartes, la version séquentielle met environ 50 secondes pour les télécharger et les sauvegarder,alors que la version parallèle met 15 secondes, soit environ une accélération de 3.3. Ce qui est en phase avec le nombre de processeurs de ma configuration quiest de 4.
Remarque : On pourrait s’attendre à une accélération de 4, mais c’est sans compter lebruit et les frictions propres au système d’exploitation et aux ressources utilisées à un instant T.
Dans le code que vous retrouverez dans la solution, vous y trouverez entre autre dans la classe VueData, la méthode MapPictureCardsAsync()
public Task MapPictureCardsAsync()
InitParallelOptions(System.Environment.ProcessorCount);
return Task.Run( async () =>
try
var cards = this.CurrentExpansion.cards;
#if FOREACH
foreach (var card in cards)
if (card.Picture == null) //No need to rebind
await BindPictureAsync(card, card.ImageInfo, TokenSource.Token);
#else
Parallel.ForEach(cards, _parallelOptions, async (card, loopState) =>
if (_parallelOptions.CancellationToken.IsCancellationRequested)
{ loopState.Stop();
if (!loopState.ShouldExitCurrentIteration)
#endif
catch (OperationCanceledException)
RaisePicturesLoadedAsync();
if (!TokenSource.IsCancellationRequested)
},TokenSource.Token);
Cette méthode démarre une nouvelle tâche (Task.Run()), en lui passant commeparamètre CancellationToken, qui nous sert à arrêter la tâche et la boucle parallèle si on revient à la vue Home. En effet, pas la peine decontinuer à télécharger les cartes, si l’utilisateur, veut visualiser d’autres cartes. Par contre cette tâche n’est pas arrêtée si l’utilisateur veut voir ledétail d’une carte.
L’arrêt de la tâche ce fait dans la méthode CancelAsync() de la classe VueData.
La boucle parallèle se fait à l’aide de la méthode statique Parallel.ForEach(),ou je lui passe comme paramètre des options qui sont initialisées dans la méthode VueData.InitParallelOption().Dans cette méthode, j’instancie le CancellationToken, ainsi que le degré de parallélisme que je souhaite, c’est-à-dire le nombre deprocesseurs virtuels à utiliser. Parfois, il est plus judicieux de ne pas utiliser tous les processeurs disponibles, et d'en laisser pour d'autres tâches.
void InitParallelOptions(int maxDegreeOfParallelism)
{ TokenSource = new CancellationTokenSource();
_parallelOptions = new ParallelOptions();
_parallelOptions.CancellationToken = TokenSource.Token;
_parallelOptions.MaxDegreeOfParallelism = maxDegreeOfParallelism;
Je passe également le paramètre loopState, qui nous permet de déterminer l’état d’une itération à un instant T (par exemple si je dois sortirde la boucle loopState.ShouldExitCurrentIteration) ou arrêter la boucle loopstate.Stop() si une demande d’arrêt est en cours.Ensuite Si le Binding a déjà été effectué, pas la peine de binder une nouvelle fois. Dans le cas contraire, j’appelle la méthode BindPictureAsync().
private async Task BindPictureAsync(IURZACommon item, URZAImageInfo imageInfo,CancellationToken token)
//Get the remote picture
if (!URZASettings.IsOffLineModeOn() && NetworkInterface.GetIsNetworkAvailable())
BindToPicture(item,imageInfo.RemotePath);
return ;
if (NetworkInterface.GetIsNetworkAvailable())
if (!await Helper.IsFileExistAsync(imageInfo.LocalFolder,imageInfo.FileName))
//I don't want to wait until the picture was downloaded and saved to disk;
BindToPicture(item, imageInfo.RemotePath);
Helper.DownloadPicture2Async(token, imageInfo);
return;
else
BindToPicture(item, imageInfo.LocalPath);
if (await Helper.IsFileExistAsync(imageInfo.LocalFolder, imageInfo.FileName))
BindToPicture(item, @"ms-appx:/Assets/widelogo.png");
Pour télécharger les images, je m’appuie désormais sur le BackGroundDownloaderet non plus sur HTTPClient.
static BackgroundDownloader _downloader = new BackgroundDownloader();
public async static void DownloadPicture2Async(CancellationToken token, URZAImageInfo imageinfo)
IStorageFile file = null;
file= await imageinfo.LocalFolder.CreateFileAsync(imageinfo.FileName, CreationCollisionOption.ReplaceExisting);
DownloadOperation downloadOperation = _downloader.CreateDownload(new Uri(imageinfo.RemotePath),file);
await downloadOperation.StartAsync().AsTask(token, null);
catch (Exception ex)
if (file !=null)
file.DeleteAsync(StorageDeleteOption.PermanentDelete);
Remarque : la constante de compilation FOREACH, permet de compiler la version séquentielle ou parallèle du code, ceci vouspermettra de vous faire une idée et choisir entre l’une ou l’autre des stratégies, voir d'utiliser les deux !!
Pour vérifier que le code parallèle est quand même intrinsèquement plus performant, j’ai ajouté une méthode Helper.Run() qui permet de mesurer letemps d’exécution d’une méthode par rapport à une autre. public async static void Run(Func<Task> func,String message)
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
await func();
StorageFile storageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("log.txt", CreationCollisionOption.OpenIfExists);
String Message = String.Format("\n\r {0} : ElapsedMilliseconds {1} \n\r", message, watch.ElapsedMilliseconds);
await Windows.Storage.FileIO.AppendTextAsync(storageFile, Message);
watch.Stop();
Et pour appeler la méthode MapPictureCardsasync() c’est simple.
String Message = "REMOTE + SAV : PARALLEL.FOREACH(8) : Nombre d'elements : " + _vueData.CurrentExpansion.cards.Count.ToString();
Helper.Run(() =>
return _vueData.MapPictureCardsAsync(); }, Message);
Cette méthode Helper.Run() sauvegarde un fichier log.txt dans le répertoire courant de l’application dont voici quelques extraits.
Image En Remote : FOREACH : Nombre d'éléments : 350 : ElapsedMilliseconds 10703
Image En Remote : PARALLEL.FOREACH(8) : Nombre d'éléments : 350 : ElapsedMilliseconds 9646
Sauvegarde des images : FOREACH : Nombre d'éléments : 350 : ElapsedMilliseconds 48687
Sauvegarde des images: PARALLEL.FOREACH(8) : Nombre d'éléments : 350 : ElapsedMilliseconds 14693
Ouverture image local : FOREACH : Nombre d'éléments : 350 : ElapsedMilliseconds 11014
Ouverture image locale : PARALLEL.FOREACH(8) : Nombre d'éléments : 350 : ElapsedMilliseconds 10767
Ont peut constater que la différence notable se fait essentiellement lorsqu'il faut télécharger et sauvegarder les images en locales, que pour le reste
la différence est minime mais présente. Encore une fois c'est à vous de vous faire une idée selon vos attentes.
Eric Vernié