tutorial start script listening installed for failed extension debugger debug chrome bookmark debugging validation

debugging - script - phpstorm start listening for php debug connections



¿Cómo abordas los errores intermitentes? (18)

Escenario específico

Aunque no quiero concentrarme solo en el problema que estoy teniendo, aquí hay algunos detalles del problema actual que enfrentamos y cómo lo he abordado hasta ahora.

El problema se produce cuando el usuario interactúa con la interfaz de usuario (un TabControl para ser exactos) en una fase particular de un proceso. No siempre ocurre y creo que es porque la ventana de tiempo para que el problema sea exhibido es pequeña. Mi sospecha es que la inicialización de un UserControl (estamos en .NET, usando C #) coincide con un evento de cambio de estado desde otra área de la aplicación, lo que lleva a que se elimine una fuente. Mientras tanto, otro control (una etiqueta) intenta dibujar su cadena con esa fuente y, por lo tanto, el bloqueo.

Sin embargo, la confirmación de lo que conduce a la disposición de la fuente ha resultado difícil. La solución actual ha sido clonar la fuente para que la etiqueta del dibujo aún tenga una fuente válida, pero esto realmente enmascara el problema de raíz que es la fuente que se está eliminando en primer lugar. Obviamente, me gustaría rastrear la secuencia completa, pero eso está resultando muy difícil y el tiempo es corto.

Enfoque

Mi enfoque fue primero mirar el seguimiento de la pila de nuestros informes de fallas y examinar el código de Microsoft usando Reflector. Desafortunadamente, esto condujo a una llamada GDI + con poca documentación, que solo devuelve un número del error: .NET lo convierte en un mensaje bastante inútil que indica que algo no es válido. Estupendo.

A partir de ahí, fui a ver qué llamada en nuestro código lleva a este problema. La pila comienza con un bucle de mensaje, no en nuestro código, pero encontré una llamada a Update () en el área general bajo sospecha y, usando instrumentación (rastreos, etc.), pudimos confirmar con un 75% de certeza que esto fue la fuente del mensaje de pintura. Sin embargo, no fue la fuente del error: pedirle a la etiqueta que pinte no es un crimen.

A partir de ahí, miré cada aspecto de la llamada de pintura que se estaba rompiendo (DrawString) para ver lo que podría ser inválido y comencé a descartar cada uno hasta que cayó sobre los artículos desechables. Luego determiné sobre cuáles teníamos control y la fuente era la única. Entonces, eché un vistazo a cómo manejamos la fuente y en qué circunstancias lo eliminamos para identificar posibles causas raíz. Pude proponer una secuencia plausible de eventos que se ajustan a los informes de los usuarios y, por lo tanto, pueden codificar una solución de bajo riesgo.

Por supuesto, se me pasó por la mente que el error estaba en el marco, pero me gustaría asumir que lo arruinamos antes de culpar a Microsoft.

Conclusión

Entonces, así es como me acerqué a un ejemplo particular de este tipo de problema. Como puede ver, es menos que ideal, pero encaja con lo que muchos han dicho.

Guión

Tienes varios informes de errores que muestran el mismo problema. Todos son crípticos con historias similares de cómo ocurrió el problema. Siga los pasos pero no reproduce de manera confiable el problema. Después de investigar un poco y buscar en la web, sospecha lo que podría estar pasando y está seguro de que puede solucionarlo.

Problema

Desafortunadamente, sin una manera confiable de reproducir el problema original, no se puede verificar que realmente solucione el problema en lugar de no tener ningún efecto en absoluto o exacerbar y enmascarar el problema real. Simplemente no podría arreglarlo hasta que se vuelva reproducible todo el tiempo, pero es un gran error y no solucionarlo causaría a sus usuarios muchos otros problemas.

Pregunta

¿Cómo haces para verificar tu cambio?

Creo que este es un escenario muy familiar para cualquiera que haya diseñado software, así que estoy seguro de que hay una gran cantidad de enfoques y mejores prácticas para abordar errores como este. Actualmente estamos viendo uno de estos problemas en nuestro proyecto, donde he dedicado un tiempo a determinar el problema, pero no he podido confirmar mis sospechas. Un colega está probando mi solución con la esperanza de que "un día corriendo sin interrupciones" equivale a "está arreglado". Sin embargo, preferiría un enfoque más confiable y me imaginé que hay una gran experiencia aquí en SO.


A menos que haya grandes restricciones de tiempo, no empiezo a probar los cambios hasta que pueda reproducir el problema de manera confiable.

Si realmente tuviera que hacerlo, supongo que podría escribir un caso de prueba que a veces parece desencadenar el problema, y ​​agregarlo a su conjunto de pruebas automatizadas (tiene un conjunto de pruebas automatizado, ¿no?) Y luego hacer su cambio y esperar. ese caso de prueba nunca falla nuevamente, sabiendo que si realmente no arreglas nada al menos ahora tienes más posibilidades de atraparlo. Pero cuando puedes escribir un caso de prueba, casi siempre reduces las cosas hasta el punto en que ya no estás lidiando con una situación (aparentemente) no determinista.


Algunas preguntas que podría hacerse:

  • ¿Cuándo funcionó esta pieza de código sin problemas?
  • Qué se ha hecho desde que dejó de funcionar.

Si el código nunca funcionó, el enfoque sería diferente de forma natural.

Al menos cuando muchos usuarios cambian mucho código todo el tiempo, este es un escenario muy común.


En esta situación, donde nada más funciona, introduzco el registro adicional.

También agrego notificaciones por correo electrónico que me muestran el estado de la aplicación cuando se rompe.

A veces agrego contadores de rendimiento ... Pongo esos datos en una tabla y miro las tendencias.

Incluso si no aparece nada, estás reduciendo las cosas. De una manera u otra, terminarás con teorías útiles.


Esos tipos de errores son muy frustrantes. Extrapolarlo a diferentes máquinas con diferentes tipos de hardware personalizado que podría estar en ellos (como en mi empresa), y boy oh boy se convierte en una pesadilla. Actualmente tengo varios errores como este en este momento en mi trabajo.

Mi regla de oro: no lo soluciono a menos que pueda reproducirlo yo mismo o me presentan un registro que muestra claramente algo incorrecto. De lo contrario, no puedo verificar mi cambio, ni puedo verificar que mi cambio no haya roto nada más. Por supuesto, es solo una regla práctica: hago excepciones.

Creo que tienes razón en preocuparte por el enfoque de tu colega.


Estos problemas siempre han sido causados ​​por:

  1. Problemas de memoria
  2. Enhebrar problemas

Para resolver el problema, debes:

  • Instrumente su código (Agregar declaraciones de registro)
  • Revisión de código enhebrado
  • Asignación / desreferenciación de memoria de revisión de código

Probablemente, las revisiones de código solo sucedan si es una prioridad, o si tiene una fuerte sospecha sobre qué código comparten los múltiples informes de errores. Si se trata de un problema de enhebrado, verifique la seguridad de su hilo: asegúrese de que las variables accesibles por ambos hilos estén protegidas. Si se trata de un problema de memoria, verifique sus asignaciones y desreferencias, y especialmente desconfíe del código que asigna y devuelve memoria, o código que utiliza la asignación de memoria por parte de otra persona que pueda liberarlo.


Estos son horribles y casi siempre resistentes a las "soluciones" que el ingeniero cree que está poniendo, ya que tienen el hábito de volver a morder meses después. Tenga cuidado con las correcciones hechas a errores intermitentes. Prepárese para un poco de trabajo pesado y un registro intensivo ya que esto suena más un problema de prueba que un problema de desarrollo.

Mi propio problema al superar errores como estos era que a menudo estaba demasiado cerca del problema, no retrocedía y miraba la imagen más grande. Intenta que otra persona mire cómo abordas el problema.

Específicamente, mi error tenía que ver con la configuración de los tiempos de espera y varios otros números mágicos que en retrospectiva estaban en el límite y así funcionaban casi todo el tiempo. El truco en mi propio caso fue hacer una gran cantidad de experimentos con configuraciones para poder averiguar qué valores ''romperían'' el software.

¿Las fallas ocurren durante períodos de tiempo específicos? Si es así, ¿dónde y cuándo? ¿Son solo ciertas personas las que parecen reproducir el error? ¿Qué conjunto de entradas parece invitar al problema? ¿En qué parte de la aplicación falla? ¿El error parece más o menos intermitente en el campo?

Cuando era un probador de software, mis principales herramientas eran un bolígrafo y un papel para registrar notas de mis acciones anteriores: recordar muchos detalles aparentemente insignificantes es vital. Al observar y recolectar pequeños bits de datos todo el tiempo, el error parecerá ser menos intermitente.


Instrumente la compilación con un registro más extenso (posiblemente opcional) y un ahorro de datos que permita una reproducción exacta de los pasos de IU variables que los usuarios tomaron antes de que se produjera el bloqueo.

Si esos datos no le permiten reproducir el problema de manera confiable, entonces ha reducido la clase de error. Es hora de buscar fuentes de comportamiento aleatorio, como variaciones en la configuración del sistema, comparaciones de punteros, datos no inicializados, etc.

Algunas veces, "sabes" (o más bien sientes) que puedes solucionar el problema sin realizar pruebas exhaustivas o andamios de pruebas unitarias, porque realmente entiendes el problema. Sin embargo, si no lo hace, a menudo se reduce a algo así como "lo ejecutamos 100 veces y el error ya no se produce, por lo que lo consideraremos reparado hasta la próxima vez que se informe".


Los errores que son difíciles de reproducir son los más difíciles de resolver. Lo que necesita para asegurarse de haber encontrado la raíz del problema, incluso si el problema en sí no se puede reproducir con éxito.

Los errores intermitentes más comunes son causados ​​por las condiciones de la raza: al eliminar la raza o asegurarse de que un lado siempre gana, ha eliminado la raíz del problema, incluso si no puede confirmarlo con éxito probando los resultados. Lo único que puedes probar es que la causa no necesita repetirse.

A veces, arreglar lo que se ve como la raíz resuelve un problema, pero no el correcto; no hay forma de evitarlo. La mejor manera de evitar errores intermitentes es ser cuidadoso y metódico con el diseño y la arquitectura del sistema.


Me he encontrado con errores en los sistemas que parecen causar errores sistemáticamente, pero al pasar por el código en un depurador el problema desaparece misteriosamente. En todos estos casos, el problema fue el momento.

Cuando el sistema funcionaba normalmente, había algún tipo de conflicto por los recursos o daba el siguiente paso antes de que finalizara el último. Cuando lo puse en el depurador, las cosas se movían lo suficientemente lento como para que el problema desapareciera.

Una vez que descubrí que era un problema de tiempo, fue fácil encontrar una solución. No estoy seguro de si esto es aplicable en su caso, pero cada vez que fallan los errores en el depurador, los problemas son los primeros sospechosos.


No hay una sola respuesta a este problema. A veces, la solución que ha encontrado le ayuda a descubrir el escenario para reproducir el problema, en cuyo caso puede probar ese escenario antes y después de la solución. A veces, sin embargo, la solución que ha encontrado solo soluciona uno de los problemas pero no todos, o como usted dice que enmascara un problema más profundo. Ojalá pudiera decir "haz esto, funciona todo el tiempo", pero no hay un "esto" que se ajuste a ese escenario.


Nunca podrá verificar la solución sin identificar la causa raíz y encontrar una forma confiable de reproducir el error.

Para identificar la causa raíz: si su plataforma lo permite, conecte la depuración post mortem al problema.

Por ejemplo, en Windows, obtenga su código para crear un archivo de minivolcado (volcado del núcleo en Unix) cuando encuentre este problema. Luego puede hacer que el cliente (o WinQual, en Windows) le envíe este archivo. Esto debería proporcionarle más información sobre cómo el código ha fallado en el sistema de producción.

Pero sin eso, todavía tendrá que encontrar una forma confiable de reproducir el error. De lo contrario, nunca podrás verificar que esté solucionado.

Incluso con toda esta información, puede terminar arreglando un error que parece, pero no es, el que el cliente está viendo.


Para un error difícil de reproducir, el primer paso suele ser la documentación. En el área del código que está fallando, modifique el código para que sea hiperexplícito: un comando por línea; manejo de excepciones pesadas y diferenciadas; salida prolija, incluso prolija de depuración. De esta forma, incluso si no puede reproducir o corregir el error, puede obtener mucha más información sobre la causa la próxima vez que se vea la falla.

El segundo paso suele ser la aserción de suposiciones y la comprobación de límites. Todo lo que creas saber sobre el código en cuestión, escribe. Afirmaciones y verificaciones. Específicamente, verifique los objetos por nulidad y (si su lenguaje es dinámico) existencia.

En tercer lugar, verifique la cobertura de su unidad de prueba. ¿Sus pruebas unitarias cubren realmente cada horquilla en ejecución? Si no tiene pruebas unitarias, este es probablemente un buen lugar para comenzar.

El problema con los errores irreproducibles es que solo son irreproducibles para el desarrollador. Si sus usuarios finales insisten en reproducirlos, es una herramienta valiosa para aprovechar el crash en el campo.


Primero necesita obtener rastros de pila de sus clientes, de esa manera usted puede hacer algunos análisis forenses.

Luego realice pruebas de fuzz con entrada aleatoria y mantenga estas pruebas en ejecución durante largos períodos, son excelentes para encontrar esos casos irracionales de frontera, que los programadores y evaluadores humanos pueden encontrar a través de casos de uso y comprensión del código.


Una vez que entiendas completamente el error (y eso es un gran "una vez"), deberías ser capaz de reproducirlo a voluntad. Cuando se escribe el código de reproducción (prueba automatizada), corrige el error.

¿Cómo llegar al punto donde entiendes el error?

Instrumente el código (log como loco). Trabaje con su control de calidad: son buenos para volver a crear el problema, y ​​debe organizar el kit completo de herramientas de desarrollo en sus máquinas. Use herramientas automatizadas para recursos / memoria sin inicializar. Solo mire fijamente el código. No hay una solución fácil allí.


Uso lo que llamo "programación defensiva de estilo pesado" : agregue afirmaciones en todos los módulos que parecen estar vinculados por el problema. Lo que quiero decir es agregar MUCHAS afirmaciones , afirmar evidencias, afirmar el estado de los objetos en todos sus miembros, afirmar el estado de "medio ambiente", etc.

Las afirmaciones lo ayudan a identificar el código que NO está vinculado al problema.

La mayoría de las veces encuentro el origen del problema simplemente escribiendo las afirmaciones, ya que te obliga a volver a leer todo el código y a pliegarte bajo las agallas de la aplicación para entenderlo.


Usted dice en un comentario que cree que es una condición de carrera. Si crees que sabes qué "característica" del código está generando la condición, puedes escribir una prueba para intentar forzarla.

Aquí hay un código arriesgado en c:

const int NITER = 1000; int thread_unsafe_count = 0; int thread_unsafe_tracker = 0; void* thread_unsafe_plus(void *a){ int i, local; thread_unsafe_tracker++; for (i=0; i<NITER; i++){ local = thread_unsafe_count; local++; thread_unsafe_count+=local; }; } void* thread_unsafe_minus(void *a){ int i, local; thread_unsafe_tracker--; for (i=0; i<NITER; i++){ local = thread_unsafe_count; local--; thread_unsafe_count+=local; }; }

que puedo probar (en un entorno pthreads) con:

pthread_t th1, th2; pthread_create(&th1,NULL,&thread_unsafe_plus,NULL); pthread_create(&th2,NULL,&thread_unsafe_minus,NULL); pthread_join(th1,NULL); pthread_join(th2,NULL); if (thread_unsafe_count != 0) { printf("Ah ha!/n"); }

En la vida real, es probable que tengas que ajustar tu código sospechoso de alguna manera para ayudar a que la carrera llegue más lejos.

Si funciona, ajusta la cantidad de hilos y otros parámetros para hacer que golpee la mayor parte del tiempo, y ahora tienes una oportunidad.


Simplemente: pregunte al usuario que lo informó.

Solo uso uno de los reporteros como un sistema de verificación. Por lo general, la persona que estaba dispuesta a informar un error está más que feliz de ayudarlo a resolver su problema [1]. Solo dale tu versión con una posible solución y pregunta si el problema se ha ido. En los casos en que el error es una regresión, el mismo método se puede utilizar para dividir en dos el lugar donde ocurrió el problema, dando al usuario con el problema múltiples versiones para evaluar. En otros casos, el usuario también puede ayudarlo a solucionar el problema dándole una versión con más capacidades de depuración.

Esto limitará cualquier efecto negativo de una posible solución a esa persona en lugar de adivinar que algo solucionará el error y luego se dará cuenta de que acabas de lanzar un "arreglo de error" que no tiene efecto o, en el peor de los casos, un efecto negativo para la estabilidad del sistema

También puede limitar los posibles efectos negativos de la "corrección de errores" dando la nueva versión a un número limitado de usuarios (por ejemplo, a todos los que informaron el problema) y liberando la solución solo después de eso.

También puede confirmar que la corrección que ha realizado funciona, es fácil agregar pruebas que aseguren que su corrección permanezca en el código (al menos en el nivel de prueba de la unidad, si el error es difícil de reproducir en un nivel más alto del sistema) )

Por supuesto, esto requiere que lo que sea que esté trabajando respalda este tipo de enfoque. Pero si no lo hiciera, realmente haría todo lo posible por permitirlo: los usuarios finales están más satisfechos y muchos de los problemas tecnológicos más difíciles simplemente desaparecen y las prioridades quedan claras cuando el desarrollo puede interactuar directamente con los usuarios finales del sistema.

[1] Si alguna vez ha denunciado un error, es muy probable que sepa que muchas veces la respuesta del equipo de desarrollo / mantenimiento es negativa desde el punto de vista de los usuarios finales o no habrá respuesta, especialmente en situaciones donde el el error no puede ser reproducido por el equipo de desarrollo.