SOCIÉTÉ

BLOGS

J'ai la mémoire qui flanche : à table !

06.01.2012
par Olivier Deschanels
A table !

Qui n'a jamais eu des messages du serveur annonçant que la mémoire est saturée ? Dans ce billet nous allons voir qu'il est très simple de perdre de la mémoire amenant à la saturation de cette dernière pouvant aller jusqu'au crash.
 

Comme il y a plusieurs type de mémoires, il y a plusieurs moyens de la perdre ! En effet nous distinguerons principalement deux types de mémoires : la mémoire moteur et la mémoire cache.
La mémoire moteur a la charge de faire fonctionner l'application. La mémoire cache de son côté est destinée aux données utilisées par l'application. Ainsi un enregistrement sera chargé en mémoire cache alors qu'une variable sera chargée en mémoire moteur.

Table des variables inter-process

Afin de gérer les variables définies par l'utilisateur, le moteur de 4D utilise une structure en mémoire. Cette structure est communément appelée "table de variables". La table est construite au moment de la compilation et est donc figée à ce moment-là.
Dans une application compilée toutes les variables inter-process sont chargées dans une table des variables inter-process unique sur chaque poste client, mais également sur le serveur. Cette table, dont la taille peut varier de quelques octets à plusieurs dizaines de kilo-octets est une zone préparée en mémoire, dès le démarrage de l'application, et destinée à recevoir les instances de chaque variable inter-process. Pour toutes les variables scalaires (autrement dit de taille fixe : entier, booléen, date ...) l'empreinte mémoire est donc en place dès le lancement de l'application. Pour les variables non scalaires, c'est une zone minimale qui est réservée. Ainsi pour une image, la variable inter-process sera préparée avec l'équivalent d'une image vide ne pesant que quelques octets, alors que lorsque cette image contiendra le joli logo de votre client, elle occupera peut-être plusieurs méga-octets. Les variables non scalaires sont les variables "à taille variable" telles que les images comme nous venons de le voir, mais également les blobs, les textes (et donc les alphas maintenant que le distingo est tout relatif depuis le passage à l'Unicode) et surtout les tableaux. En effet l'empreinte mémoire d'un tableau grossira, cela ne surprendra personne, en fonction du nombre d'éléments ajoutés dans le tableau.

Table des variables process

Pour les variables process le principe est exactement le même. Au lancement de chaque nouveau process une table des variables process est créée, quel que soit le process. Il y a donc autant de tables des variables process que de process lancés sur le poste. Supposons que cette table des variables process occupe 250 kilo-octets à son initialisation. Si le poste lance 6 process, c'est donc 1,5 méga-octets qui vont être pris environ (1,46 pour être précis) par les différentes copies de la table des variables process permettant ainsi à chaque process d'avoir sa propre instance de chaque variable process. Bien sûr cela va augmenter avec l'alimentation en valeurs des tableaux et des variables non scalaires.

Lorsque je dis que la table des variables process est créée pour chaque process du poste cela est valable pour tous les postes, qu'ils soient client ou serveur. Et là il y a une réelle difficulté potentielle pour qui ne maîtrise pas la taille de la table des variables process. En effet sur le serveur il y a bien plus de process que sur le client. Pour chaque process lancé sur un poste client, un process répliqué est généré sur le poste serveur. En reprenant notre exemple précédent, avec 10 postes clients nous avons donc 60 process répliqués sur le serveur ayant chacun une table des variables process pesant à vide 250 kilo-octets, soit près de 15 méga-octets pour les tables des variables process sur le serveur. Si de plus le serveur fait tourner quelques procédures stockées, chacune aura également sa propre table des variables process. Il en ira de même pour les process web.
Nous voyons dans ces calculs qu'il y a un facteur multiplicatif qui rentre en jeu et donc plus l'on désire étendre le nombre de postes, moins il est anodin de tenir compte de cette taille des variables process.

Avant d'avoir compris ce principe de fonctionnement, la réponse classique lorsque je pose la question sur le nombre de variables ressemble à peu près à : "nous ne sommes plus au temps du Mac 512, et la mémoire n'est pas chère aujourd'hui ..."
Certes la mémoire n'est pas chère et je vous invite à en donner autant que vos budgets le permettent à vos postes mais surtout et en priorité au serveur. Aujourd'hui, des configurations avec des centaines de postes clients sont fréquentes et dans ce cas il faut compter la mémoire et éviter le gaspillage.

Réduire l'empreinte mémoire

Il y a plusieurs façons de diminuer la mémoire nécessaire à la gestion des variables process sur le serveur.

La première façon est de limiter le nombre de process. En effet chaque process client ayant sa réplique sur le serveur, si l'on réduit le nombre de process sur le client cela a un effet direct sur la mémoire du serveur. Regardons de près les process sur le client : ils sont certainement tous utiles (sinon la solution est toute trouvée !). N'y a-t-il pas des process qui pourraient devenir des process locaux ? Typiquement, ce process permettant d'afficher une barre de progression lors des traitements longs accède-t-il aux données ? C'est rarement le cas ; dans cette hypothèse il est surement avantageux de le rendre local au poste. Ce process ne sera alors plus répliqué sur le serveur. Si ce process est présent sur chaque poste client cela sera autant de gain sur le serveur.

L'autre voie à suivre est la réduction du nombre de variables. Ce travail n'est pas toujours très simple et doit se faire dans la durée. Plusieurs axes de travail sont disponibles parmi lesquels :

  • Eviter les doublons : est-il utile d'avoir une variable "id_client" et une autre "client_id". Une unification des noms permettra de diviser le nombre de variables.

 

  • Favoriser les variables locales ou inter-process. Cette variable servant de compteur dans une boucle est-elle utile en tant que variable process ? Une locale fera sûrement parfaitement l'affaire. Cette variable contenant une constante de la base telle que l'adresse IP du serveur de messagerie ne devrait-elle pas être inter-process plutôt que process ?

 

  • Utiliser les variables dynamiques. Dans les formulaires, il est possible d'utiliser des variables dynamiques qui sont allouées au fil des besoins par 4D et qui ont le grand avantage de ne pas avoir d'empreinte dans la table des variables process. Le codage nécessite un peu plus d'attention au début, mais lorsque le principe est maîtrisé cela offre une grande souplesse sans surcharger la mémoire sur le serveur. Posez-vous la question pour une variable utilisée pour afficher une information dans un formulaire, de son utilité sur le serveur.

 

  • Supprimer les variables process inutiles, comme par exemple les variables associées aux boutons. Si vous n'utilisez pas la valeur stockée dans la variable lorsqu'on clique sur un bouton, alors ne définissez pas la variable.

 

  • Utiliser les expressions ; dans un formulaire il est possible de dessiner une variable et de mettre dans le nom de la variable une expression. Cette expression prendra la place d'une variable qu'il aurait fallu définir par ailleurs et dont l'empreinte aurait été visible dans tous les process et tous les postes.


Passer sur la balance !

Faire des changements c'est bien mais pouvoir en mesurer les impacts c'est mieux ! Il est donc utile de pouvoir mesurer la taille des tables de variables. Pour cela il existe un outil : le fichier des symboles. Vous devez demander la mise à disposition de ce fichier dans le panneau des préférences du compilateur.
Ce fichier est généré suite à la compilation de votre base et contient toute une série d'informations dont les deux tailles qui nous préoccupent aujourd'hui. Vous pourrez alors avoir une idée précise de la taille mémoire nécessaire sur le serveur en refaisant les calculs précédents avec vos valeurs.

Effacer les variables

La commande EFFACER VARIABLE permet d'effacer le contenu d'une variable (son instance) mais pas de libérer l'empreinte de la variable dans la table des variables process ou inter-process.
Ne cherchez donc pas de ce côté une planche de salut en truffant votre code d'effacement de variables.

Attention aux objets complexes

Depuis longtemps 4D propose des objets complexes comme les listes hiérarchiques, des menus ou les arbres XML.

Lorsque l'on déclare une liste hiérarchique il faut bien comprendre ce qu'il se passe en mémoire. Ecrivons la ligne suivante :

hList:=nouvelle liste

Nous venons de déclarer une liste hiérarchique en mémoire. Mais attention la mémoire utilisée n'est pas limitée à la variable hList. Si l'on regarde de près la variable hList est un simple entier long. Il est évident que ce n'est pas dans un simple entier long que la liste hiérarchique peut être construite. Elle doit être ailleurs. Alors dans ce cas qu'est-ce que cet entier long ? L'entier long contenu dans notre variable hList est un numéro de référence permettant à 4D d'accéder à la bonne zone mémoire qu'il a attribué par ailleurs pour stocker la liste hiérarchique. Nous sommes donc en présence d'une espèce d'indirection. Lorsque cette notion est mal comprise, nous avons de fortes chances de provoquer une fuite mémoire. En effet, si le process se termine, la table des variables process est libérée, libérant l'instance de la variable hList. Mais rien n'est fait pour la zone mémoire qui était référencée par la variable hList. Cette zone mémoire est occupée, mais nous avons perdu le moyen d'y accéder avec la suppression de la variable hList. Seul un arrêt de l'application libérera la mémoire ainsi perdue. C'est ce que l'on appelle une fuite mémoire. Lorsque ce phénomène se répète régulièrement l'utilisateur nous signale qu'au bout d'un certain temps son application ne fonctionne plus correctement ... lorsqu'elle ne crashe pas sans plus d'avertissement.
Pour remédier à ce problème il faut prendre soin de libérer de la mémoire les listes hiérarchiques devenues inutiles et avant suppression des variables contenant les références permettant d'y accéder. Pour cela il faut utiliser la commande SUPPRIMER LISTE qui se chargera de faire le travail en mémoire.

Bonnes pratiques

Une bonne pratique consiste à écrire en miroir et de manière défensive.
L'écriture en miroir consiste à écrire dans la même méthode la création et la suppression de la liste. L'écriture défensive consiste à tester l'existence éventuelle de la liste avant d'en créer une nouvelle instance. Ceci est assez fréquent lorsqu'on utilise des listes dans des formulaires d'entrée. Voici un exemple de code :

C_ENTIER LONG(vHList)

Au cas ou
    :(evenement formulaire=sur chargement)

    Si (Liste existante(vHList))
        SUPPRIMER LISTE(vHList;*)
    Fin de si
    vHList:=nouvelle liste
    ...

    :(evenement formulaire=sur liberation)

    Si (Liste existante(vHList))
        SUPPRIMER LISTE(vHList;*)
    Fin de si

Fin de cas

L'étoile en dernier paramètre de la commande SUPPRIMER LISTE permet de supprimer également les sous-listes associées à la liste vHList. La non suppression des sous-listes est également un facteur produisant des fuites de mémoire.

Ne pas supprimer un arbre XML, ou une image SVG produira le même type de phénomène. Rendez-vous dans la documentation pour relire les commandes permettant de nettoyer proprement la mémoire.

Limiter les images

Vous donnez la possibilité à vos utilisateurs de charger des images. En contrôlez-vous la taille ? L'image que votre utilisateur charge dans votre base est peut-être en haute résolution (300dpi) alors qu'elle est destinée à être affichée à l'écran qui ne gère pas cette résolution (qui est de l'ordre de 72 dpi). Il faut alors penser à réduire les images de taille disproportionnées grâce aux commandes de manipulation des images (CONVERTIR IMAGE, TRANSFORMER IMAGE...) et ainsi préserver la mémoire qui sera nécessaire pour l'utiliser par la suite.
A nouveau je vais lutter contre les méthodes génériques utilisées sans recul (voir mes billets précédents à ce sujet). Nombre de fois je trouve des bases ayant la même méthode d'initialisation sur le poste client et sur le poste serveur. Est-ce utile sur le serveur de charger toutes les images agrémentant les formulaires d'interface qui ne seront jamais affichées sur ce poste. Ici la "méthode générique qui ne prend pas de risque en chargeant tout" fait exactement le contraire du but recherché : elle réduit la mémoire disponible sur le serveur et donc abaisse le niveau d'absorption possible sur ce poste essentiel. Ne charger que les images strictement nécessaires est essentiel. On peut même aller plus loin en ne chargeant les images que lors de la première utilisation. C'est assez facile à coder, je vous invite à y penser !


Dans ce premier billet sur la mémoire nous venons d'examiner la mémoire moteur et plus spécifiquement les variables. Restent de nombreuses autres zones de la mémoire à explorer dans de futurs billets.

RSS 0 commentaire(s) pour ce billet

Poster un nouveau commentaire

  • Les adresses de pages web et de messagerie électronique sont transformées en liens automatiquement.
  • You may use [view:viewname] tags to display listings of nodes.
  • Each email address will be obfuscated in a human readable fashion or (if JavaScript is enabled) replaced with a spamproof clickable link.

Plus d'informations sur les options de formatage