TL;DR: Lorsque je travaillais à corriger le problème de "L'écran noir" au démarrage de l'application Flickr 1.3 pour Windows Phone 7, j'ai constaté que HttpWebRequest fait à l'interne à l'interne un appel synchrone à la thread UI ce qui peut avoir un impact particulierement négatif sur l'expérience utilisateur. La totalité de la construction d'une requête asynchrone est faite sur la thread UI, et ce n'est pas possible de le changer.
Lorsque l'on programme pour Windows Phone 7, on entend souvent que pour améliorer la performance perçue, il faut sortir de la Thread UI (aussi nommé le dispatcher) pour effectuer des opérations qui ne sont pas liées à l'interface utilsateur. Par bonne perfomance perçue, j'entend par la que l'interface répond immédiatement, et ne s'arrête pas lorsque des opérations d'arrière plan sont en cours d'exécution.
Pour faire cela, il faut utiliser des techniques connues comme l'ajout dans le ThreadPool, créer une nouvelle Thread, ou bien utiliser le pattern Begin/End.
Tout ceci est tout à fait vrai, et un très bon exemple d'une mauvaise utilisation de la Thread UI est le traitement de la réponse d'une requête web, particulièrement en utilisant WebClient où les évènements sont exécutés dans le contexte du dispatcher. Du point de vue d'un novice, ne pas avoir à s'occuper des changements de contextes pour le développement d'une application simple qui met à jour l'écran, cela donne une très bonne expérience de développement.
Mais cela a le désagréable effet de dégrader la performance percue de l'application, puisque beaucoup de code à tendance à être executé sur la Thread UI.
HttpWebRequest à la rescousse ?
Vous trouverez que HttpWebRequest est un meilleur choix pour cela. Cette classe utilise le pattern Begin/End et l'exécution d'un AsyncCallback est effectuée dans le contexte du Thread Pool. Cela fait en sorte que le code executé dans ce callback ne va naturellement pas impacter la performance percue de l'application.
En utilisant les Reactive Extensions, cela peut être écrit come ceci :
1 2 3 4 5 6 7 8 9 10 11 |
var request = WebRequest.Create("http://www.google.com");
var queryBuilder = Observable.FromAsyncPattern( (h, o) => request.BeginGetResponse(h, o), ar => request.EndGetResponse(ar));
queryBuilder() /* Perform the expensive work in the context of the AsyncCall back */ /* from the WebRequest. This will be the ThreadPool. */ .Select(response => DoSomeExpensiveWork(response)) /* Go back to the UI Thread to execute the OnNext method on the subscriber */ .ObserveOnDispatcher() .Subscribe(result => DisplayResult(result)); |
De cette manière, votre code s'exécutera pour la plupart hors de la thread UI, la où cela n'impacte pas l'expérience utilisateur.
Pourquoi ce ne serait pas à la rescousse alors ?
En fait, ca le sera toujours (jusqu'a NoDo), mais il y a un détail à noter. Et c'est un détail important, du point de vue performance.
Si l'on considère ce code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public App() { /* some application initialization code */ ManualResetEvent ev = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem( d => { var r = WebRequest.Create(http://www.google.com); r.BeginGetResponse((r2) => { }, null); ev.Set(); } ); ev.WaitOne(); } |
Ce code effectue la création d'une requête sur le ThreadPool, tout en bloquant la Thread UI dans le fichier App.xaml.cs. Cela fait en sorte que la construction (mais pas l'appel sur le réseau) de la WebRequest est synchrone, et donc que l'application attent que la requête commence avant d'afficher une page à l'utilisateur.
Bien que ce code ne soit pas dans les meilleures pratiques, il y avait un chemin de code dans l'application Flickr 1.3 qui faisait quelque chose de très similaire, mais d'une manière plus complexe. Si vous l'essayez par vous même, vous verrez que l'application s'arrête au démarrage, ce qui signifique que l'évènement n'est jamais levé.
Qu'est-ce qui se passe ?
En creusant un peu, on peut voir qu'une thread du ThreadPool est arrêtée avec la Stack Trace suivante :
mscorlib.dll!System.PInvoke.PAL.Threading_Event_Wait()
mscorlib.dll!System.Threading.EventWaitHandle.WaitOne()
System.Windows.dll!System.Windows.Threading.Dispatcher.FastInvoke(...)
System.Windows.dll!System.Net.Browser.AsyncHelper.BeginOnUI(...)
System.Windows.dll!System.Net.Browser.ClientHttpWebRequest.BeginGetResponse(...)
WindowsPhoneApplication2.dll!WindowsPhoneApplication2.App..ctor.AnonymousMethod__0(...)
La méthode BeginGetResponse est en train d'essayer d'exécuter quelque chose sur la Thread UI. Et dans notre exemple, puisque la Thread UI est bloquée par le ManualResetEvent, l'application est bloquée dans un deadlock entre un lock du dispatcher et notre évènement.
C'est aussi vrai pour la méthode EndGetResponse.
Mais si l'on creuse encore plus loin, on peut voir dans la version de System.Windows.dll dans l'émulateur WP7 (celle du SDK ne contient que des stubs des types publics), que la méthode BeginGetResponse force en fait la totalité de son travail de création de la requête web sur la Thread UI !
C'est particulièrement perturbant. Je me demande encore pourquoi du code qui n'a attrait qu'au réseau a besoin de s'exécuter sur la thread UI.
Quel est l'impact alors ?
L'impact est assez simple : Plus il y a de requête d'initiées, moins l'interface utilisateur sera réactive, à la fois pour le traitement du lancement et de la fin d'une requête web. Chaque appel aux méthodes BeginGetResponse et EndGetResponse s'executent implicitement sur la thread UI.
Dans le cas de l'applications de controle à distance comme la mienne, qui essayent d'avoir le controle à distance de la souris, ces applications sont toutes affectées par la même piètre qualité de réponse de la souris. C'est en partie à cause de la thread UI qui est occupée à traiter les évènements de manipulation, ce qui explique beaucoup les problèmes de performance des requêtes web effecutées en même temps, même avec l'utilisation de HttpWebRequest au lieu de WebClient. Cela explique aussi que tant que l'utilisateur touche l'écran, les requêtes web seront fortement ralenties.
Le problème d'écran noir de l'application Flickr 1.3
Dans l'application Flickr sur laquelle j'ai pu travailler, beaucoup d'utilisateurs rapportaient un problème d'écran noir au démarrage, après que l'application ai bien fonctionné plusieurs jours.
L'application était en fait en train d'essayer de mettre à jour une ressource de manière asynchrone en utilisant HttpWebRequest. A cause d'une race condition entre une ressource de l'application et la Thread UI qui attendait que l'application démarre, le résultat a été un "écran noir" infini qui ne pouvait être résolu qu'en réinstallant l'application.
En passant, à ce point dans l'initialisation de l'application, dans le constructeur de la classe App, l'application n'est pas tuée après 10 secondes si elle n'affiche rien à l'utilsateur. Par contre, si l'application bloque dans le constructeur de la premiere page, l'application est terminée automatiquement par l'OS après environ 10 secondes.
Enfin, à propos de l'utilisation de la Thread UI à l'intérieur de HttpWebRequest, les applications qui utilisent beaucoup le réseau pour récupérer de petites ressources comme des images, cela a un impact négatif sur la performance.
Est-ce que je peut faire quelque chose ?
Lors de l'analyse du code de System.Windows.dll, j'ai pu remarquer que BeginGetResponse vérifie que la thread courante est le dispatcher, et si c'est le cas, l'execution n'est pas poussée sur le dispatcher.
Cela veut dire qu'il est possible de grouper les appels à BeginGetResponse sur la Thread UI, ce qui évitera de changer trop souvent de contexte. Ce n'est pas la panacée, mais c'est le moins que l'on puisse gagner de ce coté.
Et dans les futures version de Windows Phone ?
Du coté des bonnes nouvelles, Scott Gu a annoncé durant le Mix 11 que les évènements de manipulation seront déplacés hors de la Thread UI, faisant en sorte que l'interface utilisateur sera "glissante comme du beurre". Beaucoup d'applications vont bénéficier de ce changement. Du coté du réseau, mis à part l'introduction des sockets, rien n'indique que les webrequests seront sorties du UI thread.
Mais attendons Mango, je suppose que tout ceci va changer pour le meilleur, et nous permettra de développer des applications performantes pour Windows Phone.