Node.js: se excedió el tamaño máximo de la pila de llamadas

80

Cuando ejecuto mi código, Node.js lanza una "RangeError: Maximum call stack size exceeded"excepción causada por demasiadas llamadas recursivas. Intenté aumentar el tamaño de la pila de Node.js sudo node --stack-size=16000 app, pero Node.js se bloquea sin ningún mensaje de error. Cuando vuelvo a ejecutar esto sin sudo, Node.js imprime 'Segmentation fault: 11'. ¿Existe la posibilidad de resolver esto sin eliminar mis llamadas recursivas?

usuario1518183
fuente
3
¿Por qué necesitas una recursividad tan profunda en primer lugar?
Dan Abramov
1
Por favor, ¿puedes publicar algún código? Segmentation fault: 11generalmente significa un error en el nodo.
vkurchatkin
1
@Dan Abramov: ¿Por qué la recursividad profunda? Esto puede ser un problema si desea iterar sobre una matriz o lista y realizar una operación asíncrona en cada una (por ejemplo, alguna operación de base de datos). Si usa la devolución de llamada de la operación asíncrona para pasar al siguiente elemento, habrá al menos un nivel adicional de recursividad para cada elemento de la lista. El anti-patrón proporcionado por heinob a continuación evita que la pila se salga.
Philip Callender
1
@PhilipCallender No me di cuenta de que estabas haciendo cosas asincrónicas, ¡gracias por la aclaración!
Dan Abramov
@DanAbramov Tampoco tiene que ser profundo para fallar. V8 no tiene la oportunidad de limpiar las cosas asignadas en la pila. Las funciones llamadas anteriormente que dejaron de ejecutarse hace mucho tiempo pueden haber creado variables en la pila a las que ya no se hace referencia pero que aún se mantienen en la memoria. Si está realizando una operación que consume mucho tiempo de manera síncrona y asigna variables en la pila mientras lo hace, todavía se bloqueará con el mismo error. Conseguí que mi analizador JSON sincrónico se bloqueara a una profundidad de pila de
llamadas

Respuestas:

114

Debes envolver tu llamada de función recursiva en un

  • setTimeout,
  • setImmediate o
  • process.nextTick

función para darle a node.js la oportunidad de borrar la pila. Si no hace eso y hay muchos bucles sin ninguna llamada de función asíncrona real o si no espera la devolución de llamada, RangeError: Maximum call stack size exceededserá inevitable .

Hay muchos artículos sobre el "bucle asincrónico potencial". Aquí tienes uno .

Ahora un código de ejemplo más:

// 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
    ...
});

Esto es correcto:

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
    ...
});

Ahora su ciclo puede volverse demasiado lento, porque perdemos un poco de tiempo (un viaje de ida y vuelta al navegador) por ronda. Pero no tiene que llamar setTimeouten todas las rondas. Normalmente está bien hacerlo cada milésima vez. Pero esto puede diferir según el tamaño de su pila:

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
    ...
});
heinob
fuente
6
Hubo algunos puntos buenos y malos en su respuesta. Me gustó mucho que hayas mencionado setTimeout () et al. Pero no es necesario usar setTimeout (fn, 1), ya que setTimeout (fn, 0) está perfectamente bien (por lo que no necesitamos setTimeout (fn, 1) cada% 1000 hack). Permite que la máquina virtual de JavaScript borre la pila y reanude inmediatamente la ejecución. En node.js, el process.nextTick () es ligeramente mejor porque permite que node.js haga otras cosas (I / O IIRC) también antes de permitir que se reanude la devolución de llamada.
joonas.fi
2
Yo diría que es mejor usar setImmediate en lugar de setTimeout en estos casos.
BaNz
4
@ joonas.fi: Mi "truco" con% 1000 es necesario. Hacer un setImmediate / setTimeout (incluso con 0) en cada ciclo es dramáticamente más lento.
heinob
3
¿Quiere actualizar sus comentarios en alemán en código con traducción al inglés ...? :) Entiendo, pero otros podrían no tener tanta suerte.
Robert Rossmann
gracias
Angelos Kyriakopoulos
30

Encontré una solución sucia:

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

Simplemente aumenta el límite de la pila de llamadas. Creo que esto no es adecuado para el código de producción, pero lo necesitaba para un script que se ejecuta solo una vez.

usuario1518183
fuente
Buen truco, aunque personalmente sugeriría usar prácticas correctas para evitar errores y crear una solución más completa.
decodificador 7283
Para mí, esta fue una solución de desbloqueo. Tuve un escenario en el que estaba ejecutando un script de actualización de terceros de una base de datos y obtenía el error de rango. No iba a reescribir el paquete de terceros pero necesitaba actualizar la base de datos → esto lo solucionó.
Tim Kock
7

En algunos lenguajes, esto se puede resolver con la optimización de la llamada de cola, donde la llamada de recursividad se transforma bajo el capó en un bucle para que no exista un error de tamaño máximo de pila alcanzado.

Pero en javascript los motores actuales no admiten esto, está previsto para una nueva versión del lenguaje Ecmascript 6 .

Node.js tiene algunos indicadores para habilitar las funciones de ES6, pero la llamada de cola aún no está disponible.

Por lo tanto, puede refactorizar su código para implementar una técnica llamada trampolín , o refactorizar para transformar la recursividad en un bucle .

Universidad Angular
fuente
Gracias. Mi llamada de recursividad no devuelve valor, entonces, ¿hay alguna forma de llamar a la función y no esperar el resultado?
user1518183
¿Y la función altera algunos datos, como una matriz, qué hace la función, cuáles son las entradas / salidas?
Angular University
5

Tuve un problema similar a este. Tuve un problema con el uso de varios Array.map () en una fila (alrededor de 8 mapas a la vez) y estaba obteniendo un error maximum_call_stack_exceeded. Resolví esto cambiando el mapa en bucles 'for'

Entonces, si está utilizando muchas llamadas de mapa, cambiarlas a bucles for puede solucionar el problema

Editar

Solo para mayor claridad y probablemente información no necesaria pero buena para saber, el uso .map()hace que la matriz se prepare (resolviendo captadores, etc.) y la devolución de llamada se almacene en caché, y también mantiene internamente un índice de la matriz ( por lo que la devolución de llamada se proporciona con el índice / valor correcto). Esto se acumula con cada llamada anidada, y se recomienda precaución cuando no está anidada también, como la siguiente.map() podría llamarse antes de que la primera matriz sea recolectada como basura (si es que lo hace).

Toma este ejemplo:

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 cambiamos esto a:

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
}

Espero que esto tenga algún sentido (no tengo la mejor forma de usar las palabras) y ayude a algunos a evitar el rascado de cabeza por el que pasé.

Si alguien está interesado, aquí también hay una prueba de rendimiento que compara el mapa y los bucles for (no es mi trabajo).

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

Los bucles for suelen ser mejores que los mapas, pero no reducen, filtran ni encuentran

Werlious
fuente
Hace un par de meses, cuando leí tu respuesta, no tenía idea del oro que tenías en tu respuesta. Recientemente descubrí esto mismo por mí mismo y realmente me hizo querer desaprender todo lo que tengo, a veces es difícil pensar en forma de iteradores. Espero que esto ayude: escribí un ejemplo adicional que incluye promesas como parte del ciclo y muestra cómo esperar la respuesta antes de continuar. ejemplo: gist.github.com/gngenius02/…
cigol el
Me encanta lo que hiciste allí (y espero que no te importe si agarro eso recortado para mi caja de herramientas). Utilizo principalmente código síncrono, por lo que generalmente prefiero los bucles. Pero esa es una joya que también obtuviste allí, y lo más probable es que encuentre su camino hacia el próximo servidor en el que trabajo
Werlious
2

Pre:

para mí, el programa con la pila de llamadas Max no se debió a mi código. Terminó siendo un problema diferente lo que provocó la congestión en el flujo de la aplicación. Entonces, debido a que estaba tratando de agregar demasiados elementos a mongoDB sin ninguna posibilidad de configuración, el problema de la pila de llamadas estaba apareciendo y me tomó unos días descubrir qué estaba pasando ... eso decía:


Siguiendo con lo que respondió @Jeff Lowery: Disfruté mucho esta respuesta y aceleró el proceso de lo que estaba haciendo al menos 10 veces.

Soy nuevo en la programación, pero intenté modularizar la respuesta. Además, no me gustó que se lanzara el error, así que lo envolví en un bucle do while. Si algo de lo que hice es incorrecto, no dude en corregirme.

module.exports = function(object) {
    const { max = 1000000000n, fn } = object;
    let counter = 0;
    let running = true;
    Error.stackTraceLimit = 100;
    const A = (fn) => {
        fn();
        flipper = B;
    };
    const B = (fn) => {
        fn();
        flipper = A;
    };
    let flipper = B;
    const then = process.hrtime.bigint();
    do {
        counter++;
        if (counter > max) {
            const now = process.hrtime.bigint();
            const nanos = now - then;
            console.log({ 'runtime(sec)': Number(nanos) / 1000000000.0 });
            running = false;
        }
        flipper(fn);
        continue;
    } while (running);
};

Echa un vistazo a esta esencia para ver mis archivos y cómo llamar al bucle. https://gist.github.com/gngenius02/3c842e5f46d151f730b012037ecd596c

cigol encendido
fuente
1

Si no desea implementar su propio contenedor, puede usar un sistema de cola, por ejemplo , async.queue , queue .

debilitado
fuente
1

Pensé en otro enfoque que usa referencias de funciones que limitan el tamaño de la pila de llamadas sin usar 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;
}

salida:

$ 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)
Jeff Lowery
fuente
0

Con respecto al aumento del tamaño de pila máximo, en máquinas de 32 y 64 bits, los valores predeterminados de asignación de memoria de V8 son, respectivamente, 700 MB y 1400 MB. En las versiones más recientes de V8, los límites de memoria en los sistemas de 64 bits ya no están establecidos por V8, lo que teóricamente indica que no hay límite. Sin embargo, el SO (sistema operativo) en el que se ejecuta Node siempre puede limitar la cantidad de memoria que puede ocupar V8, por lo que el límite real de cualquier proceso dado no se puede establecer de forma general.

Aunque V8 pone a disposición la --max_old_space_sizeopción, que permite controlar la cantidad de memoria disponible para un proceso , aceptando un valor en MB. Si necesita aumentar la asignación de memoria, simplemente pase a esta opción el valor deseado al generar un proceso de nodo.

A menudo, es una excelente estrategia reducir la asignación de memoria disponible para una instancia de Nodo determinada, especialmente cuando se ejecutan muchas instancias. Al igual que con los límites de pila, considere si las necesidades de memoria masiva se delegan mejor a una capa de almacenamiento dedicada, como una base de datos en memoria o similar.

serkan
fuente
0

Compruebe que la función que está importando y la que ha declarado en el mismo archivo no tienen el mismo nombre.

Les daré un ejemplo de este error. En Express JS (usando ES6), considere el siguiente escenario:

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

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

El escenario anterior causará el infame RangeError: el tamaño máximo de la pila de llamadas excedió el error porque la función sigue llamándose a sí misma tantas veces que se agota la pila máxima de llamadas.

La mayoría de las veces, el error está en el código (como el anterior). Otra forma de resolverlo es aumentar manualmente la pila de llamadas. Bueno, esto funciona para ciertos casos extremos, pero no se recomienda.

Espero que mi respuesta te haya ayudado.

Abhay Shiro
fuente
-4

Puede usar loop for.

var items = {1, 2, 3}
for(var i = 0; i < items.length; i++) {
  if(i == items.length - 1) {
    res.ok(i);
  }
}
Marcin Kamiński
fuente
2
var items = {1, 2, 3}no es una sintaxis JS válida. ¿Cómo se relaciona esto con la pregunta?
musemind