________________________________________________________________ ____ __ __ / ___________________________________________________________ ____ __ _ / ` \ / \[O2]/ + ,\_____ ___/ . ` |\/ , \__/ . \____ /:/=[ À la conquète du chameau #6 ] =\:\ _____/ ` x || , . " :X \_______________________________ ____ ____ / , ` ` || + , () . x , . ` ' ` \_ \ ,\\_ +\_ \ 'M ` | . || ` | ` , ` , + , ` ,--, + . \\_ \_ \ , \\_ K -+- / |+ -+- . , ` , ,',/ ,'. \_ \ . \\_ \_ \ - | ` / . | ; + + . ` | : . :`` ,_): ' ` \\_ \_ \ \\_` 1 , / ` . . x , .-=+=- `. ,. ,' ` , \_ \ + \\_` \_ \ 1 /\_____ x ' , . . X | ` `--' ` ` \\_ \_ \ , \\_ 1 + \\_ \___________________ , . . ` , + ' , , ; x ._\ \___\\___\ \ . \_ \ \__________________________________/ \ ` , \\_ /:/=[ Aka: votre chameau souffre-t-il d'alzheimer ? ]=\:\ \ \__\__________________________________________ ______________________________ \` \__\ /__/ \\_ /:/=[ Nothing94 ]=\:\ _// \__\________________________/__/ +-------------------------------------------------------------------------------------------+ | Web : nothing94.250x.com | | Email : philippe.cote@usherbrooke.ca | | Version : 1.2.0.2 | +-------------------------------------------------------------------------------------------+ +-------------------------------------------------------------------------------------------+ | Table des matières | +-------------------------------------------------------------------------------------------+ 01 - Objectif 02 - Introduction * Exemple 0x0001 : Référence circulaire * Exemple 0x0002 : Tie * Exemple 0x0003 : Hériter de Tie::StdScalar * Preuve 0x0001 : Les modules Tie sont cons 03 - Ce que nous voulons 04 - Devel::Monitor 05 - Utilisation de Devel::Monitor * Exemple 0x0004 : Utilisation de "monitor" * Exemple 0x0005 : Utilisation de "monitor" 06 - Quelques problèmes avec les constantes 07 - Outil d'insertion de moniteurs 08 - Outil de visualisation d'erreurs * VerifyMonitor.pl 09 - Comment régler les références circulaires 10 - Impossible de weaken-er un objet tied * Preuve 0x0002 : Test de base * Preuve 0x0003 : mod_perl * Preuve 0x0004 : Assaut final 11 - Utilisation de Devel::Monitor * Exemple 0x0006 : Utilisation de "print_circular_ref" 12 - Impossible d'utiliser les références des objets tied 13 - Conclusion 14 - Signature 15 - Post-scriptum +--------------------------------------------------------------------------------------------+ | Chapitre un | | | | Objectif | +--------------------------------------------------------------------------------------------+ Vous avez un site web en Perl qui comprend beaucoup de lignes de code. Vous voulez le faire rouler sur mod_perl, mais il y a des memory leaks. Vous avez donc besoin d'un outil pour trouver et supprimer les références circulaires. +--------------------------------------------------------------------------------------------+ | Chapitre deux | | | | Introduction | +--------------------------------------------------------------------------------------------+ Bonjour groupe, Êtes-vous en forme ? Ouuiiii J'ai dit, Êtes vous en forme ? OOOUuUUUiiiII Bon, nous pouvons commencer. Tout d'abord vous savez ce qu'est une référence circulaire... +--------------------------------------------------------------------------------------------+ | Exemple 0x0001 : Référence circulaire | +--------------------------------------------------------------------------------------------+ 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 5 { #Nouveau scope 6 my $a; #Une variable nommé "$a" 7 $a = \$a; #La variable $a pointe sur la référence de la variable $a 8 } +--------------------------------------------------------------------------------------------+ | EOF : Exemple 0x0001 : Référence circulaire | +--------------------------------------------------------------------------------------------+ Lorsque le scope se termine, Garbage et Collector ont toujours une discussion enlevante. Voici le log de leur conversation à la ligne 8. Ok les gars, faites le ménage. Hey, jveux deleter $a, cte variable là me gosse Non caliss, ya $a qui l'utilise Ben on a juste à deleter cette variable là aussi Non caliss, ya $a qui l'utilise Ben on a juste à deleter cette variable là aussi Non caliss, ya $a qui l'utilise Ben on a juste à deleter cette variable là aussi Non caliss, ya $a qui l'utilise Ben on a juste à deleter cette variable là aussi Non caliss, ya $a qui l'utilise Osti que vous êtes fatiguant, farmez vos yeules là Donc, on voit que Garbage et Collector n'arrivent pas à s'entendre sur la bonne décision à prendre. Ce qui fait que Perl, le médiateur, décide de les faire taire. Toutefois, à la ligne 9, qui est la fin du programme, Perl va puncher sa carte et s'en va boire une bière à la maison. Il n'existe donc plus d'autorité en ce bas monde donnant libre accès à nos deux travailleurs. Hey, Perl est parti, moi jdelete $a, là jtanné Non caliss, ya $a qui l'utilise *** Garbage delete $a *** Tin, ma job est faite, jmen va moi too Ok, on va tu boire une bière ? Bon, vous connaissez maintenant les références circulaires. Une variable n'est pas détruite tant qu'une référence pointe dessus. Toutefois, si notre programme doit rouler 24 heures sur 24, les pertes de mémoire de ce genre peuvent être fatales. C'est justement le cas avec mod_perl sur apache, qui va conserver en mémoire le processus pour accélérer les temps de réponses au client. Nous avons donc besoin d'outils pour détecter ces pertes de mémoire. Évidemment si vous programmez tout seul dans votre cave, vous pouvez vous arranger parce que vous connaissez votre code source en entier, mais si vous êtes une équipe de programmeurs, la tâche peut devenir plus fastidieuse. Certaines possibilités s'offrent à vous. - Utiliser Apache::Leak pour mod_perl 1.x (mod_perl 1.99 est considéré mod_perl 2.x) - Utiliser Devel::Peek qui ne fait pas la job dont vous avez besoin - Utiliser Devel::Leak qui ne fait pas la job dont vous avez besoin - Utiliser Data::Structure::Util qui fait parfois des segmentation fault en utilisant la fonction has_circular_ref et qui est assez incomplet Notez également que le problème de ces modules perl est qu'il vous donne des informations à l'endroit où vous mettez votre ligne de code. Donc, il faut déterminer manuellement la meilleure place où positionner la validation. Vous ne pouvez également pas utiliser valgrind (Un débuggeur mémoire *nix) parce que votre process doit rester en mémoire et doit donc être exécuter avec mod_perl. Autrement, le process se termine, et perl libère la mémoire à la toute fin, ce dont vous ne voulez pas. Cependant, il existe quelque chose de bien sympathique en Perl, que l'on appelle tie ! L'idée d'un tie, est de mettre une cravate à notre variable pour cacher les choses indésirables. Un politicien porte toujours une cravate, pour cacher ses vrais pensées. En d'autre termes, tous les accès à la variable vont passer par la cravate. Cette cravate peut donc décider ce qu'elle fait de la requête. +---------------------------------------------------------------------------------------------+ | Exemple 0x0002 : Tie | +---------------------------------------------------------------------------------------------+ 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 5 { 6 my $a; #Une variable nommé "$a" 7 tie $a, 'Tie::StdScalar'; #On pose une cravate de type scalaire standard 8 $a = 'something'; #La méthode standard STORE va être appelée 9 #et celle ci va décider de ce qu'elle fait. 10 #Elle pourrait par exemple décider d'effectuer 11 #l'affectation tel qu'espérée, ou bien 12 #empêcher l'affectation en prétendant que la 13 #variable est read-only. Ceci étant aux 14 #besoins du programmeur. 15 } #$a est détruite ici, donc la méthode standard DESTROY va être appelée. 16 #Encore une fois, nous pouvons faire ce que nous voulons. +---------------------------------------------------------------------------------------------+ | EOF : Exemple 0x0002 : Tie | +---------------------------------------------------------------------------------------------+ Nous voyons que le tie nous permet d'englobber une variable et de faire ce que nous voulons avec les transactions effectuées. Dans notre cas, nous avons besoin de savoir lorsque la variable est détruite (DESTROY). Donc nous pouvons imprimer un beau message disant que notre variable est détruite. Pour ce faire, nous pouvons hériter de l'objet Tie::StdScalar de base comme ceci. +---------------------------------------------------------------------------------------------+ | Exemple 0x0003 : Hériter de Tie::StdScalar | +---------------------------------------------------------------------------------------------+ package TestScalar; use Tie::Scalar; use base 'Tie::StdScalar'; sub DESTROY { print STDERR "TestScalar::DESTROY : $_[0]\n"; #Imprime l'objet détruit } +---------------------------------------------------------------------------------------------+ | EOF : Exemple 0x0003 : Hériter de Tie::StdScalar | +---------------------------------------------------------------------------------------------+ Cependant, cet exemple va imprimer des choses comme : TestScalar::DESTROY : SCALAR(0x12345678) Ce qui ne révèle en rien la variable qui va être détruite, si nous appliquons un tie sur 500 variables. Nous avons donc besoin d'un outil un peu plus puissant. Donc, j'ai fait un monitorizeur (nouveau mot) qui va tie-er (tie-er (verbe) : appliquer une "tie" sur une variable) n'importe quel type de variable (SCALAR, ARRAY, HASH, ...). De plus, je veux également pouvoir donner un nom à la variable que je monitor. Je voudrais faire ceci : monitor('ma variable $a' => \$a); qui en fait exécuterait ceci : tie $a, 'UnModuleQuelconque'; tout en conservant le nom "unique" de ma variable monitorée pour les prints. Notez que les modules Tie par défaut sont basiques : +---------------------------------------------------------------------------------------------+ | Preuve 0x0001 : Les modules Tie sont cons | +---------------------------------------------------------------------------------------------+ 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 use Tie::Array; 5 6 my @a = (1,2,3); 7 print '1 - ' . join(', ',@a) . "\n"; 8 tie @a, 'Tie::StdArray'; 9 print '2 - ' . join(', ',@a) . "\n"; 10 untie @a; 11 print '3 - ' . join(', ',@a) . "\n"; +----------------------+ | Résultat | +----------------------+ 1 - 1, 2, 3 2 - 3 - 1, 2, 3 +----------------------+ | Explications | +----------------------+ Les modules Tie de base ne conservent pas les valeurs d'origines de la variable. Ce qui implique que pour nos besoins, il faudrait redéfinir le contenu des variables à bien des endroits, après avoir executé le tie. Il faudrait convertir ceci : my @a = (1,2,3); tie @a, 'Tie::StdArray'; en ceci : my @a; tie @a, 'Tie::StdArray'; @a = (1,2,3); Mais encore une fois, c'est une mauvaise idée parce que notre objectif est de monitorer des centaines de variables, sans avoir à penser à comment les monitorer. +---------------------------------------------------------------------------------------------+ | EOF : Preuve 0x0001 : Les modules Tie sont cons | +---------------------------------------------------------------------------------------------+ +---------------------------------------------------------------------------------------------+ | Chapitre trois | | | | Ce que nous voulons | +---------------------------------------------------------------------------------------------+ Nous voulons une fonction qui va monitorer n'importe quoi, et ce, de manière générique. Bref, après chaque déclaration, nous voulons pouvoir assigner un tie sur la variable, sans pour autant perdre le contenu actuel. L'idéal serait d'overrider la méthode "my" (Ça se fait parce que j'ai vu le module Devel::Leak::Object qui override la méthode "bless"). Le problème est qu'il est impossible (Prouvez moi le contraire), d'obtenir le package (__PACKAGE__) et la ligne (__LINE__) de la déclaration à partir de l'endroit où la méthode "my" serait overridée. Donc, cette méthode pourrait probablement marcher, mais je n'aurais pas de nom unique pour chaque variable. Une autre méthode encore plus idéale, serait d'avoir accès à la liste de toutes les variables instanciées à un endroit précis (comme à la dernière ligne du programme). Toutefois, je n'ai rien trouvé permettant cela (Dites-le moi si vous savez, et ce dans les plus brefs délais). Tout ce qui est possible est de fouiller dans la table des symboles globaux, ce qui n'est pas d'une grande utilité. Il semble même impossible, au premier regard, de le faire avec Perl XS. C'est pour cela que nous utiliserons la méthode du "tie" sans utiliser les modules Tie par défaut, mais nous allons nous en inspirer. +---------------------------------------------------------------------------------------------+ | Chapitre quatre | | | | Devel::Monitor | +---------------------------------------------------------------------------------------------+ Regardez sur http://search.cpan.org pour Devel::Monitor Vous allez tomber sur quelque chose comme : http://search.cpan.org/~phcote/Devel-Monitor-0.9.0.4/lib/Devel/Monitor.pm Vous pouvez l'installer avec la commande "cpan -i Devel::Monitor" ou avec ppm sur windows... Bla. +---------------------------------------------------------------------------------------------+ | Chapitre cinq | | | | Utilisation de Devel::Monitor | +---------------------------------------------------------------------------------------------+ Bon, voici quelques exemples d'utilisation +---------------------------------------------------------------------------------------------+ | Exemple 0x0004 : Utilisation de "monitor" | +---------------------------------------------------------------------------------------------+ { my @a; monitor('a' => \@a); $a[0] = \@a; #Add a circular reference print STDERR "Leaving scope\n"; } print STDERR "Scope left\n"; +----------------------+ | Résultat | +----------------------+ MONITOR ARRAY a Leaving scope Scope left DESTROY ARRAY a +----------------------+ | Explications | +----------------------+ La ligne "DESTROY ARRAY a" devrait être entre les deux prints de scope. Ce qui veut dire que @a est deleté à la fin du programme. Si nous exécutons ce code avec mod_perl, la dernière ligne ne sera pas imprimée. +---------------------------------------------------------------------------------------------+ | EOF : Exemple 0x0004 : Utilisation de "monitor" | +---------------------------------------------------------------------------------------------+ +---------------------------------------------------------------------------------------------+ | Exemple 0x0005 : Utilisation de "monitor" | +---------------------------------------------------------------------------------------------+ { my @list = (1,2,3); print STDERR join(", ",@list)."\n"; for my $item (@list) { monitor("item $item" => \$item); $item+=1000; print "$item\n"; } print STDERR join(", ",@list)."\n"; print "Leaving scope\n"; } print "Scope left\n"; +----------------------+ | Résultat que vous | | pourriez escompter | +----------------------+ 1, 2, 3 MONITOR SCALAR : item 1 1001 DESTROY SCALAR : item 1 MONITOR SCALAR : item 2 1002 DESTROY SCALAR : item 2 MONITOR SCALAR : item 3 1003 DESTROY SCALAR : item 3 1, 2, 3 Leaving scope Scope left +----------------------+ | Résultat que vous | | aurez | +----------------------+ 1, 2, 3 MONITOR SCALAR : item 1 1001 MONITOR SCALAR : item 2 1002 MONITOR SCALAR : item 3 1003 1001, 1002, 1003 Leaving scope DESTROY SCALAR : item 3 DESTROY SCALAR : item 2 DESTROY SCALAR : item 1 Scope left +----------------------+ | Explications | +----------------------+ Perl passe la variables par référence dans les "for" et "foreach", donc vous utilisez les références originales. Donc, si vous modifiez le contenu de $item, le array @list est également modifié. Ceci explique également le fait que "item 1", "item 2" et "item 3" sont détruites uniquement lorsque @list est détruite. +---------------------------------------------------------------------------------------------+ | EOF : Exemple 0x0005 : Utilisation de "monitor" | +---------------------------------------------------------------------------------------------+ +---------------------------------------------------------------------------------------------+ | Chapitre six | | | | Quelques problèmes avec les constantes | +---------------------------------------------------------------------------------------------+ Vous remarquerez après usage que les constantes déclarées avec "use constant" sont en fait des symboles globaux. Voilà des équivalents : use constant CONST => [1,2]; sub CONST() { [1,2] }; sub CONST { [1,2] }; *CONST = sub () { [1,2] }; #Déclaré comme le fait constant.pm *main::CONST = sub () { [1,2] }; #Ne fonctionne pas avec mod_perl Lorsque perl compile, il s'aperçoit que la constante est inline, donc il la remplace par sa véritable valeur. Ce code : 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 use Tie::Array; 5 use constant CONST => [1,2]; 6 tie CONST(), 'Tie::StdArray'; est convertit part : 1 use Tie::Array; 2 use constant ('CONST', [1, 2]); 3 BEGIN {${^WARNING_BITS} = "UUUUUUUUUUUU"} 4 use strict 'refs'; 5 tie [1, 2], 'Tie::StdArray'; visualisable avec la commande "perl -MO=Deparse test.pl" Nous essayons donc de tie-er [1, 2], ce qui est impossible car c'est une constante tel que nous le dira perl. Nous devons tie-er la valeur retournée par la fonction comme ceci 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 use Tie::Array; 5 use constant CONST => [1,2]; 6 tie @{&CONST()}, 'Tie::StdArray'; L'équivalent avec le module monitor se fait ainsi : monitor('my constant value' => \&CONST); Également, puisqu'une constante est globale et reste en mémoire le temps que le module est en mémoire, notre monitor va nous indiquer que la variable est vivante jusqu'à la fin de notre programme. Il faut donc être en mesure d'indiquer que la variable monitorée est une constante. Regardons le cas suivant : 1 use constant CONST => [1,2]; 2 { 3 my $list = CONST(); 4 monitor('the list' => \$list); 5 } 6 print "Last program line\n"; Ceci indiquera que $list n'est pas détruite avant la fin du programme parce qu'elle contient la même référence que CONST. C'est pour cela qu'il faut monitorer les constantes à leur création, pour que par la suite, on ne puisse pas monitorer les variables qui utilise les constantes. Nous ferons comme ceci : 1 use constant CONST => [1,2]; 2 monitor('my constant value' => \&CONST); 3 { 4 my $list = CONST(); 5 monitor('the list' => \$list); 6 } 7 print "Last program line\n"; Le deuxième monitor indiquera qu'il n'est pas possible de monitorer la variable parce qu'elle est déja monitorée par "my constant value". De ce point, nos prints ne parleront pas de memory leak... puisque nous verrons immédiatement que c'est une constante qui n'est pas supprimée (ce qui est normal). L'autre problème relié aux constante serait si j'exécute ceci : 1 use constant CONST => [1,2]; 2 monitor('my constant value' => \&CONST()); 3 { 4 my $list = CONST()->[0]; 5 monitor('the list' => \$list); 6 } 7 print "Last program line\n"; J'obtiens le même problème que tantôt parce que CONST()->[0] n'est pas déjà monitoré. Pour palier à ces "problèmes", la méthode monitor va tie-er récursivement les constantes, à savoir tous les index des arrays, et toutes les clés des hashs, etc. +---------------------------------------------------------------------------------------------+ | Chapitre sept | | | | Outil d'insertion de moniteurs | +---------------------------------------------------------------------------------------------+ Si vous avez par exemple 100 fichiers à monitorer avec 100000 variables, vous aurez besoin d'un script qui va insérer un "monitor" sur chaque variable, pour vous éviter cette dure labeur. Codez vous un SetMonitor.pl qui va parser le code source et insérer des choses comme: monitor('mona::CGI::SiteMap::$self at line 32' => \$self); Je ne vous donnerai pas ce code source à moins que la demande se fasse vraiment sentir, parce qu'il est vraiment trop laid. Disons que je l'ai fait assez vite. Faire un parser improvisé en deux/trois jours, c'est bon... mais patché... Je conseille également d'exécuter un perltidy avant l'usage de ce genre de script. +---------------------------------------------------------------------------------------------+ | Chapitre huit | | | | Outil de visualisation d'erreurs | +---------------------------------------------------------------------------------------------+ Une fois les monitors implantés, vous roulez votre application, qui va printer des choses dans le STDERR, qui est /var/log/apache2/error_log dans mon cas. Une page me génère jusqu'à 1500000 (Oui, 1500000) lignes de message pour une page d'informations sur le gène Casp10 de la famille Caspase. Donc, j'ai besoin d'un outil pour me résumer le memory leak que je cherche. Voilà cet outil. NOTE : Vous devez exécuter votre application depuis mod_perl (ou d'un endroit où le process ne se termine pas...), autrement, la mémoire sera libérée. +---------------------------------------------------------------------------------------------+ | VerifyMonitor.pl | +---------------------------------------------------------------------------------------------+ #!/usr/bin/perl ##Philippe Cote, 2005 ##DOC =head1 NAME VerifyMonitor =head1 DESCRIPTION See perl VerifyMonitor -h for help =cut use strict; use warnings; use File::Find; #CONSTANTS -------------------------------------------------------------------- use constant RED => "\e[0;31m"; use constant NORMAL => "\e[0m"; sub printRed { my $msg = shift; if ($^O !~ /win32|dos/i) { print RED . $msg . NORMAL . "\n"; } else { print "$msg\n"; } } #HELP ------------------------------------------------------------------------- if ((scalar(@ARGV) < 3) || ($ARGV[0] =~ /(-h)|(--help)/)) { print <<"EOT"; Usage : perl VerifyMonitor.pl file beginTag endTag Add a print at the begin of your program print STDERR ":::PROGRAM START\n"; and at the end. print STDERR ":::PROGRAM END\n"; Run your program with some monitors It will print stuff to the error file you choose Now verify with this command perl VerifyMonitor.pl /var/log/apache/error_log ':::PROGRAM START' ':::PROGRAM END' EOT exit(0); } #------------------------------------------------------------------------------ _verifyMonitor($ARGV[0],$ARGV[1],$ARGV[2]); sub _verifyMonitor { my $filename = shift; my $beginTag = shift; my $endTag = shift; open(my $inHandle,$filename) || die("Can't open INFILE: $!"); my $lineNum = 0; my $lineBegin; my $lineEnd; my $mem = {}; #Every monitored variables while (my $line = <$inHandle>) { $lineNum++; if ($line =~ /$beginTag/) { $lineBegin = $lineNum; $mem = {}; } if ($line =~ /$endTag/) { $lineEnd = $lineNum; print "Results from line $lineBegin to $lineEnd ------------------------------------------------\n"; _printMemoryLeak($mem); } if ($line =~ /MONITOR CODE (HASH|ARRAY|SCALAR) : (.*)/) { #Nothing to do } elsif ($line =~ /MONITOR (HASH|ARRAY|SCALAR) : (.*)/) { if (exists($mem->{$2})) { $mem->{$2}++; } else { $mem->{$2} = 1; } } elsif ($line =~ /DESTROY CODE (HASH|ARRAY|SCALAR) : (.*)/) { #Nothing to do } elsif ($line =~ /DESTROY (HASH|ARRAY|SCALAR) : (.*)/) { if (exists($mem->{$2})) { if ($mem->{$2} > 1) { $mem->{$2}--; } else { delete($mem->{$2}) } } else { print "DESTROY $1 : $2 were found, but not monitored\n"; } } } close($inHandle); } sub _printMemoryLeak { my $memRef = shift; my $i = 0; foreach my $key (keys %$memRef) { print "$i - $key were not destroyed(".$memRef->{$key}.")\n"; $i++; } print "Nothing found\n" if $i == 0; } +---------------------------------------------------------------------------------------------+ | EOF : VerifyMonitor.pl | +---------------------------------------------------------------------------------------------+ Ceci imprimera vos variables n'ayant pas été détruites. N'oubliez pas de lire le help de VerifyMonitor.pl... +---------------------------------------------------------------------------------------------+ | Chapitre neuf | | | | Comment régler les références circulaires | +---------------------------------------------------------------------------------------------+ Disons que nous avons ce petit code "très con et illogique" orienté objet : 1 #!/usr/bin/perl 2 3 #-------------------------------------------------------------------- 4 # Little program 5 #-------------------------------------------------------------------- 6 7 use strict; 8 use warnings; 9 use Devel::Monitor qw(:all); 10 11 { 12 my $a = ClassA->new(); 13 my $b = $a->getClassB(); 14 monitor('$b' => \$b); 15 $b->getClassA()->printSomething(); 16 print "Leaving scope\n"; 17 } 18 print "Scope left\n"; 19 20 #-------------------------------------------------------------------- 21 # ClassA (Just a class with the "printSomething" method) 22 #-------------------------------------------------------------------- 23 24 package ClassA; 25 use strict; 26 use warnings; 27 use Scalar::Util qw(weaken isweak); 28 29 sub new { 30 my ($class) = @_; 31 my $self = {}; 32 bless($self => $class); 33 return $self; 34 } 35 36 sub getClassB { 37 my $self = shift; 38 $self->{_classB} = ClassB->new($self); 39 return $self->{_classB}; 40 } 41 42 sub printSomething { 43 print "Something\n"; 44 } 45 46 #-------------------------------------------------------------------- 47 # ClassB (A class that got a "parent" which is a ClassA instance) 48 #-------------------------------------------------------------------- 49 50 package ClassB; 51 use strict; 52 use warnings; 53 use Scalar::Util qw(weaken isweak); 54 55 sub new { 56 my ($class, $classA) = @_; 57 my $self = {}; 58 bless($self => $class); 59 $self->setClassA($classA); 60 return $self; 61 } 62 63 sub setClassA { 64 my ($self, $classA) = @_; 65 $self->{_classA} = $classA; 66 } 67 68 sub getClassA { 69 return shift->{_classA}; 70 } 71 72 1; +----------------------+ | Résultat | +----------------------+ MONITOR HASH : $b Something Leaving scope Scope left DESTROY HASH : $b +----------------------+ | Explications | +----------------------+ On voit que l'objet référencé par $b n'est pas détruit lorsqu'il sort du scope parce que $a->{_classB} le référence. Nous avons une référence circulaire. Nous pouvons alors utiliser deux méthodes. - La méthode manuelle qui consiste à détruite avec un delete ou un undef, la référence circulaire, lorsque nous n'avons plus besoin de l'objet. - La méthode plus cool, qui consiste à déclarer "weaken " une des deux références. Si A pointe vers B et B vers A, si je weaken A, lorsque le garbage collector vérifiera les variables, il verra deux références sur A, l'originale qui est weaken, et celle de B vers A. Il considère donc cela comme une seule référence, rendant possible la suppression de A, qui rendra ensuite possible la suppression de B. Le weaken sert à dire "oui j'ai une référence ici, mais fait comme si j'existais pas". On se rend compte qu'on ne peut pas mettre des weaken partout comme l'on veut parce que nos variables vont se supprimer d'elle-mêmes... +---------------------------------------------------------------------------------------------+ | Mauvaise façon de briser la référence circulaire | +---------------------------------------------------------------------------------------------+ sub getClassB { my $self = shift; $self->{_classB} = ClassB->new($self); #$self->{_classB} est la seule #référence à l'objet weaken($self->{_classB}); #on weaken la seule référence #$self->{_classB} est détruite ici print "\$self->{_classB} is now weaken\n" if isweak($self->{_classB}); return $self->{_classB}; #On retourne undef } +---------------------------------------------------------------------------------------------+ | Bonne façon de briser la référence circulaire | +---------------------------------------------------------------------------------------------+ sub getClassB { my $self = shift; my $b = ClassB->new($self); $self->{_classB} = $b; #On crée une seconde référence weaken($self->{_classB}); #On crée un weaken print "\$self->{_classB} is now weaken\n" if isweak($self->{_classB}); return $self->{_classB}; #On retourne l'objet, qui #logiquement sera référencé #par une variable de l'appelant } #$b est détruit ici, il nous reste donc la variable de l'appelant et la #variable weaken. La variable de l'appelant pourra cependant être détruite #parce que la référence circulaire est weaken d'un côté. +---------------------------------------------------------------------------------------------+ | Faire attention !!! Ceci ne fonctionne pas | +---------------------------------------------------------------------------------------------+ sub getClassB { my $self = shift; { my $b = ClassB->new($self); $self->{_classB} = $b; #On crée une seconde référence weaken($self->{_classB}); #On weaken cette référence print "\$self->{_classB} is now weaken\n" if isweak($self->{_classB}); } #$b est détruite ici, et $self->{_classB} aussi parce qu'elle est weaken #L'instance "ClassB" est détruite $self->{_classB} égale undef return $self->{_classB}; } +---------------------------------------------------------------------------------------------+ | Bonne façon | +---------------------------------------------------------------------------------------------+ sub getClassB { my $self = shift; my $b; { $b = ClassB->new($self); $self->{_classB} = $b; #On crée la seconde référence weaken($self->{_classB}); #On weaken la référence print "\$self->{_classB} is now weaken\n" if isweak($self->{_classB}); } #$b n'est pas encore détruite, donc on ne perd pas notre référence à #notre objet return $self->{_classB}; #On retourne l'objet qui sera pris en charge #par le module appelant. } +---------------------------------------------------------------------------------------------+ | Conclusion | +---------------------------------------------------------------------------------------------+ Il faut toujours s'assurer de conserver une référence "non-weak" sur notre objet Le output final sera : $self->{_classB} is now weaken MONITOR HASH : $b Something Leaving scope DESTROY HASH : $b Scope left Nous n'avons plus de références circulaires +---------------------------------------------------------------------------------------------+ | Il faut toujours weaken-er la référence de l'appelant, autrement | | on peut rendre les modules inutilisables | +---------------------------------------------------------------------------------------------+ Soit le code suivant avec les package de tantôt : 1 my $b; 2 { 3 my $a = ClassA->new(); 4 monitor('$a' => \$a); 5 $b = ClassB->new($a); 6 $b->getClassA()->printSomething(); 7 print "Leaving scope\n"; 8 } 9 print "Scope left\n"; 10 $b->getClassA()->printSomething(); avec le weaken du côté de l'appelé 1 sub setClassA { 2 my ($self, $classA) = @_; 3 $self->{_classA} = $classA; 4 weaken($self->{_classA}); 5 print "\$self->{_classA} is now weaken\n" if isweak($self->{_classA}); 6 } On obtient le output suivant avec l'erreur à la fin MONITOR HASH : $a $self->{_classA} is now weaken Something Leaving scope DESTROY HASH : $a Scope left Can't call method "printSomething" on an undefined value at test.pl line 29. $a est détruite à la fin du scope, et l'autre référence à la variable est weaken, donc elle est détruite aussi. Ceci prouvant qu'il faut weaken-er le côté de l'appelant parce que l'appelant ne mémorise pas nécessairement la référence. +---------------------------------------------------------------------------------------------+ | Chapitre dix | | | | Impossible de weaken-er un objet tied | +---------------------------------------------------------------------------------------------+ Il est impossible de weaken-er un objet tied. L'appel de la méthode ne fait tout simplement rien. +---------------------------------------------------------------------------------------------+ | Preuve 0x0002 : Test de base | +---------------------------------------------------------------------------------------------+ 1 use Devel::Monitor qw(:all); 2 use Scalar::Util qw(weaken isweak); 3 my (@a, @b); 4 tie @a, 'Devel::Monitor::TestArray'; 5 tie @b, 'Devel::Monitor::TestArray'; 6 $a[0] = \@b; 7 $b[0] = \@a; 8 weaken($b[0]); 9 if (isweak($a[0])) { 10 print "\$a[0] is weak\n"; 11 } else { 12 print "\$a[0] is not weak\n"; 13 } 14 if (isweak($b[0])) { 15 print "\$b[0] is weak\n"; 16 } else { 17 print "\$b[0] is not weak\n"; 18 } Le output sera : $a[0] is not weak $b[0] is not weak Toutefois, si nous enlevons les deux "tie", le output sera : $a[0] is not weak $b[0] is weak +---------------------------------------------------------------------------------------------+ | EOF : Preuve 0x0002 : Test de base | +---------------------------------------------------------------------------------------------+ +---------------------------------------------------------------------------------------------+ | Preuve 0x0003 : mod_perl | +---------------------------------------------------------------------------------------------+ Soit le petit programme suivant : +----------------------+ | test.pl | +----------------------+ 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 use Scalar::Util qw(weaken); 5 use Devel::Monitor qw(:all); 6 use Util::Junk; 7 8 my (@a, $b); 9 #tie @a, 'Devel::Monitor::TestArray'; 10 $a[0] = \$b; 11 $b = \@a; 12 $a[1] = Util::Junk::_20M(); 13 weaken($a[0]); +----------------------+ | EOF : test.pl | +----------------------+ +----------------------+ | Util::Junk | +----------------------+ 1 package Util::Junk; 2 use strict; 3 use warnings; 4 5 sub _20M() { 'A 20 megs string here filled with zeros' } 6 7 1; +----------------------+ | EOF : Util::Junk | +----------------------+ +----------------------+ | wget-test.pl | +----------------------+ 1 #!/usr/bin/perl 2 3 use strict; 4 use warnings; 5 6 my $baseUrl = 'http://localhost/perl/test.pl'; 7 8 my $i = 0; 9 while (1) { 10 print "Loop ".++$i."\n"; 11 12 system('wget "'.$baseUrl.'" -O /dev/null') == 0 13 or die "\nwget failed or has been interrupted : $?\n"; 14 } +----------------------+ | EOF : wget-test.pl | +----------------------+ Maintenant que nous avons un programme et un caller (ainsi que mod_perl), nous pouvons démarrer le programme. Notez que le url est hardcodé dans le programme de test. perl wget-test.pl Lorsque @a n'est pas tied (Voir la ligne 9 commentée), après environ 10 chargement de la page, les process de Apache contiennent tous la page et le chargement devient TRÈS rapide. Vous noterez également que la mémoire devient stable. Toutefois, si la ligne 9 n'est plus commentée, la mémoire va rapidement se remplir et chaque chargement de la page aura un temps constant identique aux temps de départ du programme. Nous voyons facilement que le weaken ne se fait pas. +---------------------------------------------------------------------------------------------+ | EOF : Preuve 0x0003 : mod_perl | +---------------------------------------------------------------------------------------------+ +---------------------------------------------------------------------------------------------+ | Preuve 0x0004 : Assaut final | +---------------------------------------------------------------------------------------------+ Tout d'abord, il faut s'assurer que les méthodes Scalar::Util::weaken et Scalar::Util::isweak ne contiennent pas de bugs. Si nous les regardons de plus près, nous voyons qu'elles ne font simplement qu'appeler les méthodes XS suivantes : 1 void 2 weaken(sv) 3 SV *sv 4 PROTOTYPE: $ 5 CODE: 6 #ifdef SvWEAKREF 7 sv_rvweaken(sv); 8 #else 9 croak("weak references are not implemented in this release of perl"); 10 #endif 11 12 void 13 isweak(sv) 14 SV *sv 15 PROTOTYPE: $ 16 CODE: 17 #ifdef SvWEAKREF 18 ST(0) = boolSV(SvROK(sv) && SvWEAKREF(sv)); 19 XSRETURN(1); 20 #else 21 croak("weak references are not implemented in this release of perl"); 22 #endif Nous voyons que ce code est très simple et ne fait qu'appeler des méthodes de l'API de Perl. Donc, aucun gros traitements ici et donc aucune possibilité de bugs dans ces méthodes. Il existe un outil nommé Devel::Peek qui permet de dumper le contenu d'une variable dans un format de développeur XS. XS est le langage ou plutôt l'extension de C++ pour créer des modules Perl en C++, en utilisant l'API de Perl. (Voir perlapi, perlguts et tout autre documentation sur le coeur de Perl) Si vous connaissez l'API de perl, vous saurez sans doute que le flag WEAKREF est activé lorsqu'une variable est weaken et ceci est visualisable avec un dump. Note : Devel::Peek::Dump ne fait qu'appeler sv_dump de l'API Perl. Regardons un exemple de résultat que nous devrions avoir : +----------------------+ | Code | +----------------------+ 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 use Devel::Monitor qw(:all); 5 6 use Scalar::Util qw(weaken); 7 use Devel::Peek; 8 { 9 my (@a); 10 $a[0] = \@a; 11 #tie @a, 'TestArray'; 12 Dump($a[0],1); 13 weaken($a[0]); 14 Dump($a[0],1); 15 print "Leaving scope\n"; 16 } 17 print "Scope left\n"; 18 19 package TestArray; 20 use Tie::Array; 21 use base 'Tie::StdArray'; 22 23 sub DESTROY { print "Monitor::TestArray::DESTROY : $_[0]\n"; } 24 25 1; +----------------------+ | Résultat sans le tie | +----------------------+ SV = RV(0x81829c0) at 0x814127c REFCNT = 1 FLAGS = (ROK) RV = 0x814e740 SV = PVAV(0x81426cc) at 0x814e740 REFCNT = 2 FLAGS = (PADBUSY,PADMY) IV = 0 NV = 0 ARRAY = 0x8148888 FILL = 0 MAX = 3 ARYLEN = 0x0 FLAGS = (REAL) SV = RV(0x81829c0) at 0x814127c REFCNT = 1 FLAGS = (ROK,WEAKREF,IsUV) RV = 0x814e740 SV = PVAV(0x81426cc) at 0x814e740 REFCNT = 1 FLAGS = (PADBUSY,PADMY,RMG) IV = 0 NV = 0 MAGIC = 0x8266f08 MG_VIRTUAL = &PL_vtbl_backref MG_TYPE = PERL_MAGIC_backref(<) MG_FLAGS = 0x02 REFCOUNTED MG_OBJ = 0x81411c8 SV = PVAV(0x8263704) at 0x81411c8 REFCNT = 2 FLAGS = () IV = 0 NV = 0 ARRAY = 0x82677e8 FILL = 0 MAX = 3 ARYLEN = 0x0 FLAGS = (REAL) ARRAY = 0x8148888 FILL = 0 MAX = 3 ARYLEN = 0x0 FLAGS = (REAL) Leaving scope Scope left +----------------------+ | Explications | +----------------------+ Ce qui nous concerne le plus ici est le flag WEAKREF qui nous confirme que la référence (RV) est bel et bien weaken. Cependant, regardons ce qui se passe si l'on active la ligne 11 de notre code étant le tie de la variable @a +----------------------+ | Résultat avec le tie | +----------------------+ SV = PVLV(0x817c568) at 0x81413f0 REFCNT = 1 FLAGS = (TEMP,GMG,SMG,RMG) IV = 0 NV = 0 PV = 0 MAGIC = 0x81505b8 MG_VIRTUAL = &PL_vtbl_packelem MG_TYPE = PERL_MAGIC_tiedelem(p) MG_FLAGS = 0x02 REFCOUNTED MG_OBJ = 0x814139c SV = RV(0x81829ac) at 0x814139c REFCNT = 2 FLAGS = (ROK) RV = 0x8141354 TYPE = t TARGOFF = 0 TARGLEN = 0 TARG = 0x81413f0 SV = PVLV(0x817c568) at 0x81413f0 REFCNT = 1 FLAGS = (TEMP,GMG,SMG,RMG) IV = 0 NV = 0 PV = 0 MAGIC = 0x81505b8 MG_VIRTUAL = &PL_vtbl_packelem MG_TYPE = PERL_MAGIC_tiedelem(p) MG_FLAGS = 0x02 REFCOUNTED MG_OBJ = 0x814139c SV = RV(0x81829ac) at 0x814139c REFCNT = 2 FLAGS = (ROK) RV = 0x8141354 TYPE = t TARGOFF = 0 TARGLEN = 0 TARG = 0x81413f0 Leaving scope Scope left Monitor::TestArray::DESTROY : TestArray=ARRAY(0x8141354) +----------------------+ | Explications | +----------------------+ On voit que rien n'a changé avant et après l'exécution du weaken sur la variable tied. Suite à plusieurs tests, j'ai découvert qu'une variable tied est de type lvalue (SvTYPE(sv) == SVt_PVLV). Je me suis également aperçu que la méthode Data::Structure::Util::has_circular_ref ne gère pas ce type de valeur. Il semble également que ce type n'est pas si vieux que ça. Je suis donc allé voir dans le code source de Perl pour trouver la méthode sv_rvweaken qui fait défaut. Selon la définition dans embed.h, cette méthode réfère à la méthode Perl_sv_rvweaken suivante : 1 /* 2 =for apidoc sv_rvweaken 3 4 Weaken a reference: set the C flag on this RV; give the 5 referred-to SV C magic if it hasn't already; and 6 push a back-reference to this RV onto the array of backreferences 7 associated with that magic. 9 10 =cut 11 */ 12 13 SV * 14 Perl_sv_rvweaken(pTHX_ SV *sv) 15 { 16 SV *tsv; 17 if (!SvOK(sv)) /* let undefs pass */ 18 return sv; 19 if (!SvROK(sv)) 20 Perl_croak(aTHX_ "Can't weaken a nonreference"); 21 else if (SvWEAKREF(sv)) { 22 if (ckWARN(WARN_MISC)) 23 Perl_warner(aTHX_ packWARN(WARN_MISC), "Reference is already weak"); 24 return sv; 25 } 26 tsv = SvRV(sv); 27 sv_add_backref(tsv, sv); 28 SvWEAKREF_on(sv); 29 SvREFCNT_dec(tsv); 30 return sv; 31 } En débugguant ce code, on s'aperçoit que la variable tied passe à la ligne 17, puis entre dans la condition pour finalement exécuter la ligne 18. La raison est que notre variable tied a les flags suivants : FLAGS = (TEMP,GMG,SMG,RMG). (Voir http://gisle.aas.no/perl/illguts/ pour des informations additionnelles à perlguts) Donc, comme le dit le code, la condition 17 devrait être vraie lorsque la variable est non définie, nous indiquant également - sans le mentionner - que les variables tied ne sont pas gérées par cette méthode. Il faudrait remplacer la ligne 18 par quelchose comme : if (SvMAGIC(sv)) { //*************************************** //Do something like this without bugs !!! //*************************************** //tsv = SvRV(sv); //sv_add_backref(tsv, sv); //SvWEAKREF_on(sv); //SvREFCNT_dec(tsv); //*************************************** } else { return sv; } Toutefois, je n'arrive pas à faire fonctionner ce genre de code. J'ai des problèmes avec le backref. Il semble me manquer des informations primordiales. +---------------------------------------------------------------------------------------------+ | EOF : Preuve 0x0004 : Assaut final | +---------------------------------------------------------------------------------------------+ Ce bug a été soumis à perlbug http://rt.perl.org/rt3/Ticket/Display.html?id=34524 et n'a pas été répondu à ce jour. Nous arrivons à l'immense problème où nous ne pouvons pas visualiser à tout coup que nos variables sont véritablement supprimés comme il se doit, uniquement parce que le weaken ne veut pas fonctionner avec un tie sur la variable. Pour vraiment savoir si le weaken se fait bien, nous devons à ce jour, enlever les monitors, et vérifier sur les variables suspectes s'il y a des références circulaires avec Devel::Monitor::print_circular_ref. +---------------------------------------------------------------------------------------------+ | Chapitre onze | | | | Utilisation de Devel::Monitor | +---------------------------------------------------------------------------------------------+ Lorsque vous avez détectez des variables non détruites, vous pouvez allez plus en profondeur pour savoir où se situe la référence circulaire en utilisant la méthode Devel::Monitor::print_circular_ref +---------------------------------------------------------------------------------------------+ | Exemple 0x0006 : Utilisation de "print_circular_ref" | +---------------------------------------------------------------------------------------------+ 1 my (@a, @b); 2 $a[0] = 'asdf'; 3 $a[1] = \@b; 4 $b[3] = \@b; 5 print_circular_ref(\@a); 6 print_circular_ref(\@b); +----------------------+ | Résultat | +----------------------+ ------------------------------------------------------------------------------- Checking circular references for ARRAY(0x814e358) ------------------------------------------------------------------------------- Internal circular reference found : ARRAY(0x814e358)[1][3] on ARRAY(0x814e370) 1 - Item : ARRAY(0x814e358) 2 - Source : [1] Item : ARRAY(0x814e370) 3 - Source : [3] Item : ARRAY(0x814e370) ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- Results for ARRAY(0x814e358) Circular reference : 0 Internal circular reference : 1 Weak circular reference : 0 ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- Checking circular references for ARRAY(0x814e370) ------------------------------------------------------------------------------- Circular reference found : ARRAY(0x814e370)[3] 1 - Item : ARRAY(0x814e370) 2 - Source : [3] Item : ARRAY(0x814e370) ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- Results for ARRAY(0x814e370) Circular reference : 1 Internal circular reference : 0 Weak circular reference : 0 ------------------------------------------------------------------------------- +----------------------+ | Conclusion | +----------------------+ Vous pouvez savoir où se situe les références circulaires et également savoir si elle est déjà weakened. +---------------------------------------------------------------------------------------------+ | EOF : Exemple 0x0006 : Utilisation de "print_circular_ref" | +---------------------------------------------------------------------------------------------+ +---------------------------------------------------------------------------------------------+ | Chapitre douze | | | | Impossible d'utiliser les références des objets tied | +---------------------------------------------------------------------------------------------+ Évidemment, encore des problèmes surviennent lorsque vient le temps de vérifier les références d'un objet tied. L'usage de ces références pourraient servir par exemple à vérifier les références circulaires en observant si une référence est vu plus d'une fois dans un chemin. Élas, il semble impossible de le faire. Soit le code suivant : 1 my $self = {'a' => 1, 2 'b' => 2}; 3 monitor('self' => \$self); 4 print STDERR \($self->{'a'})."\n"; 5 print STDERR \($self->{'b'})."\n"; 6 print STDERR \($self->{'a'}).\($self->{'b'})."\n"; 7 foreach my $key (keys %$self) { 8 my $keyRef = \$key; 9 my $value = $self->{$key}; 10 my $valueRef = \($self->{$key}); 11 print STDERR "KEY:$key, KEY REF:$keyRef, VALUE:$value, VALUE REF:$valueRef\n"; 12 } +----------------------+ | Résultat | +----------------------+ MONITOR HASH : self SCALAR(0x8141384) SCALAR(0x8141384) SCALAR(0x8141384)SCALAR(0x81413cc) KEY:a, KEY REF:SCALAR(0x8141420), VALUE:1, VALUE REF:SCALAR(0x824becc) KEY:b, KEY REF:SCALAR(0x81413cc), VALUE:2, VALUE REF:SCALAR(0x824becc) DESTROY HASH : self +----------------------+ | Explications | +----------------------+ Nous voyons que les références sont les mêmes pour des clés de hash différentes. Il semble que le tie me fait chier !!! Mais attendez une minute, est-ce que c'est mon module qui bug ? Essayons avec du code plus élémentaire : 1 #!/usr/bin/perl 2 use strict; 3 use warnings; 4 use Tie::Hash; 5 6 my %self; 7 tie %self, 'Tie::StdHash'; 8 $self{a} = 1; 9 $self{b} = 2; 10 print STDERR \($self{a})."\n"; 11 print STDERR \($self{b})."\n"; 12 print STDERR \($self{a}).\($self{b})."\n"; 13 foreach my $key (keys %self) { 14 my $keyRef = \$key; 15 my $value = $self{$key}; 16 my $valueRef = \($self{$key}); 17 print STDERR "KEY:$key, KEY REF:$keyRef, VALUE:$value, VALUE REF:$valueRef\n"; 18 } +----------------------+ | Résultat | +----------------------+ SCALAR(0x101112c8) SCALAR(0x101112c8) SCALAR(0x101112c8)SCALAR(0x1016cf8c) KEY:a, KEY REF:SCALAR(0x10133ca4), VALUE:1, VALUE REF:SCALAR(0x1016cfa4) KEY:b, KEY REF:SCALAR(0x1016cf8c), VALUE:2, VALUE REF:SCALAR(0x1016cfa4) +----------------------+ | Conclusion | +----------------------+ Les références d'objets tied sont mauvaises... On ne peut pas s'en servir pour détecter des références circulaires... Ceci est vraiment mauvais... +---------------------------------------------------------------------------------------------+ | Chapitre treize | | | | Conclusion | +---------------------------------------------------------------------------------------------+ Si weaken ne me faisait pas de troubles, ce serait vraiment mais vraiment bien. J'ai les mains liés et je ne peux rien faire d'autre que de visualiser les variables non détruites, puis de détecter les références circulaires pour ensuite mettre des weaken. Rendu à ce point, je ne peux plus rien faire d'autre que de m'embarquer dans la science de l'essai erreur à savoir si la variable est réellement détruite. Je dois à ce jour tester manuellement avec mod_perl. Il est réellement dommage, qu'après être allé aussi loin, que je me fasse arrêter par le simple weaken. Avoir été dans n'importe quel autre langage de haut niveau, je n'aurais tout simplement pas pu faire ce genre de chose. Avec Perl, les possibilités étaient quasi-infinies... Mais voilà qu'il resserre sa corde à mon cou tel un cowboy en furie. Pour réellement régler le problème, je devrais aller directement dans le code source de Perl, chose que je ne ferai sans doute pas faute de temps. warn "J'ai faim\n"; +---------------------------------------------------------------------------------------------+ | Chapitre quatorze | | | | Signature | +---------------------------------------------------------------------------------------------+ Amoureusement vôtre, nothing94 +---------------------------------------------------------------------------------------------+ | Chapitre quinze | | | | Post-scriptum | +---------------------------------------------------------------------------------------------+ Si vous trouvez des solutions aux problèmes énumérés dans ce texte, faites moi signe.