web-dev-qa-db-fra.com

Node.js - Taille maximale de la pile d'appels dépassée

Lorsque je lance mon code, Node.js lève une exception "RangeError: Maximum call stack size exceeded" provoquée par trop d'appels de récursivité. J'ai essayé d'augmenter la taille de pile de Node.js de Sudo node --stack-size=16000 app, mais Node.js se bloque sans message d'erreur. Lorsque je lance à nouveau sans Sudo, Node.js affiche 'Segmentation fault: 11'. Est-il possible de résoudre ce problème sans supprimer l'appel récursif? 

Merci

64
user1518183

Vous devriez envelopper votre appel de fonction récursif dans un 

  • setTimeout,
  • setImmediate ou 
  • process.nextTick 

fonction pour donner à node.js la possibilité d'effacer la pile. Si vous ne le faites pas et qu'il y a beaucoup de boucles sans appel de fonction async real ou si vous n'attendez pas le rappel, votre RangeError: Maximum call stack size exceeded sera inévitable.

Il existe de nombreux articles concernant "la boucle asynchrone potentielle". En voici un

Maintenant quelques exemples de code:

// ANTI-PATTERN
// THIS WILL CRASH

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            // this will crash after some rounds with
            // "stack exceed", because control is never given back
            // to the browser 
            // -> no GC and browser "dead" ... "VERY BAD"
            potAsyncLoop( i+1, resume ); 
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});

C'est juste:

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            // Now the browser gets the chance to clear the stack
            // after every round by getting the control back.
            // Afterwards the loop continues
            setTimeout( function() {
                potAsyncLoop( i+1, resume ); 
            }, 0 );
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});

Maintenant, votre boucle peut devenir trop lente, car nous perdons un peu de temps (un aller-retour par navigateur) par tour. Mais vous n'êtes pas obligé d'appeler setTimeout à chaque tour. Normalement c'est o.k. de le faire toutes les 1000 fois. Mais cela peut différer selon la taille de votre pile:

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            if( i % 1000 === 0 ) {
                setTimeout( function() {
                    potAsyncLoop( i+1, resume ); 
                }, 0 );
            } else {
                potAsyncLoop( i+1, resume ); 
            }
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});
88
heinob

J'ai trouvé une solution sale:

/bin/bash -c "ulimit -s 65500; exec /usr/local/bin/node --stack-size=65500 /path/to/app.js"

Il suffit d'augmenter la limite de la pile d'appels. Je pense que cela ne convient pas au code de production, mais j'en avais besoin pour un script exécuté une seule fois.

20
user1518183

Dans certaines langues, cela peut être résolu avec l'optimisation d'appel final, l'appel récursif étant transformé sous le capot en boucle, il n'y a donc pas d'erreur de taille maximale de pile atteinte.

Mais en javascript, les moteurs actuels ne le supportent pas, c'est prévu pour la nouvelle version du langage Ecmascript 6 .

Node.js a quelques options pour activer les fonctionnalités ES6 mais l'appel final n'est pas encore disponible.

Vous pouvez donc refactoriser votre code pour implémenter une technique appelée trampolining , ou refactor afin de transformer la récursivité en boucle .

5
Angular University

Si vous ne souhaitez pas implémenter votre propre wrapper, vous pouvez utiliser un système de file d'attente, par exemple. async.queue , queue .

1
weakish

J'avais un problème similaire à celui-ci . J'avais un problème d'utilisation de plusieurs Array.map () de manière consécutive (environ 8 cartes à la fois) ceci en changeant la carte en boucles "pour"

Donc, si vous utilisez beaucoup d'appels de carte, les changer en boucles for peut résoudre le problème

Modifier

Juste pour plus de clarté et pour une information probablement non nécessaire, mais utile à savoir, utiliser .map() permet de préparer le tableau (résolution de getters, etc.), de rappeler le rappel et de conserver en interne un index du tableau. (le callback est donc fourni avec l'index/la valeur correcte). Cela se cumule avec chaque appel imbriqué, et la prudence est recommandée si le n ° .map() suivant ne peut pas être imbriqué avant que le premier tableau ne soit nettoyé (s'il y en a un). 

Prenons cet exemple:

var cb = *some callback function*
var arr1 , arr2 , arr3 = [*some large data set]
arr1.map(v => {
    *do something
})
cb(arr1)
arr2.map(v => {
    *do something // even though v is overwritten, and the first array
                  // has been passed through, it is still in memory
                  // because of the cached calls to the callback function
}) 

Si nous changeons cela en:

for(var|let|const v in|of arr1) {
    *do something
}
cb(arr1)
for(var|let|const v in|of arr2) {
    *do something  // Here there is not callback function to 
                   // store a reference for, and the array has 
                   // already been passed of (gone out of scope)
                   // so the garbage collector has an opportunity
                   // to remove the array if it runs low on memory
}

J'espère que cela a du sens (je n'ai pas le meilleur moyen d'utiliser des mots) et aide quelques-uns à éviter que ma tête ne se gratte

Si quelqu'un est intéressé, voici également un test de performance comparant carte et boucles (ce n'est pas mon travail).

https://github.com/dg92/Performance-Analysis-JS

Les boucles for sont généralement meilleures que map, mais ne réduisent pas, ne filtrent pas et ne trouvent pas

1
Werlious

J'ai pensé à une autre approche utilisant des références de fonction limitant la taille de la pile d'appels sans utiliser setTimeout() (Node.js, v10.16.0) :

testLoop.js

let counter = 0;
const max = 1000000000n  // 'n' signifies BigInteger
Error.stackTraceLimit = 100;

const A = () => {
  fp = B;
}

const B = () => {
  fp = A;
}

let fp = B;

const then = process.hrtime.bigint();

for(;;) {
  counter++;
  if (counter > max) {
    const now = process.hrtime.bigint();
    const nanos = now - then;

    console.log({ "runtime(sec)": Number(nanos) / (1000000000.0) })
    throw Error('exit')
  }
  fp()
  continue;
}

sortie:

$ node testLoop.js
{ 'runtime(sec)': 18.947094799 }
C:\Users\jlowe\Documents\Projects\clearStack\testLoop.js:25
    throw Error('exit')
    ^

Error: exit
    at Object.<anonymous> (C:\Users\jlowe\Documents\Projects\clearStack\testLoop.js:25:11)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
0
Jeff Lowery

Vérifiez que la fonction que vous importez et celle que vous avez déclarée dans le même fichier ne portent pas le même nom.

Je vais vous donner un exemple pour cette erreur. Dans Express JS (utilisant ES6), envisagez le scénario suivant:

import {getAllCall} from '../../services/calls';

let getAllCall = () => {
   return getAllCall().then(res => {
      //do something here
   })
}
module.exports = {
getAllCall
}

Le scénario ci-dessus provoquera une erreur notoire RangeError: La taille maximale de la pile d'appels a été dépassée, car la fonction continue à s'appeler si souvent qu'elle ne dispose plus de la pile maximale d'appels. 

La plupart du temps, l'erreur est dans le code (comme celui ci-dessus). Une autre solution consiste à augmenter manuellement la pile d’appels. Cela fonctionne dans certains cas extrêmes, mais ce n’est pas recommandé.

J'espère que ma réponse vous a aidé.

0
Abhay Shiro

En ce qui concerne l'augmentation de la taille maximale de la pile, sur les machines 32 bits et 64 bits, les valeurs par défaut d'allocation de mémoire du V8 sont respectivement 700 Mo et 1400 Mo. Dans les versions plus récentes de V8, les limites de mémoire sur les systèmes 64 bits ne sont plus définies par V8, indiquant théoriquement aucune limite. Cependant, le système d'exploitation (système d'exploitation) sur lequel Node s'exécute peut toujours limiter la quantité de mémoire que V8 peut prendre. Par conséquent, la véritable limite d'un processus donné ne peut pas être définie de manière générale.

Bien que V8 rende disponible l'option --max_old_space_size, qui permet de contrôler la quantité de mémoire disponible pour un process , en acceptant une valeur en Mo. Si vous avez besoin d'augmenter l'allocation de mémoire, transmettez simplement cette option à la valeur souhaitée lors de la génération d'un processus de nœud.

C’est souvent une excellente stratégie pour réduire l’allocation de mémoire disponible pour une instance de nœud donnée, en particulier lors de l’exécution de nombreuses instances. Comme pour les limites de pile, déterminez si les besoins énormes en mémoire sont mieux délégués à une couche de stockage dédiée, telle qu'une base de données en mémoire ou similaire.

0
serkan