Lo mejor es enemigo de lo bueno.

Cuadro 6

Este artículo es la historia de cómo una vez decidimos mejorar nuestra herramienta interna SelfTester que aplicamos para probar la calidad del analizador PVS-Studio. La mejora fue simple y parecía ser útil, pero nos metió en algunos problemas. Más tarde resultó que mejor renunciamos a la idea.

Selftester


Desarrollamos y promovemos el analizador de código estático PVS-Studio para C, C ++, C # y Java. Para probar la calidad de nuestro analizador, utilizamos herramientas internas, genéricamente llamadas SelfTester. Creamos una versión de SelfTester separada para cada idioma compatible. Se debe a detalles específicos de las pruebas, y es más conveniente. Por lo tanto, en este momento tenemos tres herramientas internas de SelfTester en nuestra empresa para C \ C ++, C # y Java, respectivamente. Además, le contaré acerca de la versión de Windows de SelfTester para proyectos de C \ C ++ Visual Studio, llamándolo simplemente SelfTester. Este probador fue el primero en la línea de herramientas internas similares, es el más avanzado y complejo de todos.

¿Cómo funciona SelfTester? La idea es simple: tome un grupo de proyectos de prueba (estamos usando proyectos de código abierto reales) y analícelos usando PVS-Studio. Como resultado, se genera un registro del analizador para cada proyecto. Este registro se compara con el registro de referencia del mismo proyecto. Al comparar registros, SelfTester crea un resumen de los registros que se comparan de una manera conveniente para el desarrollador.

Después de estudiar el resumen, un desarrollador concluye acerca de los cambios en el comportamiento del analizador de acuerdo con el número y tipo de advertencias, velocidad de trabajo, errores internos del analizador, etc. Toda esta información es muy importante: le permite saber cómo el analizador hace frente a su trabajo.

Según el resumen de la comparación de registros, un desarrollador introduce cambios en el núcleo del analizador (por ejemplo, al crear una nueva regla de diagnóstico) e inmediatamente controla el resultado de sus ediciones. Si un desarrollador no tiene más problemas para comparar un registro regular, hace una referencia de registro de advertencias actual para un proyecto. De lo contrario, el trabajo continúa.

Entonces, la tarea de SelfTester es trabajar con un grupo de proyectos de prueba (por cierto, hay más de 120 de ellos para C / C ++). Los proyectos para el grupo se seleccionan en forma de soluciones de Visual Studio. Esto se hace para verificar adicionalmente el trabajo del analizador en varias versiones de Visual Studio, que son compatibles con el analizador (en este punto, desde Visual Studio 2010 hasta Visual Studio 2019).

Nota: además, separaré los conceptos solución y proyecto , considerando un proyecto como parte de una solución.

La interfaz de SelfTester tiene el siguiente aspecto:

Cuadro 3

A la izquierda hay una lista de soluciones, a la derecha: resultados de una comprobación para cada versión de Visual Studio.

Las etiquetas grises "No admitidas" indican que una solución no admite una versión de Visual Studio elegida o que no se convirtió para esta versión. Algunas soluciones tienen una configuración en un grupo, que indica una versión específica de Visual Studio para una verificación. Si no se especifica una versión, se actualizará una solución para todas las versiones posteriores de Visual Studio. Un ejemplo de dicha solución se encuentra en la captura de pantalla: "smart_ptr_check.sln" (se realiza una comprobación para todas las versiones de Visual Studio).

Una etiqueta verde "OK" indica que una verificación regular no ha detectado diferencias con el registro de referencia. Una etiqueta roja "Diff" indica las diferencias. Hay que prestar especial atención a estas etiquetas. Después de hacer clic dos veces en la etiqueta necesaria, la solución elegida se abrirá en una versión relacionada de Visual Studio. Allí también se abrirá una ventana con un registro de advertencias. Los botones de control en la parte inferior le permiten volver a ejecutar el análisis de la solución seleccionada o de todas, hacer que el registro elegido (o todo a la vez) haga referencia, etc.

Los resultados de SelfTester siempre se duplican en el informe html (informe diffs)

Además de la GUI, SelfTester también tiene modos automatizados para ejecuciones nocturnas. Sin embargo, el patrón de uso habitual que el desarrollador repite ejecuta un desarrollador durante la jornada laboral. Por lo tanto, una de las características más importantes de SelfTester es la velocidad de trabajo.

Por qué importa la velocidad:

  1. El rendimiento de cada paso es crucial en términos de pruebas nocturnas. Obviamente, cuanto más rápido pasan las pruebas, mejor. Por el momento, el tiempo de rendimiento promedio de SelfTester supera las 2 horas;
  2. Al ejecutar SelfTester durante el día, un desarrollador tiene que esperar menos por el resultado, lo que aumenta la productividad de su fuerza laboral.

Fue la aceleración del rendimiento lo que se convirtió en la razón de los refinamientos esta vez.

Multihilo en SelfTester


SelfTester se creó inicialmente como una aplicación multiproceso con la capacidad de probar simultáneamente varias soluciones. La única limitación era que no podía verificar simultáneamente la misma solución para diferentes versiones de Visual Studio, porque muchas soluciones deben actualizarse a ciertas versiones de Visual Studio antes de la prueba. Durante el curso, los cambios se introducen directamente en los archivos de los proyectos .vcxproj , lo que conduce a errores durante la ejecución paralela.

Para que el trabajo sea más eficiente, SelfTester utiliza un programador de tareas inteligente para establecer un valor estrictamente limitado de subprocesos paralelos y mantenerlo.

El planificador se usa en dos niveles. El primero es el nivel de soluciones , se utiliza para comenzar a probar la solución .sln utilizando la utilidad PVS-Studio_Cmd.exe . El mismo planificador, pero con otra configuración de grado de paralelismo , se utiliza dentro de PVS-Studio_Cmd.exe (en el nivel de prueba de los archivos de origen).

El grado de paralelismo es un parámetro que indica cuántos subprocesos paralelos deben ejecutarse simultáneamente. Se eligieron cuatro y ocho valores predeterminados para el grado de paralelismo de las soluciones y el nivel de los archivos, respectivamente. Por lo tanto, el número de subprocesos paralelos en esta implementación tiene que ser 32 (4 soluciones probadas simultáneamente y 8 archivos). Esta configuración nos parece óptima para el trabajo del analizador en un procesador de ocho núcleos.

Un desarrollador puede establecer otros valores del grado de paralelismo por sí mismo según el rendimiento de su computadora o las tareas actuales. Si un desarrollador no especifica este parámetro, el número de procesadores lógicos del sistema se elegirá de manera predeterminada.

Nota: supongamos además que tratamos con el grado predeterminado de paralelismo.

El planificador LimitedConcurrencyLevelTaskScheduler se hereda de System.Threading.Tasks.TaskScheduler y se refina para proporcionar el nivel de paralelismo máximo cuando se trabaja en ThreadPool . Jerarquía de herencia:

LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... } 

PausableTaskScheduler le permite pausar el rendimiento de la tarea, y además de esto, LimitedConcurrencyLevelTaskScheduler proporciona control intelectual de la cola de tareas y la programación de su rendimiento, teniendo en cuenta el grado de paralelismo, el alcance de las tareas programadas y otros factores. Se utiliza un planificador cuando se ejecutan tareas de LimitedConcurrencyLevelTaskScheduler .

Razones para refinamientos


El proceso descrito anteriormente tiene un inconveniente: no es óptimo cuando se trata de soluciones de diferentes tamaños. Y el tamaño de las soluciones en el grupo de prueba es muy diverso: de 8 KB a 4 GB, el tamaño de una carpeta con una solución y de 1 a varios miles de archivos de código fuente en cada uno.

El planificador pone las soluciones en la cola simplemente una tras otra, sin ningún componente inteligente. Permítame recordarle que, de forma predeterminada, no se pueden probar más de cuatro soluciones simultáneamente. Si actualmente se prueban cuatro soluciones grandes (el número de archivos en cada una es más de ocho), se supone que trabajamos de manera efectiva porque utilizamos tantos hilos como sea posible (32).

Pero imaginemos una situación bastante frecuente, cuando se prueban varias soluciones pequeñas. Por ejemplo, una solución es grande y contiene 50 archivos (se utilizará un número máximo de subprocesos), mientras que otras tres soluciones contienen tres, cuatro, cinco archivos cada una. En este caso, solo usaremos 20 hilos (8 + 3 + 4 + 5). Obtenemos una subutilización del tiempo del procesador y un rendimiento general reducido.

Nota : de hecho, el cuello de botella suele ser el subsistema de disco, no el procesador.

Mejoras


La mejora que es evidente en este caso es la clasificación de la lista de soluciones probadas. Necesitamos obtener un uso óptimo del número establecido de subprocesos ejecutados simultáneamente (32), pasando a proyectos de prueba con el número correcto de archivos.

Consideremos nuevamente nuestro ejemplo de probar cuatro soluciones con el siguiente número de archivos en cada una: 50, 3, 4 y 5. Es probable que la tarea que verifica una solución de tres archivos resulte más rápida. Sería mejor agregar una solución con ocho o más archivos en lugar de ella (para usar el máximo de los hilos disponibles para esta solución). De esta manera, utilizaremos 25 hilos a la vez (8 + 8 + 4 + 5). No esta mal. Sin embargo, siete hilos aún no están involucrados. Y aquí surge la idea de otro refinamiento, que es eliminar el límite de cuatro hilos en las soluciones de prueba. Porque ahora podemos agregar no una, sino varias soluciones, utilizando 32 hilos. Imaginemos que tenemos dos soluciones más de tres y cuatro archivos cada una. Agregar estas tareas cerrará completamente la "brecha" de los hilos no utilizados, y habrá32 (8 + 8 + 4 + 5 + 3 + 4 ) de ellos.

Esperemos que la idea sea clara. De hecho, la implementación de estas mejoras tampoco requirió mucho esfuerzo. Todo se hizo en un día.

Necesitábamos reelaborar la clase de tarea: heredar de System.Threading.Tasks.Task y asignar el campo "peso". Utilizamos un algoritmo simple para establecer el peso en una solución: si el número de archivos es menor que ocho, el peso es igual a este número (por ejemplo, 5). Si el número es mayor o igual a ocho, el peso será igual a ocho.

También tuvimos que elaborar el planificador: enseñarle a elegir soluciones con el peso necesario para alcanzar el valor máximo de 32 hilos. También tuvimos que permitir más de cuatro subprocesos para las pruebas simultáneas de soluciones.

Finalmente, necesitábamos un paso preliminar para analizar todas las soluciones en el grupo (evaluación usando MSBuild API) para evaluar y establecer el peso de las soluciones (obtener números de archivos con código fuente).

Resultado


Creo que después de una presentación tan larga ya has adivinado que no salió nada.

Cuadro 12

Sin embargo, es bueno que las mejoras fueran simples y rápidas.

Aquí viene esa parte del artículo, donde les voy a contar sobre lo que "nos metió en muchos problemas" y todas las cosas relacionadas con él.

Efectos secundarios


Entonces, un resultado negativo también es un resultado. Resultó que la cantidad de soluciones grandes en el grupo supera con creces la cantidad de soluciones pequeñas (menos de ocho archivos). En este caso, estas mejoras no tienen un efecto muy notable, ya que son casi invisibles: probar proyectos pequeños requiere una cantidad muy pequeña de tiempo en comparación con el tiempo, necesario para proyectos grandes.

Sin embargo, decidimos dejar el nuevo refinamiento como "no perturbador" y potencialmente útil. Además, el conjunto de soluciones de prueba se repone constantemente, por lo que en el futuro, tal vez, la situación cambiará.

Y luego ...

Cuadro 5

Uno de los desarrolladores se quejó de la caída del SelfTester. Bueno, la vida pasa. Para evitar que se pierda este error, creamos un incidente interno (ticket) con el nombre "Excepción al trabajar con SelfTester". El error ocurrió al evaluar el proyecto. Aunque una gran cantidad de ventanas con errores indicaron el problema nuevamente en el controlador de errores. Pero esto fue eliminado rápidamente, y durante la semana siguiente nada se estrelló. De repente, otro usuario se quejó de SelfTester. Nuevamente, el error de una evaluación de proyecto:

Cuadro 8

Esta vez, la pila contenía mucha información útil: el error estaba en formato xml. Es probable que al manejar el archivo del proyecto Proto_IRC.vcxproj (su representación xml) algo le haya sucedido al archivo en sí, por eso XmlTextReader no pudo manejarlo.

Tener dos errores en un período de tiempo bastante corto nos hizo mirar más de cerca el problema. Además, como dije anteriormente, SelfTester es muy utilizado por los desarrolladores.

Para empezar, analizamos el último accidente. Es triste decirlo, no encontramos nada sospechoso. Por si acaso, les pedimos a los desarrolladores (usuarios de SelfTester) que estén atentos e informen sobre posibles errores.

Punto importante: el código erróneo se reutilizó en SelfTester. Originalmente se usó para evaluar proyectos en el analizador ( PVS-Studio_Cmd.exe ). Es por eso que la atención al problema ha crecido. Sin embargo, no hubo tales bloqueos en el analizador.

Mientras tanto, el ticket sobre problemas con SelfTester se complementó con nuevos errores:

Cuadro 9

XmlException nuevamente. Obviamente, hay hilos competidores en algún lugar que funcionan con la lectura y escritura de archivos de proyecto. SelfTester trabaja con proyectos en los siguientes casos:

  1. Evaluación de proyectos en el curso del cálculo preliminar de los pesos de las soluciones: un nuevo paso que inicialmente suscitó sospechas;
  2. Actualización de proyectos a las versiones necesarias de Visual Studio: se realiza justo antes de la prueba (los proyectos no interfieren) y no debe afectar el proceso de trabajo.
  3. Evaluación de proyectos durante las pruebas: un mecanismo seguro para subprocesos bien establecido, reutilizado desde PVS-Studio_Cmd.exe ;
  4. Restaurar archivos de proyecto (reemplazando archivos .vcxproj modificados con archivos de referencia iniciales) al salir de SelfTester, porque los archivos de proyecto pueden actualizarse a las versiones necesarias de Visual Studio durante el trabajo. Es un paso final, que no tiene impacto en otros mecanismos.

La sospecha recayó en el nuevo código agregado para la optimización (cálculo de peso). Pero su investigación de código mostró que si un usuario ejecuta el análisis justo después del inicio de SelfTester, el probador siempre espera correctamente hasta el final de la evaluación previa. Este lugar parecía seguro.

Nuevamente, no pudimos identificar la fuente del problema.

Dolor


Todo el mes siguiente, SelfTester continuó fallando una y otra vez. El ticket siguió llenándose de datos, pero no estaba claro qué hacer con estos datos. La mayoría de los bloqueos tuvieron la misma XmlException. Ocasionalmente había algo más, pero en el mismo código reutilizado de PVS-Studio_Cmd.exe .

Imagen 1

Tradicionalmente, a las herramientas internas no se les imponen requisitos muy altos, por lo que seguimos desconcertando los errores de SelfTester sobre un principio residual. De vez en cuando, se involucraban diferentes personas (durante todo el incidente, seis personas trabajaron en el problema, incluidos dos pasantes). Sin embargo, tuvimos que distraernos con esta tarea.

Nuestro primer error De hecho, en este punto podríamos haber resuelto este problema de una vez por todas. Como? Estaba claro que el error fue causado por una nueva optimización. Después de todo, antes de que todo funcionara bien, y el código reutilizado claramente no puede ser tan malo. Además, esta optimización no había traído ningún beneficio. Entonces, ¿qué había que hacer? Eliminar esta optimización. Como probablemente entiendas, no se hizo. Continuamos trabajando en el problema, que creamos nosotros mismos. Continuamos buscando la respuesta: "¿CÓMO?" ¿Cómo se bloquea? Parecía estar escrito correctamente.

Nuestro segundo error. Otras personas se involucraron en la solución del problema . Es un error muy, muy grande. No solo no resolvió el problema sino que también requirió recursos adicionales desperdiciados. Sí, nuevas personas aportaron nuevas ideas, pero llevó mucho tiempo de trabajo implementar (por nada) estas ideas. En algún momento, hicimos que nuestros pasantes escribieran programas de prueba que emulaban la evaluación de un mismo proyecto en diferentes hilos con modificación paralela de un proyecto en otro proyecto. No sirvió de nada. Solo descubrimos que la API de MSBuild era segura para subprocesos en el interior, lo cual ya hemos conocido. También agregamos el guardado automático de mini dump cuando ocurre la excepción XmlException . Teníamos a alguien que estaba depurando todo esto. ¡Pobre chico! Hubo discusiones, hicimos otras cosas innecesarias.

Finalmente, tercer error. ¿Sabes cuánto tiempo ha pasado desde el momento en que ocurrió el problema de SelfTester hasta el punto en que se resolvió? Bueno, puedes contar contigo mismo. El boleto se creó el 17/09/2018 y se cerró el 20/02/2019. ¡Hubo más de 40 comentarios! Chicos, eso es mucho tiempo! Nos permitimos estar ocupados durante cinco meses con ESTO. Al mismo tiempo, estábamos ocupados apoyando Visual Studio 2019, agregando el soporte del lenguaje Java, introduciendo el estándar MISRA C / C ++, mejorando el analizador C #, participando activamente en conferencias, escribiendo un montón de artículos, etc. Todas estas actividades recibieron menos tiempo de los desarrolladores debido a un error estúpido en SelfTester.

Amigos, aprendan de nuestros errores y nunca hagan esto. Nosotros tampoco lo haremos.

Eso es todo, ya terminé.

Cuadro 15

Bien, fue una broma, te diré cuál fue el problema con SelfTester :)

Bingo!


Afortunadamente, había una persona entre nosotros con una vista clara (mi colega Sergey Vasiliev), que solo miró el problema desde un ángulo muy diferente (y también, tuvo un poco de suerte). ¿Qué pasa si está bien dentro del SelfTester, pero algo del exterior bloquea los proyectos? Usualmente no teníamos nada lanzado con SelfTester, en algunos casos controlamos estrictamente el entorno de ejecución. En este caso, este mismo "algo" podría ser SelfTester, pero una instancia diferente.

Al salir de SelfTester, el hilo que restaura los archivos de proyecto a partir de referencias, continúa funcionando durante un tiempo. En este punto, el probador podría iniciarse nuevamente. La protección contra las ejecuciones simultáneas de varias instancias de SelfTester se agregó más tarde y ahora tiene el siguiente aspecto:

Cuadro 16

Pero en ese momento no lo teníamos.

Locos, pero cierto: durante casi seis meses de tormento nadie le prestó atención. Restaurar proyectos a partir de referencias es un procedimiento de fondo bastante rápido, pero desafortunadamente no lo suficientemente rápido como para no interferir con el relanzamiento de SelfTester. ¿Y qué pasa cuando lo lanzamos? Así es, calculando el peso de las soluciones. Un proceso reescribe archivos .vcxproj mientras otro intenta leerlos. Saluda a XmlException .

Sergey descubrió todo esto cuando agregó la capacidad de cambiar a un conjunto diferente de registros de referencia para el probador. Se hizo necesario después de agregar un conjunto de reglas MISRA en el analizador. Puede cambiar directamente en la interfaz, mientras el usuario ve esta ventana:

Cuadro 14

Después de eso, SelfTester se reinicia. Y anteriormente, aparentemente, los usuarios de alguna manera emularon el problema ellos mismos, ejecutando el probador nuevamente.

Culpabilidad y conclusiones


Por supuesto, eliminamos (es decir, deshabilitamos) la optimización creada anteriormente. Además, fue mucho más fácil que hacer algún tipo de sincronización entre reinicios del probador por sí mismo. Y todo comenzó a funcionar perfectamente, como antes. Y como medida adicional, agregamos la protección anterior contra el lanzamiento simultáneo del probador.

Ya he escrito anteriormente sobre nuestros principales errores al buscar el problema, por lo que basta de autoflagelación. Somos seres humanos, así que podríamos estar equivocados. Es importante aprender de sus propios errores y sacar conclusiones. Las conclusiones de este caso son bastante simples:

  • Deberíamos monitorear y estimar la complejidad de la tarea;
  • A veces necesitamos parar en algún momento;
  • Trata de ver el problema más ampliamente. Con el tiempo, se puede obtener una visión del túnel del caso, mientras que requiere una nueva perspectiva.
  • No tengas miedo de eliminar el código viejo o innecesario.

Eso es todo, esta vez definitivamente he terminado. Gracias por leer hasta el final. ¡Te deseo un código sin errores!

Source: https://habr.com/ru/post/466739/


All Articles