Node: Faltan seguimientos de pila de funciones asíncronas después de la primera espera

Creado en 15 mar. 2017  ·  127Comentarios  ·  Fuente: nodejs/node

Versión: v7.7.3
Plataforma: Windows 7x64

El propósito de este problema es realmente una solicitud más amplia para mejores seguimientos de pila con async / await, pero pensé que comenzaría con un caso muy específico. Si necesitamos ampliar la descripción, estoy bien con eso. He visto una serie de discusiones extensas sobre este tema en varios lugares, pero no veo ningún problema real al respecto, así que pensé en comenzar una y, con suerte, no es solo un duplicado de algo que me perdí.

Estoy presentando el problema específico aquí porque parece que la funcionalidad async / await recién agregada proporciona un manejo de errores menos útil de lo que podríamos obtener con los generadores.

async function functionOne() {
  await new Promise((resolve) => {
    setTimeout(() => { resolve(); }, 1);
  });
  throw new Error('Something Bad');
}

async function functionTwo() {
  await functionOne();
}

functionTwo()
  .catch((error) => {
    console.error(error);
  });

Salidas:

Error: Something Bad
    at functionOne (C:\Work\sandbox.js:5:9)

A esa pila le falta todo lo que llamó functionOne ( functionTwo específicamente).

El generador equivalente a esto:

const co = require('co');

function* functionOne() {
  yield new Promise((resolve) => {
    setTimeout(() => { resolve(); }, 1);
  });
  throw new Error('Something Bad');
}

function* functionTwo() {
  yield* functionOne();
}

co(functionTwo())
  .catch((error) => {
    console.log(error);
  });

Salidas:

Error: Something Bad
    at functionOne (C:\Work\sandbox.js:7:9)
    at functionOne.next (<anonymous>)
    at functionTwo (C:\Work\sandbox.js:11:10)
    at functionTwo.next (<anonymous>)
    at onFulfilled (C:\Work\NPS\nps-dev\node_modules\co\index.js:65:19)

Aquí puede ver tanto functionOne como functionTwo en la pila.

Si el error se produce antes de cualquier await en el código, entonces obtendrá un seguimiento de pila completo incluso si la función fue llamada en una cadena completa de esperas e independientemente de si esas esperas fueron las primeras o no:

async function throwFunction() {
  throw new Error('Something bad');
}

async function functionOne() {
  return await throwFunction();
}

async function functionTwo() {
  return await Promise.resolve();
}

async function functionThree() {
  await functionTwo();
  return await functionOne();
}

functionThree()
  .catch((error) => {
    console.log(error);
  });

Salidas:

Error: Something bad
    at throwFunction (C:\Work\sandbox.js:2:9)
    at functionOne (C:\Work\sandbox.js:6:16)
    at functionThree (C:\Work\sandbox.js:15:16)
    at process._tickCallback (internal/process/next_tick.js:109:7)
    at Module.runMain (module.js:607:11)
    at run (bootstrap_node.js:425:7)
    at startup (bootstrap_node.js:146:9)
    at bootstrap_node.js:540:3

La verdadera fuerza impulsora detrás de esto fue que finalmente encontré la receta para obtener seguimientos completos de la pila, incluso cuando se trata de código existente usando promesas. Con un try ... catch en el generador, podemos usar VError para soldar los errores arrojados por las promesas con la pila del código que llama al generador. Esto no parece funcionar con funciones asíncronas.

Aquí hay un ejemplo más completo usando generadores que realmente desearía que siguieran funcionando con funciones asíncronas:

const co = require('co');
const VError = require('verror');

function calledFromAPromise() {
  throw new Error('Something bad');
}

function doAPromise() {
  return new Promise((resolve) => {
    setTimeout(() => { resolve(); }, 1);
  })
    .then(() => { calledFromAPromise(); });
}

function* queryFunction() {
  return yield* yieldRethrow(doAPromise());
}

function* functionOne() {
  return yield* queryFunction();
}

function* functionTwo() {
  return yield* functionOne();
}

function* yieldRethrow(iterable) {
  try {
    return yield iterable;
  } catch (error) {
    throw new VError(error);
  }
}

co(functionTwo())
  .catch((error) => {
    console.log(error);
  });

Salidas (con algunas cosas no relevantes eliminadas):

{ VError: : Something bad
    at yieldRethrow (C:\Work\sandbox.js:31:11)
    at yieldRethrow.throw (<anonymous>)
    at queryFunction (C:\Work\sandbox.js:16:17)
    at functionOne (C:\Work\sandbox.js:20:17)
    at functionTwo (C:\Work\sandbox.js:24:17)
    at onRejected (C:\Work\NPS\nps-dev\node_modules\co\index.js:81:24)
  jse_cause: 
   Error: Something bad
       at calledFromAPromise (C:\Work\sandbox.js:5:9)
       at Promise.then (C:\Work\sandbox.js:12:19),

El equivalente asíncrono genera esto:

{ VError: : Something bad
    at yieldRethrow (C:\Work\sandbox.js:30:11)
  jse_cause: 
   Error: Something bad
       at calledFromAPromise (C:\Work\sandbox.js:4:9)
       at Promise.then (C:\Work\sandbox.js:11:19),

Como puede ver, esto tiene el mismo problema que en la parte superior, ya que el error relanzado no tiene una pila completa.

V8 Engine diag-agenda feature request promises question

Comentario más útil

¡Gracias! Y para poner un punto en el problema real, imagine que la capa de mi base de datos arroja un error como el siguiente:

error: syntax error at end of input
    at Connection.parseE (C:\Project\node_modules\pg\lib\connection.js:567:11)
    at Connection.parseMessage (C:\Project\node_modules\pg\lib\connection.js:391:17)
    at Socket.<anonymous> (C:\Project\node_modules\pg\lib\connection.js:129:22)
    at emitOne (events.js:96:13)
    at Socket.emit (events.js:191:7)
    at readableAddChunk (_stream_readable.js:178:18)
    at Socket.Readable.push (_stream_readable.js:136:10)
    at TCP.onread (net.js:560:20)

¿En qué me resulta útil esta información, especialmente si ocurrió en producción donde no sé qué la causó? No hay una sola línea de código en esa pila de mi código y el error de la biblioteca que estoy usando ni siquiera me dice lo que realmente está mal. No tendría absolutamente ninguna esperanza de averiguar cuál era la causa de esto sin revisar cada declaración de la base de datos en mi código.

¿Estoy en la minoría aquí que piensa que este es un tema importante al tratar de resolver los problemas?

Todos 127 comentarios

Cc @ nodejs / diagnósticos

Eso es porque Error.stack solo refleja la pila real. El inspector proporciona seguimientos de pila asíncronos, pero Error.stack nunca incluirá las partes asíncronas debido a la sobrecarga de recopilarlo.

¿No sería bueno si hubiera una forma de habilitar los seguimientos de pila asíncronos a través de una bandera o algo así? ¿O cuando NODE_ENV === 'development' y

La sobrecarga es enorme, pero hay casos en los que los desarrolladores querrán que Error.stack genere errores asíncronos para ellos.

Puede ejecutar el nodo con --inspect y habilitar el seguimiento de pila asíncrono en DevTools.

Creo que sería mejor algo más de línea de comandos. Personalmente, solo uso Chrome DevTools cuando realmente quiero averiguar con precisión qué está sucediendo. Pero la mayoría de las veces, los desarrolladores solo quieren rastros de pila informativos en su terminal.

algo más de línea de comandos sería mejor

¿ node -r longjohn funcionaría para usted?

Arrr, quizás

¿Sería posible agregar una opción V8 que habilite pilas de error asíncronas para propiedades de error.stack "normales"? ¿O sería demasiado arriesgado?

Tenemos ganchos de

El plan @ nodejs / diagnostics para resolver esto es:

  1. actualice a V8 5.7, esto contiene la API PromiseHook C ++ - PR https://github.com/nodejs/node/pull/11752
  2. Integre PromiseHook C ++ API a async_hooks y obtenga async_hooks - PR: https://github.com/nodejs/node/pull/11883
  3. Esto permitirá a los autores de módulos crear un modelo de seguimiento de pila larga que siempre funciona.

Es poco probable que este problema se resuelva directamente con el núcleo del nodo, solo daremos las herramientas a los autores del módulo para que puedan resolverlo. Por ejemplo, he escrito https://github.com/AndreasMadsen/trace , que usa una versión sin documentar de async_hooks . _Esto no resolverá este problema como está, porque todavía no tenemos la API de PromiseHook C ++, pero eventualmente se actualizará.

Para que lo entienda, ¿cuál es el razonamiento detrás de resolver esto en el área de usuario en lugar del núcleo del nodo?

Y supongo que la pregunta que tengo es ¿qué cambió? Este problema no parecía existir con el código generador equivalente. Entiendo que hay diferencias técnicas entre generadores y async / await, pero la idea fundamental de que la máquina virtual pausa la ejecución y luego la reanuda parece la misma y, sin embargo, cuando hicimos eso con los generadores, el seguimiento de pila completo estaba disponible.

Los generadores se reanudan a partir del código de usuario. Las funciones asíncronas se reanudan desde la cola de micro tareas.

Para que lo entienda, ¿cuál es el razonamiento detrás de resolver esto en el área de usuario en lugar del núcleo del nodo?

Porque se puede resolver en la tierra del usuario, una vez que los componentes básicos estén en su lugar. Node. trace es un módulo de 80 líneas (con comentarios en su mayoría), por lo que podría decirse que no es tan difícil. También hay diferentes formas de implementarlo, lo que implica compensaciones entre velocidad, memoria, integridad y usabilidad. Probablemente veremos diferentes módulos, que priorizan de manera diferente.

Yo diría que trace prioriza la memoria y la integridad, mientras que longjohn prioriza la velocidad y la usabilidad (pero probablemente estoy sesgado).

Los generadores se reanudan a partir del código de usuario. Las funciones asíncronas se reanudan desde la cola de micro tareas.

Correcto. Hay una segunda preocupación relacionada con las promesas. Anteriormente, new Promise() se usaba en combinación con generadores, esto permitía implementaciones de seguimiento de pila larga para parchear global.Promise y esencialmente implementar una aproximación _PromiseHook C ++ API_ de esta manera. async function devuelve consistentemente una promesa usando una referencia interna v8 (no global.Promise ), por lo que el parche de mono global.Promise no es suficiente.

Este problema ha estado inactivo durante el tiempo suficiente y parece que quizás debería cerrarse. No dude en volver a abrir (o dejar un comentario solicitando que se vuelva a abrir) si no está de acuerdo. Solo estoy arreglando y no actuando con una opinión muy fuerte ni nada de eso.

Me gustaría argumentar que se reabre este tema. Si no es por otra razón, puede ser un lugar para informar sobre cualquier tipo de progreso que se haya realizado para agregar un mejor soporte para los seguimientos de pila en Node. Creo que hay una brecha legítima en la funcionalidad aquí y hasta que esa brecha se pueda cerrar, yo diría que este problema debería permanecer abierto.

Lo he vuelto a abrir. Además, lo cambié de "pregunta" a "solicitud de función"

¡Gracias! Y para poner un punto en el problema real, imagine que la capa de mi base de datos arroja un error como el siguiente:

error: syntax error at end of input
    at Connection.parseE (C:\Project\node_modules\pg\lib\connection.js:567:11)
    at Connection.parseMessage (C:\Project\node_modules\pg\lib\connection.js:391:17)
    at Socket.<anonymous> (C:\Project\node_modules\pg\lib\connection.js:129:22)
    at emitOne (events.js:96:13)
    at Socket.emit (events.js:191:7)
    at readableAddChunk (_stream_readable.js:178:18)
    at Socket.Readable.push (_stream_readable.js:136:10)
    at TCP.onread (net.js:560:20)

¿En qué me resulta útil esta información, especialmente si ocurrió en producción donde no sé qué la causó? No hay una sola línea de código en esa pila de mi código y el error de la biblioteca que estoy usando ni siquiera me dice lo que realmente está mal. No tendría absolutamente ninguna esperanza de averiguar cuál era la causa de esto sin revisar cada declaración de la base de datos en mi código.

¿Estoy en la minoría aquí que piensa que este es un tema importante al tratar de resolver los problemas?

Para que lo entienda, ¿cuál es el razonamiento detrás de resolver esto en el área de usuario en lugar del núcleo del nodo?

Porque se puede resolver en la tierra del usuario, una vez que los componentes básicos estén en su lugar. Node.

Y supongo que me gustaría desafiar esto también. Me parece que algo tan fundamental como obtener una pila completa de errores de mi código es exactamente lo que esperaría de mi plataforma en tiempo de ejecución. No estoy seguro de que el argumento de que existen compensaciones funciona en este caso particular. Yo diría que casi cualquier entorno de producción razonable querrá una característica como esta e implementada de una manera que no tenga grandes inconvenientes (como un gran impacto en el rendimiento). Si se puede hacer algo así, casi todos lo querrán y si no se puede hacer, casi nadie lo querrá. No veo mucho espacio para las opciones aquí, así que darme opciones no me hace sentir mejor.

@TazmanianD +1. Me he mantenido cuerdo usando transform-async-to-generator , y usando Bluebird con rastros de pila largos. Estoy seguro de que me estoy perdiendo las ganancias de rendimiento de async / await nativo, pero al igual que usted, no podría manejar el manejo de los seguimientos de pila sin contexto.

No eres una minoría. Existe una solución secreta llamada _coroutines_. Otros lenguajes como golang los tienen y también se pueden habilitar en node.js ( npm install fibers o npm install f-promise (mejor interoperabilidad con promesas)).

Beneficios: 1) puede eliminar el 99% de la sobrecarga de sintaxis asíncrona / en espera, 2) seguimientos de pila perfectos (sin sobrecarga de rendimiento, en realidad lo contrario)

y el uso de Bluebird con trazas de pila largas.

De su propia documentación:

Los seguimientos de pila larga implican una penalización sustancial del rendimiento, alrededor de 4-5 veces para el rendimiento.

Lo que consideraría inaceptable para uso de producción. La ironía aquí es que el principal argumento a favor del modelo asincrónico de Node es precisamente por razones de rendimiento y si lo primero que haces es algo para matar el rendimiento precisamente como consecuencia de ese modelo, debes preguntarte cuál es el sentido de usar Node es :-).

npm instalar fibras o npm instalar f-promise

¡Oh hombre, eres mi héroe! Parece que las fibras son exactamente lo que quería y puedo ver cómo resuelven un montón de problemas diferentes. Solo jugué un poco con él, pero esta puede ser una solución de "espacio de usuario" a mi problema original con la que estoy realmente feliz.

Probé su biblioteca f-promise y una cosa que noté en un punto de referencia simple es que parece ser un poco más lento que usar generadores o async / await (aproximadamente 2%). ¿Eso es una sorpresa? Por supuesto, un costo tan pequeño vale totalmente el beneficio.

Parece haber un montón de bibliotecas construidas sobre fibras. ¿Le importaría argumentar por los suyos sobre los demás? Es posible que desee considerar agregar su biblioteca a su lista: https://github.com/laverdet/node-fibers/wiki.

La sobrecarga de las fibras depende de los patrones de su código. Si está escribiendo código de bajo nivel que está completamente dominado por devoluciones de llamada (cada devolución de llamada hace poco trabajo, con pilas poco profundas), no recomendaría fibras debido a la sobrecarga de cambiar a una fibra en cada devolución de llamada.

Por otro lado, tan pronto como empiece a tener una capa de lógica algo gruesa en la parte superior de las llamadas asíncronas, las corrutinas son un ganador. Una función asíncrona que llame a otra función asíncrona no implicará ninguna sobrecarga intermedia de creación / resolución de promesas; solo será una llamada de función normal y directa (es por eso que obtienes trazas de pila _perfect_ gratis). Entonces, cuanto más profundas sean sus pilas, más ganará.

Y esto no es un juguete. Algunas estructuras (Meteor) se han construido con fibras. El proyecto en el que estoy involucrado lo ha estado usando durante más de 5 años. Lo usamos escondido debajo de un preprocesador (streamline.js) inicialmente, lo que nos dio la opción de intercambiar fibras por devoluciones de llamada o generadores puros (en caso de que algo saliera mal con las fibras). Pero las partes más nuevas del proyecto ahora están escritas con f-promise.

Un problema importante: las fibras no se ejecutan en los navegadores, ya que requieren un complemento binario. Espero que las corrutinas lleguen a JavaScript algún día, pero hasta ahora ha habido una resistencia muy fuerte (la regla de continuación de un solo cuadro).

El principal punto de venta de mi pequeña biblioteca: la simplicidad de su API (2 llamadas) y la facilidad de interoperabilidad con promesas (y devoluciones de llamada también; me di cuenta de que esto no estaba documentado, así que lo agregué al archivo README). También lo diseñé para que fuera una actualización natural para la comunidad streamline.js (la función CLS es compatible). Pero simplemente busque en Google y encontrará otras bibliotecas.

Y supongo que me gustaría desafiar esto también. Me parece que algo tan fundamental como obtener una pila completa de errores de mi código es exactamente lo que esperaría de mi plataforma en tiempo de ejecución. No estoy seguro de que el argumento de que existen compensaciones funciona en este caso particular. Yo diría que casi cualquier entorno de producción razonable querrá una característica como esta e implementada de una manera que no tenga grandes inconvenientes (como un gran impacto en el rendimiento). Si se puede hacer algo así, casi todos lo querrán y si no se puede hacer, casi nadie lo querrá. No veo mucho espacio para las opciones aquí, así que darme opciones no me hace sentir mejor.

No existe una solución mágica. No digo que sea imposible, pero es muy difícil. El día que tengamos seguimientos de pila larga de alto rendimiento que funcionen en todos los límites asincrónicos (no solo promesas), me tomaré unas largas vacaciones y dejaré de trabajar en el núcleo del nodo.

Hasta entonces, las soluciones imperfectas con diferentes compensaciones tendrán que competir.

Honestamente, creo que hay muy poco que podamos hacer aquí porque mirar la pila siempre tendrá una gran penalización de rendimiento. Estoy seguro de que en cuanto haya algo que se pueda hacer, también se hará. Tener una solicitud de función para esto realmente no ayuda mucho. Por lo tanto, tiendo a cerrar esto nuevamente y a eliminar la etiqueta de solicitud de función (además del hecho de que es una pregunta y una solicitud de función en este momento).
@AndreasMadsen ¿qué te parece?

@BridgeAR, como yo lo entiendo, mirar la pila no es realmente la parte costosa, es traducir las direcciones a sitios de llamada de JavaScript. Entonces, en teoría, si hay una estructura de datos inmutable para eso, entonces solo se vuelve costoso cuando se imprime la pila. Pero tal estructura de datos es poco probable con un tiempo de ejecución JIT.

Esta solicitud de función definitivamente está en V8 para implementar, el nodo ya tiene los ganchos para DevTools, por lo que nuestro lado no es el problema.

Estoy de acuerdo con cerrar esto si hay un problema con la v8.

CC @ nodejs / v8

@AndreasMadsen Async stack trace ya está disponible a través de Dev Tools / V8 Inspector después de https://github.com/nodejs/node/pull/13870 (que integra async_hooks con la integración de Inspector).

El seguimiento de pila asíncrono ya está disponible a través de Dev Tools / V8 Inspector después de # 13870 (que integra async_hooks con la integración de Inspector).

Sí, eso es lo que dije.

node ya tiene los ganchos para DevTools, por lo que nuestro lado no es el problema.

No hay nada más que podamos hacer ahora.

Bueno, me parece que las fibras son una verdadera solución al problema, lo que me dice que obviamente se puede resolver. Ahora bien, eso no es del todo justo porque las fibras son una solución mucho más grande que hace cosas que ustedes obviamente han elegido no incluir en el núcleo del nodo.

Lo que todavía no me queda claro es que si hay algo en el lado de V8 que puedan implementar, eso mejoraría las cosas para Node, ya sea en el núcleo del nodo o en el espacio del usuario. Si hay cosas en V8 para las que hay algún plan razonable, yo diría que lo mantendría abierto hasta entonces (siempre que haya un plazo razonable). Pero dado lo que veo en las fibras hasta ahora, no me quejaré si quieres cerrar esto :-).

En cualquier caso, creo que hay espacio para alguna documentación o introducción del equipo de Node. He sido desarrollador de Java durante muchos años y hace un año me cambié a un equipo que usaba Node y decidí intentarlo. Lo odiaba y las dos razones principales eran el infierno de la devolución de llamada y los rastros de pila inútiles (lo que debería decirle cuán fuertemente me sentía sobre este problema). Esos dos, junto con mi disgusto por la naturaleza débil de Javascript, casi me convirtieron en un oponente activo de Node. Afortunadamente, finalmente descubrí soluciones a todas mis quejas. Las promesas ayudan un poco y los generators / async / await / fiber son soluciones totales para el infierno de devolución de llamada. Typecript es una solución del 90% a mi problema de mecanografía y, hasta ahora, encontré que los generadores eran una solución del 75% al ​​problema de seguimiento de la pila (pero no async / await, por eso comencé este problema).

Así que pasé de ser casi un oponente activo a tal vez no ser un fanático, pero tener una tendencia en esa dirección. Pero me tomó un año entero pasar por ese proceso. Trabajaba con personas que habían estado usando Node durante años y apenas conocían las promesas y nada sobre generadores y ciertamente no sobre fibras. Si observa la guía de introducción en la página de inicio principal de Node, no se menciona ninguna de estas tecnologías adicionales y cada uno de los ejemplos muestra el uso de devoluciones de llamada.

Quizás debería considerar algún tipo de artículo sobre "Nodo para desarrolladores de Java" que explique las trampas y las posibles soluciones que alguien como yo puede tener. Supongo que están adoptando un enfoque bastante agnóstico para decirles a los usuarios qué hacer con Node y no quieren elegir lados o recomendar módulos externos o bibliotecas, pero creo que al no hacerlo, dejan a mucha gente a la suerte. de Internet para encontrar soluciones a problemas realmente comunes.

Mirar la pila no es costoso, así como la resolución podría no ser costosa si almacenamos en caché los marcos simbolizados correctamente (como se midió en la naturaleza, no hay muchos marcos de llamada únicos). Actualmente podemos acelerar la búsqueda de caché, creo que 2 veces si eliminamos el soporte de Function.prototype.displayName de V8. No estoy seguro de cuán útil es esta función: displayName permite cambiar el nombre de la función en pilas desde el tiempo de ejecución de JS. [1] O al menos podemos limitar esta característica de que displayName se puede cambiar solo una vez antes de la primera llamada de esta función.

AFAIR, la pena por promise.prototype.then fue alrededor de 3-4 veces más lenta, principalmente porque promise.prototype.then es súper rápido por sí mismo. Para Chromium setTImeout, setInterval y requestAnimationFrame async stack habilitado / deshabilitado casi no observable desde el punto de vista del rendimiento. Puedo comprobar los números.

Otro punto es que en el mundo donde usamos funciones asíncronas en lugar de promesas, las pilas asíncronas potencialmente pueden ser muy rápidas. Dado que es suficiente almacenar solo un puntero de un generador a otro para reconstruir el seguimiento de pila asíncrono.

[1] https://cs.chromium.org/chromium/src/v8/src/isolate.cc?rcl=5ec87e41b4b216167e449f2bf477459e93d72907&l=686

@ ak239 Un problema que tenemos con async_hooks y los seguimientos de pila asíncronos es que Error.prepareStackTrace se llama de forma perezosa, por lo que cuando se llama Error.prepareStackTrace el contexto asíncrono es el contexto de error.stack y no new Error() . ¿Sería posible recibir una notificación cuando se crea un error y de manera similar cuando se llama a Error.captureStackTrace ?

En realidad, no necesitamos crear la cadena de error cuando se crea un nuevo error, solo necesitamos recibir una notificación para establecer el contexto.

Esta no será una solución rápida, pero es una muy buena solución cuando el rendimiento no es un problema.

_Ver https://github.com/AndreasMadsen/trace/issues/34 para ver la discusión original.
_ / cc @fasterthanlime_

Una nota que haría sobre el rendimiento es que, idealmente, el sistema en funcionamiento normal no tendría un impacto notable en el rendimiento. Si la construcción de los seguimientos de la pila es lenta, pero solo cuando ocurre un error, diría que es bastante aceptable. En el mundo de Java se nos enseña que las excepciones son costosas y solo debe usarlas en circunstancias excepcionales y no como un medio de flujo de control normal.

Vine aquí buscando una manera de mostrar los seguimientos de pila adecuados cuando uso async-await en node.js.

Hasta que finalice esta función, el uso de promesas de Bluebird con BLUEBIRD_LONG_STACK_TRACES=1 puede servir como solución alternativa.

@ejoebstl que no ayuda para este problema en particular. Los seguimientos de pila larga ayudan si utiliza promesas sin procesar. Con await la pila se sorprende cada vez; vea mi problema https://github.com/tgriesser/knex/issues/2500, por ejemplo

Hola. Creo que he resuelto esto en el seguimiento PR # 40 . Me encantaría cualquier comentario o prueba que alguien pudiera dar este código :)

Puedes probarlo instalándolo desde mi repositorio:

npm install 'git://github.com/jbunton-atlassian/trace#a4e78d89e09ab4830f302bf85a30a528a5746e0b'

@capaj Depende. Si usa, por ejemplo, babel o babel-node con transform-async-to-promises , funciona. Mucha gente parece usar esta configuración.

En mi caso específico, lo estaba usando para pruebas unitarias y era justo lo que necesitaba. Estoy de acuerdo en que esto es un truco y que el problema subyacente debe solucionarse lo antes posible.

Experimenté el mismo problema, ¿cómo puedo ayudar?

Realmente no necesito una pila completa, realmente solo necesito el maldito número de línea para el error. Esta debería ser una funcionalidad básica para cualquier herramienta de codificación. ¿Cómo no funciona esto en el nodo?

@shkkmo Falta tu descripción, pero parece un problema diferente. Presenta un nuevo error si no tienes un caso de prueba de un departamento de terceros.

Con respecto a este tema, creo que deberíamos cerrarlo. Ha sido discutido por personas conocedoras y si el consenso es que hay buenas razones por las que las cosas son como son, entonces no tiene sentido mantenerlo abierto.

Cerrado por las razones mencionadas.

Hm. Estoy muy contento de que este tema haya sido discutido por personas conocedoras, pero agradecería un poco más de información de antecedentes. ¿Cuál es la forma propuesta de obtener el seguimiento de la pila en este caso?

Sí, este es un gran dolor de cabeza para los usuarios finales. Para aquellos de nosotros que queremos usar async / await según lo previsto, con promesas nativas, esto requiere que hagamos uso de soluciones alternativas o hacks para obtener información útil de depuración.

¿Quién tiene la intención de hacerse cargo de esto ahora? ¿Se supone que es un problema de v8? Si es así, un enlace a un lugar relevante para hacer un seguimiento y rastrear esto sería increíble. Si la solución es "no hay solución, use las bibliotecas de rastreo Bluebird / userland / vuelva al infierno de devolución de llamada" bueno ... sería realmente bueno saberlo antes de construir más proyectos en Node.

Sé que los chicos de Node.js quieren mantener fuera del núcleo todo lo que se puede hacer en el área de usuario. Pero para mí, generar seguimientos de pila parece una característica lo suficientemente importante como para hacer una excepción.

@ aleksey-bykov Ya recibió una advertencia y una eliminación más arriba en este hilo. No dejes que vuelva a suceder.

Quiero agregar un último reconocimiento a la solución mencionada anteriormente que ha funcionado para mí. He estado usando fibras Node y la biblioteca f-promise por un tiempo y ha sido genial. Es efectivamente una solución a este problema y la recomiendo encarecidamente.

@TazmanianDI bueno, podría ser una buena solución para un nuevo proyecto, pero para las bases de código existentes, 'reescribir usando una nueva biblioteca' no suena demasiado atractivo.

Estoy usando trace para depurar. Para la producción, la penalización del rendimiento es demasiado grande, pero le brinda trazas de pila razonables. Funciona con promesas nativas.

(Pero como escribí anteriormente, este es el tipo de funcionalidad que esperaría de la plataforma, ya sea V8 o Node.js.)

https://mathiasbynens.be/notes/async-stack-traces
Podría malinterpretar esto, pero parece un desarrollador de núcleo v8 que explica que los seguimientos de pila asíncronos de rendimiento son posibles.

@Janpot , ¡sería genial si se

La biblioteca trace es una gran solución para el usuario, pero la mayoría de nuestros errores ocurren en entornos de producción ... Supongo que eso significa que la compensación es el retraso en el rendimiento frente a saber dónde están ocurriendo realmente nuestros errores. Esa no es realmente una gran compensación para tener que hacer la OMI.

(en una nota relacionada, los seguimientos de pila incorrectos en realidad invalidan muchas de nuestras herramientas de seguimiento de errores; los errores de Stackdriver, por ejemplo, agrupan varios errores diferentes bajo el mismo banner porque el sitio throw es el mismo)

Actualmente no tenemos planes de cambiar la forma en que Error.stack funciona en V8. Todo lo que dice la publicación del blog de Mathias es que el seguimiento de los seguimientos de pila asíncronos para asíncrono puro / espera se puede hacer mucho más barato que Promesas en general. Sin embargo, crear un objeto Error ya es caro. Seguir la cadena de funciones asincrónicas suspendidas lo hace aún más caro.

Consideraría experimentar con una implementación en algún momento, pero sin promesas. E incluso si lo tuviera al final de este trimestre, no lo obtendría hasta Node.js 10. Además, las contribuciones siempre son bienvenidas.

@ ak1394

@TazmanianDI bueno, podría ser una buena solución para un nuevo proyecto, pero para las bases de código existentes, 'reescribir usando una nueva biblioteca' no suena demasiado atractivo.

Sí, necesitaría agregar una nueva biblioteca, pero si su código base ya está basado en async/await , la conversión es muy fácil. Es más o menos reemplazar await x con wait(x) y async function con solo function . Lo mismo ocurre con los generadores. Es casi una diferencia de sintaxis; no debería tener que hacer ninguna reescritura significativa. También tendrá que agregar algunas llamadas run en sus puntos de entrada (donde comienzan las fibras).

Si su código se basa en promesas, es un poco más de trabajo, pero básicamente significa simplemente colapsar todas sus llamadas then/catch y hacerlas en línea (algo que tendría que hacer para cambiar a async/await todos modos).

@TazmanianD para bibliotecas, eso simplemente no es sostenible: una solución que solo funciona para el código de la aplicación realmente no es una solución viable.

Creo que para muchas aplicaciones, el rastreo es una opción viable incluso en producción.

En particular, me encantaría cualquier comentario / prueba sobre mis mejoras, consulte mi comentario anterior para obtener más detalles.

Editar: Tenga en cuenta que mis mejoras para rastrear incluyen la reparación de la pérdida de memoria que @ foray1010 menciona a continuación.

Dudo que el uso de trace en producción
vea mi comentario sobre la fuga de memoria de trace en https://github.com/AndreasMadsen/trace/issues/35#issuecomment -361838159

Sí, necesitaría agregar una nueva biblioteca, pero si su código base ya está basado en async / await, la conversión es muy fácil. Es más o menos reemplazar await x con wait (x) y la función async con solo function. Lo mismo ocurre con los generadores. Es casi una diferencia de sintaxis; no debería tener que hacer ninguna reescritura significativa. También tendrá que agregar algunas llamadas de ejecución en sus puntos de entrada (donde comienzan las fibras).

Tendrás que perdonar mi escepticismo @TazmanianD, pero nada de eso parece fácil ... suena más como una reescritura total de una base de código, en realidad, junto con pruebas de extremo a extremo para garantizar que no haya cambios inesperados en el comportamiento. Definitivamente un enfoque que consideraría para un proyecto nuevo, pero no para un código base existente en producción.

Este hilo tenía una pregunta importante que se ahogó y nunca se respondió (afaict):

@ ak239 Un problema que tenemos con async_hooks y async stack traces es que Error.prepareStackTrace se llama de forma perezosa, por lo que cuando se llama Error.prepareStackTrace, el contexto async es el contexto de error.stack y no el nuevo Error (). ¿Sería posible recibir una notificación cuando se crea un error y de manera similar cuando se llama a Error.captureStackTrace?

Si tal devolución de llamada existiera en el nodo, parece que el seguimiento podría funcionar incluso con código asincrónico / en espera. Sin tal devolución de llamada, no hay nada más que @AndreasMadsen pueda hacer en seguimiento .

Me retiro de este hilo y recomendaría a las personas que no desarrollan el núcleo del nodo que hagan lo mismo (consulte https://github.com/nodejs/node/issues/11865#issuecomment-359298802 para obtener una solución alternativa, con un impacto en el rendimiento), pero me gustaría ver una respuesta del equipo (en algún momento) sobre si esa devolución de llamada es factible y si están interesados ​​en agregarla.

TL; DR: El problema no se ha resuelto, se debe reabrir y restringir a los colaboradores para evitar ruidos.

Error.prepareStackTrace se llama de forma perezosa, por lo que cuando se llama Error.prepareStackTrace, el contexto asíncrono es el contexto de error.stack y no el nuevo Error (). ¿Sería posible recibir una notificación cuando se crea un error y de manera similar cuando se llama a Error.captureStackTrace?

@fasterthanlime Eso no haría una diferencia práctica, si entiendo su pregunta correctamente: mientras que el seguimiento de la pila se formatea de forma perezosa, se captura con entusiasmo.

@fasterthanlime Eso no haría una diferencia práctica, si entiendo su pregunta correctamente: mientras que el seguimiento de la pila se formatea de forma perezosa, se captura con entusiasmo.

Si entendí a @AndreasMadsen correctamente, el quid del problema es que el seguimiento de la pila se captura cuando se produce un error, en lugar de cuando se crea. Tener una devolución de llamada cuando se crea el error permitiría que el seguimiento use otro seguimiento de pila capturado en new Error() lugar del que tiene actualmente, capturado en el lanzamiento. (¡Puedo estar completamente equivocado!)

La lectura de las primeras publicaciones de https://github.com/AndreasMadsen/trace/issues/34 debería dar algo de contexto.

Vale la pena señalar que la propuesta de seguimiento de pila de la etapa 1 requerirá que el seguimiento se capture cuando se cree el error; tirarlo es irrelevante.

Sip. El contenido de Error.stack se captura en la creación del error, pero se formatea de forma perezosa.

@fasterthanlime Correcto. Es imposible dar un seguimiento de pila largo sin conocer el contexto asincrónico en la creación del error. Para obtener el contexto asincrónico en la creación del error, se necesita un enlace compatible con V8 para la creación de errores.

@hashseed ^ Mencioné esto en el Grupo de trabajo de diagnóstico, pero nunca pude abrir un informe de error de V8.

Esta puede ser una pregunta totalmente ignorante dado que no sé nada de los detalles de implementación, pero ¿hay alguna lección que aprender de las fibras de nodo? Ese complemento parece contener una solución real a este problema y una que no parece tener ningún impacto significativo en el rendimiento. Es un complemento nativo y no sé si eso altera su utilidad aquí.

Parece haber cierto debate sobre si es posible o no una solución cuando me parece que, de hecho, ya existe una solución, pero que requiere un complemento.

@hashseed Abrí una solicitud de función: https://bugs.chromium.org/p/v8/issues/detail?id=7525

@TazmanianDI node-

@bnoordhuis

@TazmanianDI node-

¿Qué te hace decir eso? Esto parecería argumentar que no es tan pesado: https://groups.google.com/forum/#!topic/nodejs/3dbidW8fTYw.

Hice una prueba de rendimiento rápida y sucia antes de cambiar a las fibras (concedido que no miré el uso de la memoria) y encontré que la diferencia de rendimiento entre las fibras y async / await, así como las promesas, era insignificante, pero tal vez mi prueba fue una mierda . Si tiene un alto costo, entonces me sentiría un poco alarmado ya que el rendimiento es una razón por la que no elegí las otras opciones mencionadas anteriormente, como longjohn, que no desea ejecutar en producción.

Oh, lo recuerdo mal: no era el contexto por fibra el problema, tenía que ver con el tamaño de la pila y las fibras como referencias débiles.

Hace unos años ayudé a Meteor a depurar un problema de rendimiento que resultó ser una acumulación de fibras que provocaba que el recolector de basura se fundiera. Las referencias débiles necesitan un manejo especial en el recolector de basura; no puede tener demasiados sin implicaciones de rendimiento.

Eso no es para desanimarte, si las fibras funcionan para ti, genial, úsalas, pero no es una solución de uso general.

@bnoordhuis

Aquí está el código que asigna un Fiber : https://github.com/laverdet/node-fibers/blob/master/src/fibers.cc#L495
No crea un nuevo contexto; utiliza el contexto actual.

Además, la asignación de pilas es bastante inteligente, con mmap . Ver https://github.com/laverdet/node-fibers/blob/master/src/libcoro/coro.c#L653

Ahora, considere un patrón de pilas profundas de llamadas asincrónicas (async f1 en espera en async f2 en espera en async f3, ...) con el estado (variables locales) que deben rastrearse en cada función. ¿Cómo se mantiene el estado? Con fibras se mantiene directamente en la pila de fibras. Con promesas, se mantiene en cierres. No estoy seguro de quién gana.

En mi experiencia, las fibras prosperan con grandes cantidades de llamadas asíncronas.

Hay trampas, por supuesto, y debe tener cuidado de reducir el nivel de paralelismo (porque, a pesar de lo que dije anteriormente, un objeto de fibra requiere más memoria que una promesa), pero esto no siempre es algo malo (alguna vez se queda sin archivo descriptores ?, o generó un DDOS contra su base de datos?).

Las fibras son gorutinas del nodo. ¿Por qué las corrutinas apiladas serían la norma en marcha, pero inaceptables en JavaScript? Y probablemente las fibras se podrían hacer más rápido si se integraran al lenguaje, en lugar de implementarlas como un complemento.

@bjouhier Con respecto a la discusión de la fibra: si bien es definitivamente una tecnología interesante y útil, pasar a las fibras no es

@hashseed Estaba experimentando atravesando los generadores de funciones asíncronas para pilas asíncronas y creo que puedo encontrar algún trabajo relacionado en mi máquina y subirlo a la herramienta de revisión de código.

Tengo una pregunta más genérica. Creo que nuestras pilas asíncronas en DevTools son lo suficientemente buenas para muchos usuarios y dado que, en cualquier caso, incluimos este código como parte del inspector dentro de V8, ¿podríamos agregar alguna API de V8 que habilite nuestras pilas asíncronas y las incluya en Error? apilar. Como siguiente paso, podemos agregar un módulo nativo, que se llame 'devtools-async-stacks', que simplemente llamará a esta API. El usuario podría requerir este módulo usando el indicador -r con el nodo para obtener pilas asíncronas de devtools en la terminal. Sería casi cero código en el lado del nodo y solo expondrá la característica que ya tenemos en V8.

@moberemk Bueno, el tema de las fibras fue traído nuevamente a la discusión por @TazmanianDI , el OP de este número. Y las fibras realmente resuelven su problema.

Respondí porque la @bnoordhuis era engañosa y ha habido mucha negatividad sobre las fibras en el pasado. Gracias Ben por corregirlo.

Con respecto a las trazas de pila con async / await, tal vez pueda compartir mi experiencia con streamline.js, que es básicamente una implementación temprana del modelo async / await.

Streamline proporciona seguimientos de pila largos, incluso cuando el código se transpila a devoluciones de llamada. En realidad, proporciona dos stacktraces en este modo: el stacktrace _raw_ que V8 da por defecto + un stacktrace _async_ que contiene la pila de llamadas en espera. Me pareció importante proporcionar ambos y no golpear la pila sin procesar.

El seguimiento de pila asíncrono se construye presionando un pequeño objeto _frame_ que contiene el nombre de archivo + el número de línea en una pila justo antes de la llamada await y haciéndolo estallar cuando la llamada await regresa (en un turno posterior del evento lazo). Esta pila de objetos marco se gestiona como una simple lista enlazada. El puntero a la parte superior de esta pila se rastrea en el cierre de la función asíncrona.

Como streamline es un transpilador, esto se implementó emitiendo código adicional en cada función asincrónica. Para async / await, debería ser posible implementar algo similar en el compilador V8. Desafortunadamente, esto introduce gastos generales en todas las llamadas await , pero estos gastos generales son menores que llamar a Error.captureStacktrace . Desafortunadamente, esto no se puede rellenar (¿cómo obtendría el relleno múltiple __filename y el número de línea?)

Streamline admite 3 modos de transpilación: devoluciones de llamada puras, generadores y fibras. En los dos primeros modos, los errores llevan los dos rastros de pila. En el modo de fibras, el seguimiento de pila sin procesar es suficiente y no hay gastos generales para obtenerlo.

@hashseed Estaba experimentando atravesando los generadores de funciones asíncronas para pilas asíncronas y creo que puedo encontrar algún trabajo relacionado en mi máquina y subirlo a la herramienta de revisión de código.

Eso suena como un plan. Pero creo que, en general, deberíamos unificar la forma en que almacenamos los seguimientos de pila. El del objeto Error y el que usamos para los seguimientos de pila asíncronos son similares, pero diferentes. Sería genial si pudiéramos compartir más de la implementación.

¿Alguien tiene ganas de escribir una pequeña biblioteca que implementaría async/await usando https://github.com/laverdet/node-fibers? Intentaré escribir algo usando Futures del mismo proyecto, pero no será tan efectivo. No entiendo cómo funcionan las fibras, de lo contrario, lo habría hecho yo mismo.

Esta idea está lejos de ser una solución sexy, pero no tuve mucha suerte ni siquiera con las fibras.

Básicamente, envolvemos una promesa en catchMe() proporcionando un método de fábrica que creará un new Error() con el uso. Eso es necesario o todas las llamadas catchMe() terminarán creando un montón de errores solo para agotar la memoria.

const me = require('./catch-me').catchMe

await me(() => Error(), iAmReturningPromise())

catch-me.js

async function catchMe(errorFactory, promise) {
  try {
    return await promise
  } catch (e) {
    e.stack += errorFactory().stack
    throw e
  }
}

module.exports = catchMe

Espero que alguien me ayude a crear un método menos detallado ...

@zatziky async /

Si desea probar las fibras, le sugiero que comience con algo como f-promise y que se olvide temporalmente de async / await. Una vez que se sienta más cómodo, puede comenzar a mezclarlos (pero perderá trazas de pila completas en los bits asíncronos / en espera).

@zatziky En lugar de concatenar las pilas como lo hace, debería probar la biblioteca verror . Básicamente es un error anidado que le permite mantener la totalidad de cada error, incluidos los detalles que puedan estar en ellos. Esa era la solución que estábamos usando cuando usábamos generadores, pero esa solución dejó de funcionar completamente con async / await, que es precisamente para lo que creé este problema originalmente para abordar.

Usaremos un pequeño módulo en nuestros proyectos. Básicamente abarca la solución que propuse anteriormente:
https://github.com/amio-io/await-trace

Así es como puede refactorizar rápidamente el código:
image

@TazmanianDI Me quedé con el nodo Error lugar de verror porque habría creado 2 objetos de error en lugar de solo 1. Pero me gusta esa biblioteca verror . 👍

¿Alguien puede recomendar un problema a seguir para que pueda recibir una notificación cuando se solucione el problema mencionado en el título del OP?

Volveré a la implementación nativa de Promise una vez que se solucione este problema, pero en mi caso, encontrar rápidamente la línea de error que rompe mi código y / o pruebas unitarias tiene prioridad sobre el rendimiento.

FWIW, incluso obtener la última línea de código que no sea de biblioteca sería útil

Todavía no se corrigió en el nodo 10. :(

@fgarcia La mejor solución actual es @AndreasMadsen 's traza , que a veces no funciona. El trabajo de Andreas en esto es impresionante, pero parece que no hay forma de solucionar algunos casos sin la ayuda de v8, así que lo que querrás seguir es su problema con el rastreador de v8 .

/ cc @ nodejs / v8 @ nodejs / diagnostics _Lamento enviarle un mensaje sobre un tema tan candente, pero sería genial si pudiéramos obtener algún progreso en https://bugs.chromium.org/p/v8/issues/detail? id = 7525. Hablamos de ello en la Cumbre de Diagnóstico de Ottawa y nuestros amigos de V8 me dijeron que abriera una edición. Sin embargo, en realidad no pasa nada.

Dado que la propuesta de pilas de errores requerirá que la pila se llene por completo y probablemente se congele en el momento de la creación del objeto de error, no creo que el problema de la v8 progrese mucho.

@ljharb que no necesariamente ayudará a resolver esto. Sin un gancho ejecutado en la creación de seguimientos de pila, no hay forma de rastrear su contexto de ejecución asíncrona.

Si v8 implementara su propuesta usando Error.prepareStack eso debería ser suficiente (ya que puede ser parcheado), pero de acuerdo con este problema , quieren eliminar esa función.

Supongo que una mejor solución sería tener una forma estándar de conectarse a la creación de seguimientos de la pila, y usar API específicas de la plataforma para enriquecerlas con información de ejecución asíncrona.

La especificación no permitiría un punto de inyección parcheable para monos.

El motor, por supuesto, podría inyectar cualquier información que quisiera en el momento de la creación del objeto de error.

@ljharb ¿Puedes vincularme a qué propuesta es esa? Pero incluso si el error estuviera "completamente poblado y probablemente congelado", estaría bien. Podríamos usar un WeakMap .

Para que quede claro esto es lo que me gustaría:

diff --git a/src/messages.cc b/src/messages.cc
index 1800b1ef71..494e717b2f 100644
--- a/src/messages.cc
+++ b/src/messages.cc
@@ -1138,6 +1138,24 @@ MaybeHandle<Object> ErrorUtils::Construct(
                       isolate->CaptureAndSetSimpleStackTrace(err, mode, caller),
                       Object);

+  // If there's a user-specified "Error.constructorHook" function, call it.
+  Handle<JSFunction> global_error = isolate->error_function();
+  Handle<Object> constructor_hook;
+  ASSIGN_RETURN_ON_EXCEPTION(
+      isolate, constructor_hook,
+      JSFunction::GetProperty(isolate, global_error, "constructorHook"),
+      Object);
+
+  if (constructor_hook->IsJSFunction()) {
+    const int argc = 1;
+    ScopedVector<Handle<Object>> argv(argc);
+    argv[0] = err;
+
+    RETURN_ON_EXCEPTION(isolate,
+      Execution::Call(isolate, constructor_hook, global_error, argc, argv.start()),
+      Object);
+  }
+
   return err;
 }

Entonces podemos implementar:

const errorCreationContext = new WeakMap();
Error.constructorHook = function (err) {
  errorCreationContext.set(err, async_hooks.executionAsyncId);
}

Un punto importante que puede explicar la dificultad aquí: el seguimiento de pila útil es la solo el compilador V8 ve las palabras clave de espera .

El parche de mono Error o Promise no resolverá realmente el problema porque no podrá distinguir entre Errors / Promises que fluyen a través de una espera call y Errors / Promises que se originan / fluyen en otra parte.

En resumen, una solución adecuada requiere superpoderes : ya sea ajustar el compilador o transplilar (para que pueda ajustar las palabras clave de espera ), o ajustar el tiempo de ejecución (con fibras).

@bjouhier solo para aclarar, estamos hablando de mono parcheando algunas propiedades específicas de v8 de Error , no reemplazándolo. Hacer eso te da los superpoderes a los que te refieres.

@alcuadrado y la especificación propuesta prohibirán activamente este parche, por lo que no es una estrategia viable a largo plazo.

@ljharb La propuesta solo menciona la congelación de objetos en GetStack , GetStackFrames , FromStackFrame , FromStackFrameSpan , FromStackFramePosition . Todos ellos congelan nuevos objetos.

No veo por qué no podría parchear Error.prepareStackTrace . Tenga en cuenta que es una propiedad de Error , no de las instancias de Error .

¿Me estoy perdiendo de algo? ¿Cómo debería ser una solución a largo plazo en su opinión?

@alcuadrado el texto no está finalizado, pero incluiría completar la ranura [[ErrorData]] en el constructor, y prohibiría devolver diferentes resultados para la misma ranura, es decir, una vez observado, la pila es inmutable.

@ljharb Ya veo, gracias por la explicación.

Una solución a largo plazo para mostrar algo similar a un seguimiento de pila asíncrono no debería asignar nada al error.stack real, pero mantener sus datos separados, ¿verdad?

Creo que todavía se necesita un gancho en la creación de errores, ya que uno tendría que realizar un seguimiento del contexto de ejecución asincrónica en ese momento.

@alcuadrado Todavía estoy desconcertado porque no veo cómo la API de error le permitiría distinguir entre los errores que se propagan a través de llamadas await de los errores que se propagan a través de llamadas que no están en espera (pero tal vez no profundicé suficientemente profundo).

Permítanme tomar 2 ejemplos para ilustrar mi punto, asumiendo fs API prometida.

Primero un caso con await llamadas:

async function f3() { const data = await fs.readFile('invalid path'); return data[0]; }
async function f2() { const r = await f3();  return r + 1; }

async function f1() {
  try { await f2(); } 
  catch (ex) { console.error(ex.stack); } 
}

Aquí, no es de extrañar. Espero ver fs.readFile <- f3 <- f2 <- f1 en el seguimiento de la pila

Ahora, consideremos las llamadas sin await :

function f3() { return fs.readFile('invalid path'); }
function f2(promise) { promise.catch(err => console.error(err.stack)); }

function f1() {
  const promise = f3();
  console.log('doing something else');
  f2(promise);
}

¿Qué debo incluir en el stacktrace?

  • fs.readFile <- f3 <- f1 ? ¿Pila donde se asignó la promesa? Puede ser confuso porque f3 completó normalmente y 'doing something else' se imprimió en la consola. ¿Una pila del pasado, con marcos de pila fantasma?
  • promise.catch <- f2 <- f1 ? ¿Apilar donde se detectó el error? Contraintuitivo porque está en desacuerdo con el clásico try/catch donde la pila está arraigada en el bloque try , no en la cláusula catch . También mucho menos informativo.

En mi opinión, el stacktrace no debe contener f1 , ni f2 , ni f3 en este caso, solo las funciones de bajo nivel que invocaron la devolución de llamada dentro del método catch .

@bjouhier Lo siento, mi explicación estaba incompleta.

Esas API v8 no son suficientes, pero se pueden usar junto con el enlace asíncrono del nodo. La última le brinda la información necesaria para realizar un seguimiento de los diferentes contextos de ejecución asíncrona. Para obtener un excelente ejemplo de cómo hacerlo, consulte Trace .

@alcuadrado Bien pero antes de sumergirnos en el cómo tenemos que saber lo que queremos conseguir. Para esto, sería útil tener una respuesta a la pregunta que planteé anteriormente: qué seguimiento de pila esperaría obtener en el segundo escenario anterior: fs.readFile <- f3 <- f1 , o promise.catch <- f2 <- f1 , o simplemente el crudo ¿apilar?

Adoptaría un enfoque conservador aquí, que es limitar los rastros de pila asíncronos a la pila de marcos suspendidos por un await , porque esto es simple, natural y se puede implementar de manera eficiente (lo hice con streamline.js transpilador y encontró que era suficiente para la mayoría de los propósitos prácticos).

Las soluciones en torno a trace / async-hook parecen más ambiciosas ya que intentan rastrear pilas para _todas_ las devoluciones de llamada de continuación. ¿Es esto realmente útil? ¿natural? ¿Todavía se puede implementar de manera eficiente (CPU pero también memoria)?

Una ventaja de limitar la función a las pilas de llamadas suspendidas por await es que se convierte en una función de _language_ pura. Sin dependencia de una biblioteca de host como async-hook; se puede implementar completamente en V8.

¡Stacktraces asíncronos en el navegador!

@bjouhier Veo tu punto. Hay diferentes enfoques que se pueden tomar. Tener la capacidad de implementarlos en la zona del usuario permite que el usuario elija.

Tener su enfoque a nivel de idioma sería útil desde el principio para muchas personas, pero eso debería proponerse al TC39, no al nodo.

@Trott Todavía hay mucha discusión aquí y parece que se está haciendo algún progreso para eventualmente tener una solución para esto. Creo que sería bueno reabrir este tema.

Sí, pero como mencionó @alcuadrado , lo que propongo debería enviarse a TC39. Puedo publicar en es-discusion si hay interés.

@bjouhier, siéntete libre, pero hasta que la propuesta de pilas progrese, no hay ningún concepto de pilas en el lenguaje para que puedas proponer cambios.

@ljharb ¿No podría ser una propiedad de cadena? Algo como awaitStack . El formato real del seguimiento de la pila se dejaría al motor, como la propiedad actual stack . O un símbolo, para evitar colisiones con propiedades existentes.

@bjouhier creo que estás malentendiendo; en cuanto a la especificación de lenguaje se ocupa actualmente, no hay tal cosa como la traza de la pila, pilas de error, los marcos de pila, etc. La propuesta de fase 1 es el primer intento de estandarizar el concepto - y los relacionados con la pila nada tendría que construir fuera de ese.

@ljharb ¡¡Interesante !!
Nunca verifiqué y pensé que el estándar al menos especificaría que los errores tienen una propiedad de cadena stack , pero no aplica nada más en el formato de pila. ¡Pero ni siquiera llega tan lejos! Error.prototype.stack es solo un estándar de facto.

AQUÍ Aunque no es 100% relevante para el problema de los seguimientos de pila asíncronos, pero cuando desee recordar pilas utilizando devoluciones de llamada, consulte erotic para poder capturar pilas asincrónicas. Tendrá que crear un ancla en el punto donde desea que comience el seguimiento de la pila, pero esa es la única forma de lidiar con él ATM.

Uso:

import { readFile } from 'fs'
import erotic from 'erotic'

const read = async (path) => {
  const er = erotic() // stack has the anchor point

  await new Promise((resolve, reject) => {
    readFile(path, (err, data) => {
      if (err) {
        const e = er(err) // stack also includes this line
        return reject(e)
      }
      return resolve(data)
    })
  })
}

(async function example() {
  const path = 'non-existent-file.txt'
  try {
    await read(path)
  } catch ({ stack }) {
    console.log(stack)
  }
})()
Error: ENOENT: no such file or directory, open 'non-existent-file.txt'
    at ReadFileContext.readFile [as callback] (/Users/zavr/adc/erotic/example/read-file.js:10:19)
    at read (/Users/zavr/adc/erotic/example/read-file.js:5:14)
    at example (/Users/zavr/adc/erotic/example/read-file.js:21:11)
    at Object.<anonymous> (/Users/zavr/adc/erotic/example/read-file.js:25:3)

En lugar de nodos estándar

Error: ENOENT: no such file or directory, open 'non-existent-file.txt'

O puede hacer que arroje errores transparentes (si es un autor de biblioteca):

import { readFile } from 'fs'
import erotic from 'erotic'

const read = async (path) => {
  const er = erotic(true)

  await new Promise((resolve, reject) => {
    readFile(path, (err, data) => {
      if (err) {
        const e = er(err)
        return reject(e)
      }
      return resolve(data)
    })
  })
}

(async function example() {
  const path = 'non-existent-file.txt'
  try {
    await read(path) // error appears to be thrown here
  } catch ({ stack }) {
    console.log(stack)
  }
})()
Error: ENOENT: no such file or directory, open 'non-existent-file.txt'
    at example (/Users/zavr/adc/erotic/example/transparent.js:21:11)
    at Object.<anonymous> (/Users/zavr/adc/erotic/example/transparent.js:25:3)

¿Por qué está cerrado este problema?

¿Existe una resolución? Encontré dos recomendaciones que no requieren cambiar el código (longjohn y trace), ninguna de ellas funciona, el resto (fibra, f-promises, erotic) todas requieren que modifiques tu código.

¿Por qué está cerrado este problema?

Por inactividad. @Trott mencionó querer saber si la gente no está de acuerdo (¿presumiblemente para volver a abrirlo si ese fuera el caso?). Estoy bastante seguro de que un buen número de personas no está de acuerdo :)

Un buen número de personas está en desacuerdo, pero actualmente este problema parece estar en la corriente del equipo V8 / tc39. He estado siguiendo esto por un tiempo y no parece haber impulso para arreglar esto desde el lado del nodo; si me equivoco en eso, puede valer la pena que un colaborador principal lo indique y limite la discusión solo a los colaboradores.

@Trott , vuelva a abrir.

Realmente espero que se encuentre una solución antes de que async / await se propague, Internet ya está lleno de "por qué async / await es mucho mejor que las promesas", depurar errores que no tienen indicadores de dónde provienen es un tipo especial de infierno.

Estamos considerando si ofrecer de forma nativa seguimientos de pila larga para async await. Sin embargo, nada concreto en este momento.

Actualmente no hay nada que se pueda hacer en Node.js para mejorar la situación. No creo que debamos reabrir el tema mientras ese sea el caso.

@hashseed, tal vez alguna discusión pueda ir a https://github.com/tc39/proposal-error-stacks sobre esto. un Error.prototype.trace que representa la pila para sincronización o la pila combinada / etc. para asincrónica podría ser una dirección positiva.

no se agregarían cosas nuevas a Error.prototype para esto; tendría que ir en algo como System, junto con getErrorStack y amigos. Sin embargo, abra algo para debatir sobre el repositorio.

Acabo de leer todo ese hilo largo y todavía no entiendo una cosa básica: ¿cuándo / por qué Node decidió que los seguimientos de pila son responsabilidad del usuario? A lo largo de la mayor parte del historial de programación, los seguimientos de pila han sido responsabilidad del lenguaje central.

No estoy diciendo que no deberían ser responsabilidad del usuario, es solo que en todo lo anterior no pude encontrar una sola publicación que dijera "es por eso que estamos haciendo algo que ningún otro idioma hace, y lo estamos haciendo su responsabilidad de utilizar una biblioteca de terceros si desea rastreos de pila precisos, en lugar de proporcionar esa funcionalidad nosotros mismos ".

Si alguien pudiera aclarar este punto, lo encontraría muy útil.

@machineghost no es responsabilidad del usuario, es responsabilidad del motor. en este caso, V8 ya ha comenzado a trabajar en seguimientos de pila asíncronos y, en algún momento, el nodo se enviará con una versión de V8 donde esté habilitado.

@devsnek Muchas gracias por esa aclaración. No fue obvio para mí por el hilo anterior, ya que algunos mantenedores parecían estar diciendo lo contrario, pero su respuesta tiene mucho más sentido.

¡V8 ahora ofrece una bandera --async-stack-traces ! Ver: https://github.com/v8/v8.dev/pull/49/files#diff -af1399a75d6310211a72be0dc2571ea5R44

No estoy seguro de si algún nodo admite esta nueva capacidad V8.

Versión actual (nodo 11) recién actualizada a v8 7.0; Parece que los seguimientos de pila asíncronos vienen en 7.1 si estoy leyendo esto bien, así que espero que llegue pronto.

Esta característica de V8 todavía es experimental y depende de un cambio de especificación a un detalle particular de async / await. Tampoco puede reconstruir el seguimiento de la pila completa en algunos casos. Pero es mejor que nada. Espero que puedas usarlo en Node 12, pero probablemente ya puedas comprobarlo con Node Canary.

Acabo de confirmar que la versión del nodo en https://nodejs.org/download/v8-canary/v12.0.0-v8-canary20181217e2f11a7b92/ admite la bandera --async-stack-traces mencionada en el artículo del blog v8.

@eliranamar
Rastreos de pila asíncronos @ 12.0

¿Se requiere --async-stack-traces en v12?

@gajus no. Esos se rastrean de forma predeterminada.

¿Se supone que los seguimientos de pila asíncronos funcionan dentro de los bloques de captura? ¿O depende del código intermedio tener una cadena asíncrona? Faltan para mí con un código como este en el Nodo 12.16.2 al menos ... (connection.query involucra emisores de eventos debajo del capó)

Tenía la esperanza de que el new Promise creado dentro del método garantizara que el seguimiento de la pila asíncrona se agregue a cualquier error de rechazo.

async query(sql, parameters) {
  try {
    await new Promise((resolve, reject) => connection.query(sql, (error, result) => error ? reject(error) : resolve(result)));
  } catch (err) {
    // None of these include the async stack traces...
    console.error(err.stack)
    console.error(new Error('Test').stack)
    throw new Error('Test')
  }
  // but if we get here than this error does get the async stack trace
  throw new Error('Test')
}

@TazmanianDI Edward j

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

filipesilvaa picture filipesilvaa  ·  3Comentarios

seishun picture seishun  ·  3Comentarios

Brekmister picture Brekmister  ·  3Comentarios

stevenvachon picture stevenvachon  ·  3Comentarios

willnwhite picture willnwhite  ·  3Comentarios