Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Kévin Gosse

Clair, .NET, et précis

Faire de l'AJAX sans restrictions de domaine

Les développeurs de la librairie Javascript Dojo ont mit au point un moyen sécurisé d'envoyer des requêtes AJAX (via une iframe) à l'aide de la propriété window.name, ce qui a pour principal intérêt de contourner les restrictions de domaine imposées par le navigateur.

Ça se passe ici, et c'est vraiment épatant.

Présentation sur Velocity

Une petite présentation PowerPoint sympathique sur Velocity a été mise sur le blog du projet. Elle a le mérite de recentrer les choses et d'indiquer quelles sont les ambitions de ce projet. Je vous conseille donc vivement de la lire si vous êtes intéressé par les problématiques de cache au sein d'un cluster.

A noter qu'il y a également un podcast de la présentation pour ceux qui aiment ce genre de média ;)

Google AJAX Libraries API

Prototype, script.aculo.us, jQuery… Autant de librairies Javascript dont le nom doit être familier aux oreilles de ceux qui s’intéressent au développement Web 2.0, de part l’éventail de fonctionnalité qu’elles offrent et leur popularité croissante. Cependant, leur puissance à un cout : leur poids, de plusieurs dizaines de kilo-octets, a un impact significatif sur le temps de chargement des pages, le rendu de celles-ci étant en général dépendant de l’exécution des scripts.

Les librairies se retrouvent en général dans le cache du navigateur après le premier chargement, mais ce cache n’étant pas partagé entre les différents sites, l’utilisateur se retrouve à télécharger x copies identiques de la même librairie, là où une aurait suffit. Alors pourquoi ne pas tenter de factoriser ce cout ?

Et c’est là qu’intervient « AJAX Libraries API » de Google. Le principe est simple : proposer des liens vers les principales librairies Javascript utilisées, hébergées sur les serveurs de Google pour qu’elles soient toujours chargées depuis la même adresse, et donc au final utiliser au mieux le cache des navigateurs quand l’utilisateur surfe d’un site à l’autre.

Loin de s’arrêter là, Google nous propose également un système simple mais bien pensé de versionning. En effet, l’adresse vers les librairies est de la forme http://ajax.googleapis.com/ajax/libs/[Nom de la librairie]/[Version]/[Fichier javascript].js
Ainsi, pour jQuery en version 1.2.6, on aura : http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.js

L’astuce est qu’il est possible de ne spécifier qu’une partie de la version. Si par exemple vous souhaitez la dernière version dans la branche 1.2 de jQuery, il suffit d’aller chercher à l’adresse : http://ajax.googleapis.com/ajax/libs/jquery/1.2/jquery.js

Ou encore http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js si vous voulez la dernière version en date.

« AJAX Libraries API » est donc un moyen simple d’améliorer le confort de l’utilisateur en améliorant l’efficacité du cache de son navigateur, tout en permettant une mise à jour automatique des librairies utilisées. Bref, un outil à connaître et à garder sous le coude.

Vous pourrez trouver plus d’informations et la liste des librairies supportées à l’adresse suivante : http://code.google.com/apis/ajaxlibs/documentation/index.html

Récupération de la version des Assemblies référencées par une application

Après avoir eu plusieurs fois des erreurs dues au déploiement de mauvaises versions des fichiers, j’ai voulu ajouter une page de test vérifiant la version des DLL utilisées par l’application. J’ai donc écris le code suivant :

string version = "Version des librairies : <br/><ul>";

Assembly currentAssembly = Assembly.GetExecutingAssembly();

version += "<li>" + currentAssembly.FullName.ToString() + "</li>";

AssemblyName[] assemblyNames = currentAssembly.GetReferencedAssemblies();

foreach (AssemblyName assemblyName in assemblyNames)

version += "<li>" + assemblyName.FullName + "</li>";

Après exécution du code, la version de toutes les DLL référencées s’affiche, mission accomplie.

… C’est du moins ce que je pensais avant de remplacer une des DLL par une version plus ancienne. Et là, surprise : le script continue de me renvoyer le même numéro de version.

Après investigation, il se trouve que la fonction Assembly.GetReferencedAssemblies ne va pas vérifier la version des Assemblies référencées, mais se contente de renvoyer les informations qu’elle trouve dans les métadonnées de l’Assembly sur laquelle on exécute la fonction. Elle nous renvoie donc la version des DLL qu’elle s’attend à trouver, et non la version qu’elle trouve effectivement.

Pour pallier au problème sans pour autant avoir à parcourir les fichiers sur le disque ou recharger les assemblies, il est possible d’utiliser la fonction Assembly.GetAssembly, qui prend en paramètre un type et renvoie l’assembly dans laquelle le type est défini.

Le code devient donc quelque chose comme :
 

string version = "Version des librairies : <br/><ul>";

Assembly currentAssembly = Assembly.GetExecutingAssembly();

version += "<li>" + currentAssembly.FullName.ToString() + "</li>";

version += "<li>" + Assembly.GetAssembly(typeof(Type1)).FullName + "</li>";

version += "<li>" + Assembly.GetAssembly(typeof(Type2)).FullName + "</li>";

version += "<li>" + Assembly.GetAssembly(typeof(Type3)).FullName + "</li>";

Où Type1, Type2, et Type3, sont des types définis chacun dans une librairie à tester.

C’est clairement moins élégant, on perd le coté dynamique, et cela créé des dépendances supplémentaires entre les librairies, mais ça marche. Il existe d’autres méthodes mais il faut à chaque fois s’assurer que l’on ne va pas charger inutilement une copie de l’assembly en mémoire, ce qui pourrait vite devenir gênant pour l’application.

MAJ : Comme le fait remarquer Cyril, il est aussi possible de faire la même chose de manière générique en passant pas l'AppDomain :

foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())

{

Console.WriteLine(assembly.FullName);

}

Posted: mercredi 28 mai 2008 20:03 par KooKiz | 6 commentaire(s)
Classé sous : ,
Coupler Basic Auth et formulaire au sein d'une application ASP.NET

J'ai pu être confronté dans le cadre de mon travail à une application devant présenter deux modes d'authentification : une première, classique, par formulaire, et une seconde, pour les utilisateurs accédant à l'application depuis l'intranet, par fenêtre Basic Auth, remplie automatiquement à l'aide d'un système de SSO (Single Sign On).

Prit séparément, ces deux systèmes ne présentent aucune difficulté particulière pour leur implémentation, mais les cumuler présente un certain nombre de difficultés, comme par exemple la configuration du serveur : si on active l'accès anonyme, la boite d'authentification Basic Auth n'apparait pas. En revanche, si on active l'authentification Basic Auth, les utilisateurs ne peuvent atteindre la page de saisie du login et mot de passe (pour l'authentification par formulaire) sans voir la popup d'authentification.

Après quelques recherches, notamment sur des blogs de développeurs ayant rencontré le même problème, j'ai pu trouver trois solutions.

Solution numéro 1 : Simuler un Basic Auth.

La première solution consiste à tout faire soi-même. Le principe est donc de désactiver l'authentification dans le web.config, activer l'accès anonyme dans IIS, et déterminer à l'aide de l'URL d'où vient l'utilisateur. S'il vient d'internet, on vérifie qu'il est authentifié, et si ce n'est pas le cas on le redirige vers la page de login. A partir de là, on retrouve un fonctionnement par formulaire classique, à ceci près qu'il faut gérer certains mécanismes soit même vu que l'authentification est désactivée dans le web.config. Si l'utilisateur vient de l'intranet, on renvoie un code d'erreur 401 pour déclencher l'affichage de la popup d'authentification sur le navigateur. Une fois que l'utilisateur entre ses informations, il est possible de récupérer le login et le mot de passe dans l'en-tête de la requête http (respectivement Request.ServerVariables["LOGON_USER"] et Request.ServerVariables["AUTH_PASSWORD"]). Il ne reste plus qu'à authentifier l'utilisateur.
Bien que pouvant être très couteuse en termes de temps de développement et de test (simuler l'authentification Basic Auth est plus difficile qu'il n'y parait), cette solution est de mon point de vue la plus souple. Il faut aussi garder à l'esprit que tout gérer soi-même expose considérablement l'application, le moindre bug pouvant se transformer en faille critique de sécurité.

Solution numéro 2 : Séparer le module d'authentification du reste de l'application

La seconde solution consiste à isoler le code d'authentification de l'application dans une nouvelle application (ou plus exactement : deux nouvelles applications). Une gérant le Basic Auth, et l'autre l'accès par formulaire. En fonction de l'URL, l'utilisateur est dirigé vers l'une ou l'autre. A partir de là, il faut effectuer une authentification classique, à ceci près qu'il faut ensuite rediriger l'utilisateur vers l'application principale. Cela implique donc de désactiver l'authentification sur cette dernière, et mettre en place un système couplant cookies et informations de sessions stockées dans la base de données pour transmettre les informations d'authentification d'une application à l'autre. L'avantage de cette solution est d'avoir une configuration cohérente entre le serveur et les applications, pas de difficulté particulière pour le développement (si ce n'est la transmission des informations de l'utilisateur), et un système robuste en termes de sécurité (utilisation des modes d'authentification de la manière prévue par .NET). Le principal problème est le surcout engendré par le déploiement et la maintenance de deux applications, même limitées au processus d'authentification.

Solution numéro 3 : Utiliser deux répertoires virtuels

La troisième et dernière solution consiste à créer dans IIS deux répertoires virtuels pointant vers la même application. L'un sera configuré en accès anonyme, et l'autre en accès Basic Auth. L'application est quant à elle configurée en formulaire dans le web.config. Il faut ensuite distinguer d'où vient l'utilisateur, soit à l'aide de l'URL, ou de manière plus sûre à l'aide du nom du répertoire virtuel. Si l'utilisateur vient d'internet, on se retrouve dans une authentification par formulaire classique. Si l'utilisateur vient de l'intranet, IIS se chargera de lui afficher la popup d'authentification Basic Auth pour qu'il saisisse ses identifiants, et vérifiera leur validité auprès de l'Active Directory. Il suffit ensuite de récupérer le login dans l'application. Attention toutefois, l'application étant configurée en mode formulaire dans le web.config, les informations de l'utilisateur ne sont pas renseignées dans la propriété CurrentPrincipal comme dans une authentification Windows classique. Par chance, après l'authentification de l'utilisateur, IIS transmet à l'application la requête HTTP telle qu'elle a été reçue. Il est donc possible de récupérer le login dans les en-têtes HTTP, comme dans le cas de la première solution. Je conseille d'ailleurs de récupérer dans la foulée le mot de passe pour effectuer une double validation (des fois qu'un utilisateur malveillant parvienne à faire croire à l'application qu'il vient de l'intranet alors qu'il est passé par le répertoire virtuel en accès anonyme).
Cette solution a l'avantage d'être la plus simple des trois. Elle a par contre un inconvénient majeur : si deux répertoires virtuels pointent vers les mêmes fichiers, cela implique que la même application soit chargée deux fois en mémoire, entrainant une forte surconsommation et une augmentation de la fragmentation. De plus, il faut faire attention à ce que les deux applications n'utilisent pas par exemple les mêmes fichiers de log, sous peine de voir l'une d'elles bloquée par le verrouillage en écriture de l'autre.

En bref

Chacune des trois solutions a ses avantages et ses inconvénients, aussi chaque situation est à étudier pour déterminer celle qui convient le mieux. Cependant, dans la plupart des cas, je recommanderai la seconde, car c'est celle qui respecte le mieux le cadre prévu des fonctions d'authentification de .NET. Cette liste de solution n'étant pas forcément exhaustive, je vous invite à réagir si vous envisageriez d'autres solutions pour faire cohabiter une authentification formulaire et Basic Auth au sein de la même application.

ASP.NET : Memory Pressure et fragmentation

Je me propose ici de décrire le mécanisme de Memory Pressure et de commencer à introduire un problème qui peut survenir sur la plupart des applications ASP.NET un tant soit peu gourmandes en mémoire : la fragmentation.

Malgré tous les mécanismes de récupération de la mémoire s'exécutant en arrière plan, il arrive que les besoins en mémoire d'une application se mettent à dépasser les capacités de la machine l'exécutant. Pour pallier à ce genre de situations et tenter de préserver la stabilité de l'application, ASP.NET utilise quelques procédés, à savoir :

- Appel forcé du Garbage Collect

- Libération du cache (System.Web.Caching)

- Recyclage du processus (relance automatique de l'application)

Pour savoir quand faire appel à ces mécanismes, .NET se base sur deux indicateurs : « Memory Pressure » et « Memory Limit ».

Memory Pressure et Memory Limit

Régulièrement durant le cycle de vie de l'application, .NET exécute un algorithme pour déterminer la marge de manœuvre de l'application vis-à-vis de la mémoire restante.

Cet algorithme est très simple, et répond à la formule suivante :

Memory Pressure = (Mémoire totale - Mémoire disponible) / Mémoire totale

La « Memory Limit » est un paramétrage situé dans le fichier Machine.Config. Il s'agit d'une valeur indiquant le pourcentage de la mémoire physique total de la machine que l'application ne doit pas dépasser. Sa valeur par défaut est de 60.

Si la valeur de Memory Pressure dépasse 90, ASP.NET tente alors de libérer des entrées du cache (System.Web.Caching) et déclenche un Garbage Collect. Si cela ne suffit pas, et que la consommation mémoire de l'application continue d'augmenter jusqu'à dépasser la « Memory Limit », ASP.NET déclenche le recyclage du processus, qui consiste ni plus ni moins en un arrêt-relance. La conséquence pour l'utilisateur est une indisponibilité de quelques secondes et la perte de la session si elle n'est pas stockée sur un serveur tiers.

Avec ce système, il est en théorie impossible que ASP.NET renvoie une des redoutées « OutOfMemory Exception ». Et pourtant, ces erreurs apparaissent et deviennent un fléau sur nombre d'applications. Pourquoi ? A cause justement d'un effet retords de ce système.

Tout système ayant ses limites…

Petit rappel : sur une machine disposant d'un système d'exploitation 32 bits, la taille maximale de mémoire adressable pour un processus est de 4 go. Windows se réserve 2 go sur cet espace, il ne reste donc plus que 2 go par processus.

Considérons maintenant l'exemple d'une machine disposant de 4 go de ram, et reprenons les conditions utilisées :

- La Memory Pressure atteint 90 % => 90% de 4 go représentent 3,6 go, taille que le processus ne pourra jamais atteindre

- L'occupation totale en mémoire de l'application est à moins de 10 % de la « Memory Limit » => La Memory Limit est fixée à 60 % de la mémoire totale, soit 2,4 go. L'application sera donc en Memory Pressure si elle consomme au moins 2,16 go, taille que le processus ne pourra jamais atteindre

Résultat, ASP.NET ne se met jamais en état d'alerte, et des exceptions se mettent à apparaitre un peu partout, avec les conséquences néfastes que l'on peut facilement imaginer.

Et sur un serveur possédant 3 go de ram ? En faisant rapidement le calcul, on déduit que le processus est recyclé si la consommation de mémoire excède 1,8 go. Pas de problème alors ? Et pourtant, là encore les « OutOfMemory » guettent. Comment est-ce possible ?

La mémoire virtuelle et ASP.NET

Plongeons un peu dans les mécanismes d'allocations de la mémoire utilisés par .NET. Le processus ASP.NET a une quantité de mémoire qui lui est réservée, appelée mémoire virtuelle. Le nom de virtuelle lui vient du fait qu'elle ne représente pas forcément de la mémoire physique : Windows la gère comme bon lui semble, gardant certains morceaux à divers endroits de la RAM, plaçant d'autres morceaux sur le disque dur dans le fichier d'échange (swap)… Comme ce remue-ménage serait difficile à gérer de la part des applications qui s'exécutent, Windows leur fait croire qu'elles disposent d'un espace mémoire en RAM contigu, et s'occupe de faire le lien avec la mémoire physique (qui peut tout à fait ressembler à un gruyère) de manière transparente. A charge pour l'application de préserver son espace de mémoire virtuelle contigüe, Windows ne pouvant quand même pas s'occuper de tout.

De son côté, ASP.NET stocke les objets par piles, en général de 64 mo. Si un nouvel objet est alloué, et que la pile correspondante est pleine, ASP.NET parcourt la mémoire à la recherche d'un espace contigu de 64 mo pour créer une nouvelle pile. S'il ne trouve pas, il fait appel à Windows, qui accepte gracieusement de lui agrandir son espace de mémoire virtuelle. Mais progressivement, à force d'allocations et de libérations, la mémoire finit par se fragmenter : des espaces de mémoire libres apparaissent, trop petits pour créer une nouvelle pile. Ce phénomène est aggravé par le mode de chargement des Assemblies, qui peuvent se positionner dans la mémoire à leur adresse favorite, au lieu de se positionner bien sagement en bloc au début de l'espace mémoire (je ne critique pas, je constate). Si la fragmentation de la mémoire n'est pas maitrisée, ASP.NET va finir par atteindre la limite des 2 go, et Windows va refuser d'agrandir encore la mémoire virtuelle. N'ayant pas d'alternative (l'espace mémoire réellement occupé n'a pas atteint la « Memory Limit »), ASP.NET renvoie une erreur « OutOfMemory » alors qu'il reste de la mémoire libre.

En conclusion ?

Même s'il ne concerne pas toutes les applications, et qu'ilpeut être réduit par un ensemble de bonnes pratiques, le phénomène de fragmentation peut nuire à la stabilité d'une application, et le programmeur doit donc apprendre à composer avec. Alors comment régler la « Memory Limit » ? Chaque cas est unique, et cela dépend donc de la propension de l'application à fragmenter. Mais d'une manière générale, je recommanderai de ne pas dépasser les 1,4 go, voire 1,2 go. Donc n'oubliez pas, si l'on vous confie une application ASP.NET tournant sur un système d'exploitation 32 bits, n'oubliez surtout pas de vérifier ce paramétrage dans le fichier machine.config.

La gestion de m&#233;moire en .NET

Vu que je pense consacrer une large partie de mes prochains billets aux problématiques de mémoire, je pense qu'il est intéressant de commencer par un très bref rappel de la gestion de la mémoire dans .NET.

Le Garbage Collector

Je suppose que la majorité des gens qui lisent ces lignes savent déjà que .NET est une machine virtuelle. Au sein de cette machine virtuelle, toutes les allocations et désallocations de mémoire effectuées par du code managé (depuis le code .NET donc) sont tracées par le Garbage Collector (GC). Il est ainsi capable de déterminer quels objets maintiennent une référence vers un autre objet, et sait donc quand un objet n'est plus utilisé. Il peut alors libérer la mémoire qu'il occupait.

Quand un objet n'est plus référencé, il n'est pas libéré tout de suite. La libération de la mémoire par le GC étant couteuse en temps processeur, elle n'est effectuée que de temps en temps, selon plusieurs critères que je détaillerai par la suite. Quand le GC fait un "Garbage Collect", il parcourt la liste des objets qui ont été alloués à la recherche de ceux qui ne sont plus référencés, et les libère.  Pour optimiser ce traitement, un système de "génération" est mis en place :

A leur création, tous les objets sont alloués dans un heap dit "Generation 0". Au cours d'un garbage collect, les objets qui ne sont pas libérés sont déplacés vers un autre heap, dit "Generation 1". Au garbage collect suivant, s'ils ne sont toujours pas libérés, ils sont à nouveau déplacés vers un autre heap, appelé "Generation 2". Arrivés là, ils ne bougeront plus tant qu'ils seront référencés.

Quel est l'intérêt de ce système ?

Les concepteurs du Garbage Collector de .NET ont ici considéré que les objets utilisés dans une application ont soit une durée de vie très courte, soit très longue. Ce qui à l'usage se révèle en général vrai : les objets temporaires déclarés dans une fonction sont libérés dès la fin de l'exécution de celle-ci (durée de vie courte), tandis que certaines classes, comme les Form, peuvent être utilisés pendant presque toute la durée d'exécution de l'application (durée de vie longue).

A partir de ce constat, le Garbage Collector nettoie souvent la génération 0 (dont la plupart des objets seront libérés), et beaucoup moins souvent la génération 2 (qui contient les objets à longue durée de vie, et donc qui ont peu de chance d'être libérés). Le temps processeur utilisé par le Garbage Collector est ainsi réduit.
Au passage, la génération 2 est un très bon indicateur pour diagnostiquer la présence de fuites de mémoire : si le nombre d'objet en génération 2 (ce nombre peut être surveillé par des compteurs de performance) ne cesse d'augmenter au cours de l'exécution d'une application, il y a de fortes probabilités que des objets ne soient pas libérés quand il le faudrait.

Lors des Garbage Collect, en plus de libérer les objets qui ne sont plus utilisés, le GC en profite pour compacter la mémoire : il déplace et regroupe les objets restants en mémoire de manière à créer des segments de mémoire contigus les plus larges possibles. Si vous avez du mal à vous représenter ce remue-ménage, c'est exactement ce que font la plupart des défragmenteurs de disque : déplacer les fichiers dans les premiers secteurs du disque, pour créer un seul bloc d'espace libre qui s'étend jusqu'à la fin du disque.

Le Large Object Heap

La dégragmentation présentée précédemment, aussi utile soit-elle pour garantir une utilisation optimale de la mémoire, présente un inconvénient majeur : si les objets en mémoire sont très gros, elle peut devenir très couteuse en temps processeur. C'est là que fait son entrée le Large Object Heap.

Aux trois générations du Garbage Collector se rajoute ce quatrième heap un peu particulier.Il a deux particularités :

- Il stocke uniquement les objets de taille importante (supérieure à 85 ko)

- Il n'est jamais défragmenté

Après mon explication précédente, son intérêt devient immédiat : il évite que le Garbage Collector se mette à déplacer des objets volumineux en mémoire, ce qui aurait un impact négatif sur les performances. La contrepartie est que ce heap est très sensible à la fragmentation. Il faut donc que le développeur veille à libérer le plus vite possible les objets volumineux pour ne pas l'encombrer inutilement.

Déclenchement du "Garbage Collect"

Attardons nous maintenant sur le déclenchement du Garbage Collect. Il se produit lorsqu’une des trois conditions suivantes est remplie :

- Un appel à la fonction GC.Collect() est fait depuis le code ou depuis une fonction .NET native

- Le nombre d'objets dans la génération 0 dépasse une certaine limite (je ne saurais vous dire combien)

- Le système est dit en état de "Memory Pressure" (Il s'agit d'un algorithme utilisé par .NET pour déterminer quand les besoins en mémoire de l'application commencent à dépasser ce que la machine peut lui fournir)

Il est très rare qu'un développeur ait à appeler manuellement la fonction GC.Collect, ce qui est par ailleurs formellement déconseillé dans une application ASP.NET : le "Garbage Collect" étant succeptible de déplacer des objets en mémoire, tous les threads sont suspendus durant son exécution.

Une gestion de la mémoire maitrisée ?

Tout ce beau monde tourne sans que le développeur n'ait à gérer quoi que ce soit, et dans la grande majorité des cas la gestion de la mémoire de l'application .NET ne posera pas de problème et saura parfaitement se faire oublier. Mais (parce qu'il y a toujours un "mais"), aussi rodé ce système soit-il, il existe des situations ou des pièges qui feront que cette gestion de la mémoire se retournera contre le développeur. Et ceux sont quelques unes de ces situations que je me propose de vous exposer dans mes prochains billets.

DataSet et .NET 1.1 : Attention !

De part ses similitudes avec la structure des données en base, le DataSet (et les classes gravitant autour : DataView, DataTable, DataRow, ...) est un outil de manipulation de données très appréciable. Cependant, son utilisation peut mener à quelques pièges, dont certains peuvent être fatals pour la stabilité de l'application.

Dans une application ASP.NET classique orientée autour d'une base de donnée, nous sommes souvent amenés à afficher des donner dans un tableau. A cet usage, un DataGrid alimenté par un DataSet ou un DataView suffit dans la plupart des cas, et permet de produire un code souple et efficace en peu de temps.

Si la source de données comporte beaucoup de résultats, il devient alors utile d'ajouter un système de pagination. Toujours dans une optique de simplicité, le développeur sera tenté de réaliser cette pagination directement dans le code C#, à l'aide de tous les outils fournis par ASP.NET. Je ne m'attarderais pas aujourd’hui sur les risques d'une telle approche (un indice toutefois : Large Object Heap), pour me concentrer sur un piège plus vicieux et plus dangereux.

 

Mise en situation

C'est l'histoire d'une fonction de recherche particulièrement lourde (temps d'exécution de l'ordre de la dizaine de secondes) sur une page ASPX appelée "Page Y", d'un ingénieur, et d'un client exigeant. Le client, non satisfait (à raison) de la lenteur de chargement de la page, demande à l'ingénieur de remédier au problème. L'ingénieur, après avoir appliqué toutes les astuces d'optimisation simples sur sa procédure stockée (je vous ferai sans doute un billet sur l'utilisation du plan d'exécution sous SQL Server si ça en intéresse certains), décide que la page Y serait bien plus agréable à utiliser si les données n'étaient pas rechargées systématiquement à chaque opération de l'utilisateur (tri par exemple). Le candidat idéal apparaît alors rapidement : la session. Ni une, ni deux, notre ingénieur place le DataSet stockant le jeu de résultat dans la session, pour le conserver d'un appel à l'autre de notre chère page Y.

Un client heureux

Le problème de performance est corrigé et le client est satisfait, notre ingénieur peut dormir sur ses deux oreilles. Mais d'évolution en évolution, l'application s'embellit, les visiteurs sont de plus en plus nombreux, et les besoins de mémoire vont crescendo. Commencent à apparaître des problèmes graves de mémoire et, à nouveau mécontent, le client refait appel à notre ingénieur.

Celui-ci, après analyse méthodique des logs, découvre que les problèmes de mémoires apparaissent sur la plupart des pages. Une analyse plus poussée montrera une tendance : en début de journée, la page Y est la première à manquer de mémoire, avant que le problème ne s'étende sur le reste de l'application. De plus, les utilisateurs rapportent que se déconnecter et se reconnecter permet de s'affranchir des erreurs pendant un temps.

Mais que se passe-t-il ?

Fort de son expérience, notre ingénieur fait tout de suite le rapprochement entre la session et la déconnexion de l'utilisateur... et retourne donc 'intéresser à la page Y. La taille du DataSet est certes grande (plusieurs dizaines de kilo-octets), mais elle ne suffit pas à expliquer les problèmes de mémoire. En étendant son champs de recherche, l'ingénieur se souvient alors que, cluster de serveurs oblige, la session est stockée dans une base de données. Après quelques recherches pour retrouver dans quelle table est stockée la session, notre ingénieur concocte une petite requête pour déterminer la taille de la session en base, avant de tomber de son siège.

Profitons de ce moment d'hébétement chez notre ingénieur pour comprendre à notre tour ce qu'il se passe. Au début de l'exécution de chaque page, ASP.NET récupère les données de session depuis la base de données, et les renvoie à la source à la fin du traitement. Les données sont stockées en base dans un champ binaire, ASP.NET appelle donc fort logiquement le serializer binaire sur chaque objet avant de les envoyer dans la base, et fait la manipulation inverse avant de les récupérer.

L'étude de l'objet DataSet avec Reflector (ou une petite recherche sur Internet) nous montre que, même par le serializer binaire, le DataSet est stocké en XML. Donc un simple DataSet contenant une colonne "nombre" et cinq lignes peut devenir quelque chose comme :

<NewDataSet>
  <Table1>
    <nombre>1</nombre>
  </Table1>
  <Table1>
    <nombre>2</nombre>
  </Table1>
  <Table1>
    <nombre>3</nombre>
  </Table1>
  <Table1>
    <nombre>4</nombre>
  </Table1>
  <Table1>
    <nombre>5</nombre>
  </Table1>
</NewDataSet>

Fort de cette information, nous pouvons jeter un oeil sur l'écran de notre ingénieur qui se relève péniblement sur sa chaise, et nous constatons que son DataSet occupe la bagatelle de 3 méga-octets dans la base de données. A cela, ajoutons l'espace mémoire supplémentaire nécessaire pour désérialiser puis resérialiser l'objet, multiplions par le nombre moyen de pages visualisées par un utilisateur ayant notre DataSet en session, et nous comprenons rapidement que nous avons un problème.

Que faire alors ?

Déjà, il faut savoir que ce problème a été corrigé dans .NET 2.0 (parait-il en tout cas, j'avoue que je n'ai pas encore vérifié). Donc le problème ne se pose que pour ceux qui utilisent le Framework 1.1. A partir de là, la solution la plus évidente est : ne pas mettre le DataSet en session, et essayer de s'en sortir par des moyens alternatifs (comme le cache, si le jeu de résultats est partagé par tous les utilisateurs). Ce n'est hélas pas toujours possible, et s'il n'est pas possible de s'affranchir de l'utilisation de la session, je recommande l'utilisation de l'objet DataSetSurrogate, téléchargeable à l'adresse suivante :

http://support.microsoft.com/kb/829740

Il s'agit d'un objet récupérant les informations du DataSet, et se sérialisant en binaire. Son utilisation est très simple :

 

DataSet ds = new DataSet();

// Mise en session
Session["monDataSet"] = new DataSetSurrogate(ds);

// Récupération des données, je passe les contrôles de rigueur
ds = ((DataSetSurrogate) Session["monDataSet"]).ConvertToDataSet();

 

Un nouveau test dans la base de données montre que le même DataSet que précedememnt occupe maintenant moins de 200 kilo-octets. Ce n'est pas parfait, mais cela devient suffisant pour corriger le problème de mémoire. Le client est à nouveau heureux (je plaisante, c'est un client quand même), et notre ingénieur peut retourner rêver d'un projet en .NET 3.5.

 

Cet ingénieur, cela aurait pu être moi, et cela aurait pu être vous. Alors ne prenez jamais à la légère l'utilisation des DataSet, surtout en .NET 1.1.

Son of Strike sous Visual Studio 2005

WinDbg couplé à son extension SOS (Son of Strike) est un outil extrêmement puissant (que j'essaierai de vous présenter à l'occasion), utilisable à des fin de déboggage/profiling sur une application .NET. Cependant, les deux moyens classiques pour analyser une application depuis WinDbg sont :
- En réalisant des dumps de la mémoire, ce qui est vite contraignant et prend de la place sur le disque, mais c'est souvent le seul moyen pour une application en production
- En s'attachant au processus, ce qui implique que vous n'ayez pas déjà attaché le debugger de Visual Studio

Pour ceux qui souhaitent utiliser les fonctionnalités de WinDbg/SOS en même temps que le debugger de Visual Studio, sachez qu'il est possible d'en exécuter les commandes directement depuis l'IDE (depuis la version 2005) :

- Dans un premier temps, il faut activer le déboggage du code non managé, dans les propriétés du projet

 dbg1

- Ensuite, lancez votre application et à tout moment appuyez sur le bouton pause (inutile de mettre un point d'arrêt)
- Enfin, dans la fenêtre d'exécution immédiate, tapez :

.load sos

dbg2
- Et c'est tout !

A noter quand même que l'exécution des commandes s'en trouve fortement ralentie. Donc pour analyser une application occupant plusieurs centaines de méga-octets de mémoire vive, je suggère vivement de continuer à passer par des dumps ;)

Parce qu'il faut bien commencer quelque part...

En continuation directe de l'angoisse de la page blanche, je pense que ce premier message sera le plus difficile du lot (je l'espère tout au moins).

Après des années d'hésitations, j'ai décidé de me lancer dans l'aventure, et ouvrir ce qui peut être communément appelé "Blog technique". Je partagerai ici mes expériences et mon point de vue sur ce qui touche à l'informatique et la programmation, essentiellement autour des technologies de Microsoft (ce qui ne m'empêchera pas de faire quelques écarts pour aborder le monde "péri-Billou").

Pour poser les bases, je vais commencer par une rapide présentation de moi même :

Je m'appelle Kévin (nom pas toujours facile à porter) Gosse (bis repetitae), jeune diplômé de l'EFREI.

Je suis tombé dans la programmation dès mon plus jeune age, avec ce bon vieux QBASIC, puis Visual Basic. Vint une période "touche à tout" qui me permit d'essayer nombre de langages, VB.NET, une longue période PHP, avant de finalement revenir sur C#, étape actuelle de mon voyage.

Professionnellement, je suis un jeune diplômé de l'EFREI travaillant chez une SSII répondant au nom de Novedia Solutions (feu Smart-Up). Bien qu'elle sera la cible de quelques uns de mes billets sur ce blog (surtout les premiers en fait), je ne vais présenter que brièvement l'application sur laquelle je travaille :
- .NET 1.1, ASP.NET/C# (ça date mais on a vu pire)
- SQL Server 2000 (sympathique si on ne regarde pas de trop près)
- Commerce Server 2002 (un outil qui doit être très bon couplé avec ASP3, mais que je déconseille formellement dans un projet .NET...)

Voilà. Je peux ajouter aussi qu'elle souffre de quelques problèmes de performance/consommation mémoire qui m'ont permit de me faire mes armes sur plusieurs outils de profiling, expérience que je ne manquerai pas de partager.

Parce qu'il ne faut pas trop en faire dès la première fois, je vais arrêter ici cette introduction, et vous remercier d'avoir prit le temps de lire ces quelques lignes. Je remercie au passage Julien Chable (alias Neodante) pour s'être chargé des formalités de création de ce petit blog, sans qui je serais probablement en train d'écrire ces lignes dans un petit sous-répertoire perdu d'un serveur web coupé de la civilisation (GoogleBot est un lecteur assidu mais n'est pas très bavard).



Les 10 derniers blogs postés

- Monitoring et Patron de méthode par Le blog de Marc Ranchin le il y a 2 heures et 48 minutes

- ADO.NET Data Services Hooking POC v2 par Matthieu MEZIL le il y a 3 heures et 2 minutes

- Back from NYC ! par .net is good... C# is better ;) le il y a 3 heures et 40 minutes

- Hello World! par Le blog de hamid le il y a 11 heures et 48 minutes

- MSBuild Extension Pack sur codeplex par Michel Perfetti [Miiitch] le il y a 13 heures et 7 minutes

- TCB : Travailler en équipe sans réseau par The Mit's Blog le il y a 16 heures et 17 minutes

- Accès anonyme et les pages Forms / viewlsts.aspx... par Nicolas Humann le il y a 20 heures et 11 minutes

- l'Atelier 4 du coach C# est disponible par Bernard Fedotoff le il y a 21 heures et 46 minutes

- [WPF] Formatter l’affichage lors d’un binding, via StringFormat par Thomas Lebrun le 10-07-2008, 10:22

- WSC08 : Le bilan, Les Photos, Les Webcasts à voir ou à revoir par Blog de Daniel TIZON [daniel] le 10-07-2008, 01:14