Doctrine Free (true) et la mémoire

17 janvier 2012 par: Benoit Bonneville

Doctrine 1.2, l’ORM de Symfony 1.4 peu facilement creer un Mémory Leak (fuite Mémoire). Un script PHP avec quelques milliers d’objets peu entrainer un PHP Fatal error: Allowed memory size of X bytes exhausted.

Nettoyer la mémoire avec free()

La documentation officielle de doctrine sur le nettoyage mémoire se trouve ici.

En simplifié, la méthode ->free() de votre objet Doctrine (un Doctrine_Record pour être exact) nettoie la mémoire occupé par cet objet.
Cela détruira (unset) tout les attributs de l’objet.

Les limite de free() sur la mémoire

Pour comprendre la suite vous devez savoir qu’un objet PHP ne sera nettoyé de la mémoire que si aucune autre « référence » ne pointe vers lui. C’est à dire qu’aucune variable dans votre code ne doit plus contenir cet objet.

C’est justement le cas si vous aviez une relation avec un autre objet.

Free() ne nettoiera pas l’objet lié, qui conserve un pointeur vers notre object que nous essayons de nettoyer.

Et donc la mémoire ne sera jamais libérée, même si le « free » aura diminué l’espace mémoire occupé par notre objet en détruisant ses attributs, il continue d’exister.

Le nettoyage de masse ou free(true);

Appeler ->free(true) sur votre objet, détruira tout les attributs comme free(), mais en plus si c’est un objet Doctrine il appellera aussi free sur cet objet.

La mémoire sera alors 100% nettoyée, mais cette méthode comporte des risques.

Les risques du free()

Utiliser un objet qui a été nettoyé sera une véritable catastrophe pour votre application.

L’objet aura perdu le type Doctrine_Record et sera devenu un stdObjet, c’est à dire un objet de base en PHP.
Mais il conserve toutes ses méthodes, et ses attributs sont vidées.
L’appel à ses méthode provoquera des erreurs fatal et imprévisibles

Exemple d’un « ->getId() »  vous aurez le droit au message  « id » undefined for « MyClass ».

Nettoyer les relations

Relations 1-1

Rien de plus facile, free(true) fera l’affaire.
Les objets étant simplement interdépendants, détruire l’un ou l’autre n’a pas d’importance, le paramètre « true » détruira l’autre.

Relation 1-N

Votre script devra se focaliser sur l’objet de la relation qui est le moins nombreux, le « 1 ».
Exemple pour une relation entre des Utilisateurs qui ont écris des commentaires :

Nous voulons mettre à jour la signature du commentaire par celle de l’utilisateur (qui est contenu dans l’objet utilisateur)

Dans ce cas notre script devra se faire comme ceci :

foreach($users as $user) {
    foreach($user->getComment() as $comment) {
        // Do some Huge job related to comment and user
        $comment->updateUserSignature();
    }
    $user->free(true);
}

Ici tout se passera bien pour votre mémoire.

Voici le mauvais exemple :

foreach($comments as $comment) {
    // Do some Huge job related to comment and user
    $comment->updateUserSignature();
    $comment->free(true);
}

Explication du Contre-Exemple :

  • Boucle Foreach, iteration 1 : Commentaire 1 de l’Utilisateur 1
  • Exécute updateUserSignature() sur Commentaire 1
  • Nettoyage de Commentaire 1 et Utilisateur 1
  • Boucle Foreach itération 2 : Commentaire 2 de l’Utilisateur 1
  • Execute sur updateUserSignature() sur Commentaire 2

=> Erreur Fatale, car au moment ou updateUserSignature() du commentaire 2 vas appeler son utilisateur, celui ci a été nettoyé juste avant… votre script PHP s’arretera.

Relation N-N

Cela dépendra de votre quantité de données de chaque coté de la N-N.
Imaginons le cas ou nous avons des Articles (Post) avec des mots clef (Tag)

Dans ce cas, boucler sur les Post ou les Tags, n’empêcherons pas le risque d’avoir un autre élément lié.
Donc l’appel de free(true) est extremement risqué.

A partir du moment ou une des relations contiens peu d’éléments, vous pouvez tout charger en mémoire.
Vous ferez le ménage pour chaque ensemble d’éléments traité  (un batch).

Exemple :  si vous n’avez que 1000 Tags pour 10 Millions de posts,
Alors vous pouvez ne travailler qu’avec des ensemble de ~10 000 Posts liés au 1000 Tags,
(10 000 Posts + 1000 Tags devraient rentrer dans votre mémoire.)
Vous les nettoierez tout après.

// $posts and Tags could fit in Memory (10 000 Posts records).
foreach($posts as $post) {
    $post->getTag();
    // Then do something with Tags

    // You can't free Tags here
    // because another post could need again one Tag
}
// Then clean everything
foreach($posts as $post) {
    $post->free(true);
}
// Then Load the next 10K records Post, with the Tags again

Si jamais vous avez  plus de Tags, à vous d’adapter le nombre d’éléments à traiter dans le batch.
Ici réglé à 10 000, vous pouvez le descendre jusqu’à 1 Post à la fois.
Plus vous diminuerez le nombre d’éléments à traiter, moins vos performances seront bonne.
(Plus de requette SQL pour charger les éléments et répétition du chargement chargement des éléments liés (ici Tag) qui devront être rechargée N fois.)

Si votre relation ne tiens pas dans un sens comme dans l’autre dans votre mémoire, à vous de découper en plusieurs requette SQL,
Mais si cela est votre cas, peu être qu’il faudrait envisager une gestion en tableau qui plus rapide qu’une gestion objet et plus simple à nettoyer de la mémoire.

 

 

 

Execute doJobWithUser()
Filed under: Développement

Répondre