62 votes

La méthode JavaScript setInterval() provoque-t-elle une fuite de mémoire ?

Je développe actuellement un projet d'animation basé sur JavaScript.

J'ai remarqué qu'une bonne utilisation de setInterval() , setTimeout() et même requestAnimationFrame alloue de la mémoire sans que je le demande, et provoque de fréquents appels au ramassage des ordures. Plus d'appels au GC = flickers :-(

Par exemple, lorsque j'exécute la commande suivante code simple en appelant init() dans Google Chrome, l'allocation de mémoire + garbage collection est correcte pendant les 20-30 premières secondes...

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    return true
}

D'une manière ou d'une autre, en l'espace d'une minute environ, commence une étrange augmentation de la mémoire allouée ! Puisque init() n'est appelé qu'une seule fois, Quelle est la raison de l'augmentation de la taille de la mémoire allouée ?

(Edit : chrome screenshot uploaded)

chrome screenshot

NOTE #1 : Oui, j'ai essayé d'appeler clearInterval() avant le prochain setInterval(). Le problème reste le même !

NOTE #2 : Afin d'isoler le problème, je garde le code ci-dessus simple et stupide.

52voto

luqmaan Points 720

EDITAR: Réponse de Yury est meilleur.


tl;dr IMO il n'y a pas de fuite de mémoire. La pente positive est simplement l'effet de setInterval et setTimeout. Les déchets sont collectés, comme le montrent les motifs en dents de scie, ce qui signifie par définition qu'il n'y a pas de fuite de mémoire. (Je pense).

Je ne suis pas sûr qu'il y ait un moyen de contourner cette soi-disant "fuite de mémoire". Dans ce cas, la "fuite de mémoire" se réfère à chaque appel à la fonction setInterval qui augmente l'utilisation de la mémoire, comme le montrent les pentes positives dans le profileur de mémoire.

En réalité, il n'y a pas de fuite de mémoire : le ramasse-miettes est toujours en mesure de collecter la mémoire. Par définition, une fuite de mémoire "se produit lorsqu'un programme informatique acquiert de la mémoire mais ne la restitue pas au système d'exploitation".

Comme le montrent les profils de mémoire ci-dessous, il n'y a pas de fuite de mémoire. L'utilisation de la mémoire augmente à chaque appel de fonction. Le PO s'attend à ce qu'il n'y ait pas d'augmentation de la mémoire parce qu'il s'agit de la même fonction qui est appelée à plusieurs reprises. Or, ce n'est pas le cas. La mémoire est consommée à chaque appel de fonction. Finalement, les déchets sont collectés, ce qui crée le schéma en dents de scie.

J'ai exploré plusieurs façons de réorganiser les intervalles, et elles aboutissent toutes au même schéma en dents de scie (bien que certaines tentatives aient conduit à ce que le ramassage des ordures ne se produise jamais, car les références étaient conservées).

function doIt() {
    console.log("hai")
}

function a() {
    doIt();
    setTimeout(b, 50);
}
function b() {
    doIt();
    setTimeout(a, 50);
}

a();

http://fiddle.jshell.net/QNRSK/14/

function b() {
    var a = setInterval(function() {
        console.log("Hello");
        clearInterval(a);
        b();                
    }, 50);
}
b();

http://fiddle.jshell.net/QNRSK/17/

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
}
init();

http://fiddle.jshell.net/QNRSK/20/

function init()
{
    window.ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
    clearInterval(window.ref);
    init();
}
init();​

http://fiddle.jshell.net/QNRSK/21/

Apparemment setTimeout y setInterval ne font pas officiellement partie de Javascript (et ne font donc pas partie de la v8). L'implémentation est laissée à l'appréciation de l'implémenteur. Je vous suggère de jeter un coup d'œil à l'implémentation de setInterval et autres dans node.js

29voto

Yury Semikhatsky Points 861

Le problème ici n'est pas dans le code lui-même, il n'y a pas de fuite. Il est dû à la façon dont le panneau Timeline est implémenté. Lorsque la ligne du temps enregistre des événements, nous collectons des traces de pile JavaScript à chaque invocation du rappel setInterval. La trace de pile est d'abord allouée dans le tas JS, puis copiée dans les structures de données natives. Une fois la trace de pile copiée dans l'événement natif, elle devient un déchet dans le tas JS. Cela se reflète dans le graphique. Désactivation de l'appel suivant http://trac.webkit.org/browser/trunk/Source/WebCore/inspector/TimelineRecordFactory.cpp#L55 rend le graphique de la mémoire plat.

Il existe un bogue lié à ce problème : https://code.google.com/p/chromium/issues/detail?id=120186

12voto

ICR Points 6960

Chaque fois que vous faites appel à une fonction, celle-ci crée un fichier cadre de la pile . Contrairement à beaucoup d'autres langages, Javascript stocke le cadre de la pile sur le tas, comme tout le reste. Cela signifie que chaque fois que vous appelez une fonction, ce que vous faites toutes les 50 ms, un nouveau cadre de pile est ajouté au tas. Cela s'accumule et est finalement ramassé par les poubelles.

C'est en quelque sorte inévitable, étant donné le fonctionnement de Javascript. La seule chose que l'on puisse vraiment faire pour l'atténuer est de réduire au maximum la taille de la pile, ce que toutes les implémentations font, j'en suis sûr.

6voto

mrdc Points 401

Je voulais répondre à votre commentaire sur setInterval et le scintillement :

J'ai remarqué que l'utilisation correcte de setInterval(), setTimeout() et même requestAnimationFrame alloue de la mémoire sans que je le demande, et provoque de fréquents appels au garbage collection. Plus d'appels au GC = flickers :-(

Vous pouvez essayer de remplacer l'appel à setInterval par un appel à moins de mal fonction auto-invoquante basée sur setTimeout. Paul Irish en parle dans son exposé intitulé "10 things I learned from the jQuery source" (vidéo en anglais). aquí , note aquí voir n°2). Ce que vous faites, c'est remplacer votre appel à setInterval par une fonction qui s'invoque elle-même indirectement via setTimeout après avoir accompli le travail qu'il est censé faire . Pour citer l'exposé :

Nombreux sont ceux qui affirment que setInterval est une fonction maléfique. Elle continue d'appeler une fonction à des intervalles spécifiés, que la fonction soit terminée ou non.

En utilisant votre code d'exemple ci-dessus, vous pourriez mettre à jour votre fonction init de :

function init() 
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

à :

function init()
{
     //init stuff

     //awesome code

     //start rendering
     drawLoop();
}

function drawLoop()
{
   //do work
   draw();

   //queue more work
   setTimeout(drawLoop, 50);
}

Cela devrait aider un peu car :

  1. draw() ne sera pas rappelé par votre boucle de rendu tant qu'elle n'est pas terminée
  2. comme le soulignent plusieurs des réponses ci-dessus, tous les appels de fonction ininterrompus de setInterval entraînent une surcharge pour le navigateur.
  3. le débogage est un peu plus facile car vous n'êtes pas interrompu par le déclenchement continu de setInterval

J'espère que cela vous aidera !

3voto

allyourcode Points 5670

Chrome ne subit pratiquement aucune pression de mémoire de la part de votre programme (1,23 Mo est une utilisation très faible de la mémoire selon les normes actuelles), il ne pense donc probablement pas qu'il doive utiliser la GC de manière agressive. Si vous modifiez votre programme pour qu'il utilise plus de mémoire, vous verrez le ramasse-miettes se mettre en marche :

<!html>
<html>
<head>
<title>Where goes memory?</title>
</head>
<body>

Greetings!

<script>
function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    var ar = new Array();
    for (var i = 0; i < 1e6; ++i) {
        ar.push(Math.rand());
    }
    return true
}

init();
</script>

</body>
</html>

Lorsque j'exécute cette opération, j'obtiens un schéma d'utilisation de la mémoire en dents de scie, avec un pic autour de 13,5 Mo (ce qui est encore une fois assez faible par rapport aux normes d'aujourd'hui).

PS : Spécificités de mes navigateurs :

Google Chrome   23.0.1271.101 (Official Build 172594)
OS  Mac OS X
WebKit  537.11 (@136278)
JavaScript  V8 3.13.7.5
Flash   11.5.31.5
User Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X