Lo mejor es enemigo de lo bueno.

Cuadro 12

Este artículo trata sobre cómo una vez que decidimos mejorar ligeramente la herramienta interna SelfTester, utilizada para verificar la calidad del analizador PVS-Studio. La mejora fue simple y parecía útil, pero nos creó muchos problemas, y luego resultó que sería mejor si no lo hiciéramos.

Selftester


Desarrollamos y promovemos el analizador de código estático PVS-Studio para C, C ++, C # y Java. Para verificar la calidad del analizador, utilizamos herramientas internas que se denominan colectivamente SelfTester. Cada uno de los idiomas admitidos tiene su propia versión de SelfTester. Esto se debe a las características de las pruebas, y es más conveniente. Por lo tanto, en este momento, nuestra empresa utiliza tres herramientas internas de SelfTester para C \ C ++, C # y Java, respectivamente. A continuación, hablaré sobre 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 tales herramientas internas, es el más avanzado y complejo de todos.

¿Cómo funciona SelfTester? La idea es simple: tome un grupo de proyectos de prueba (usamos proyectos reales con código fuente abierto) y analícelos usando PVS-Studio. Como resultado, se genera un registro de advertencia del analizador para cada proyecto. Este registro se compara con el registro de referencia para el mismo proyecto. Al comparar registros, SelfTester crea un registro de comparación de registros en una forma conveniente para que los desarrolladores lo perciban.

Después de estudiar el libro de registro, el desarrollador llega a una conclusión sobre los cambios en el comportamiento del analizador: el número y la naturaleza de las advertencias, la velocidad de operación, los errores internos del analizador, etc. Toda esta información es muy importante, le permite comprender qué tan bien el analizador hace su trabajo.

Según el registro de comparación de registros, el desarrollador realiza cambios en el núcleo del analizador (por ejemplo, al crear una nueva regla de diagnóstico), controlando inmediatamente el efecto de sus ediciones. Si el desarrollador ya no tiene preguntas sobre la próxima comparación de los registros, entonces hace referencia al registro de advertencia actual para el proyecto. De lo contrario, el trabajo continúa.

Entonces, la tarea de SelfTester es trabajar con un grupo de proyectos de prueba (por cierto, ya hay más de 120 de ellos para C / C ++). Los proyectos para el grupo se seleccionan como soluciones de Visual Studio. Esto se hace para probar adicionalmente el analizador en diferentes versiones de Visual Studio que el analizador admite (desde Visual Studio 2010 hasta Visual Studio 2019 en este momento).

Nota : Separaré aún más los conceptos de solución y proyecto , entendiendo el proyecto como parte de la solución, como es habitual en Visual Studio.

La interfaz de SelfTester se ve así:

Cuadro 3

A la izquierda hay una lista de soluciones, a la derecha están los resultados de la prueba para cada versión de Visual Studio.

Las marcas grises "No admitido" indican que la solución no es compatible con la versión seleccionada de Visual Studio o que no se ha convertido para esta versión. Algunas soluciones en el grupo tienen una configuración que indica la versión específica de Visual Studio para verificar. Si no se especifica la versión, la solución se actualizará a todas las versiones posteriores de Visual Studio. Un ejemplo de dicha solución en la captura de pantalla es "smart_ptr_check.sln" (se realizó la verificación para todas las versiones de Visual Studio).

Una marca verde "OK" indica que la próxima verificación no reveló ninguna diferencia con el registro de referencia. Una marca roja "Diff" indica diferencias. Es en tales etiquetas que el desarrollador debe prestar atención. Para hacer esto, necesita hacer doble clic en la etiqueta deseada. La solución seleccionada se abrirá en la versión deseada de Visual Studio, y allí también se abrirá una ventana con un registro de advertencia. Los botones de control a continuación le permiten reiniciar el análisis de las decisiones seleccionadas o todas, asignar el registro seleccionado (o todos a la vez) a los estándares, etc.

Los resultados presentados del trabajo de SelfTester siempre se duplican en el informe html (registro de diferencias).

Además de la GUI, SelfTester también tiene modos automatizados para ejecutarse durante las compilaciones nocturnas. Sin embargo, el patrón de uso habitual son los lanzamientos repetidos por el desarrollador durante la jornada laboral. Por lo tanto, una de las características importantes para SelfTester es su velocidad .

Por qué la velocidad es importante:

  1. Para ejecutar durante las pruebas nocturnas, el tiempo necesario para completar cada paso es crítico. Obviamente, cuanto más rápido pasan las pruebas, mejor. Y el tiempo de funcionamiento promedio de SelfTester actualmente supera las 2 horas;
  2. Al iniciar SelfTester durante el día, el desarrollador tiene que esperar menos por el resultado, lo que aumenta la productividad laboral.

Fue el deseo de acelerar el trabajo de SelfTester lo que causó las mejoras esta vez.

Multithreading en SelfTester


SelfTester se creó originalmente como una aplicación multiproceso con la capacidad de verificar múltiples soluciones en paralelo. La única limitación es que no puede verificar simultáneamente la misma solución para diferentes versiones de Visual Studio, ya que muchas soluciones necesitan actualizarse a ciertas versiones de Visual Studio antes de verificar. Durante esto, los cambios se realizan directamente en los archivos de proyecto .vcxproj , lo que genera errores al ejecutarse en paralelo.

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

El planificador se usa en dos niveles. El primero es el nivel de solución , que se utiliza para comenzar a verificar la solución .sln utilizando la utilidad PVS-Studio_Cmd.exe . Dentro de PVS-Studio_Cmd.exe (en el nivel de comprobación de archivos de código fuente) se utiliza el mismo planificador, pero con un grado diferente de configuración de paralelismo .

El grado de paralelismo es un parámetro que realmente indica cuántos hilos paralelos deben ejecutarse simultáneamente. Para los valores de grado de paralelismo en el nivel de decisión y los archivos, se seleccionaron los valores predeterminados de cuatro y ocho , respectivamente. Por lo tanto, el número de subprocesos paralelos para esta implementación debe ser igual a 32 (cuatro soluciones probadas simultáneamente y ocho archivos). Esta configuración nos parece óptima para que el analizador funcione en un procesador de ocho núcleos.

El desarrollador puede establecer independientemente otros valores del grado de paralelismo, centrándose en el rendimiento de su computadora o en las tareas actuales. Si no establece este parámetro, se seleccionará de manera predeterminada el número de procesadores lógicos del sistema.

Nota : además consideraremos que el trabajo se lleva a cabo con el grado predeterminado de valores de paralelismo.

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

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

PausableTaskScheduler le permite pausar tareas, y LimitedConcurrencyLevelTaskScheduler , además, proporciona un control inteligente de la cola de tareas y la programación de su ejecución, teniendo en cuenta el grado de paralelismo, la cantidad de tareas programadas y otros factores. El planificador se usa al iniciar las tareas System.Threading.Tasks.Task .

Prerrequisitos para mejoras


La implementación del trabajo descrito anteriormente tiene un inconveniente: no es óptima cuando se trabaja con soluciones de diferentes tamaños. Y el tamaño de las soluciones en el grupo de prueba es muy diferente: de 8 KB a 4 GB para el tamaño de la carpeta con la solución, y de uno a varios miles de archivos de código fuente en cada uno.

El planificador pone en cola las decisiones simplemente en orden, sin ningún componente intelectual. Permítame recordarle que, de forma predeterminada, no se pueden verificar más de cuatro soluciones al mismo tiempo. Si en este momento se están verificando cuatro soluciones grandes (la cantidad de archivos en cada una es superior a ocho), se supone que estamos trabajando de manera eficiente, ya que utilizamos la cantidad máxima posible de subprocesos (32).

Pero imagine una situación bastante común cuando se prueban varias soluciones pequeñas. Por ejemplo, una solución es grande y contiene 50 archivos (se incluirán un máximo de ocho hilos), y los otros tres contienen tres, cuatro y cinco archivos cada uno. En este caso, usamos solo 20 hilos (8 + 3 + 4 + 5). Obtenemos una subutilización del tiempo del procesador y una disminución en el rendimiento general.

Nota : de hecho, el cuello de botella, como regla, sigue siendo el subsistema de disco, no el procesador.

Mejoras


Una mejora que se sugiere en este caso es la clasificación de la lista de soluciones enviadas para verificación. Es necesario lograr el uso óptimo de un número determinado de subprocesos ejecutados simultáneamente (32) mediante la presentación de proyectos con el número "correcto" de archivos para su verificación.

Veamos nuevamente nuestro ejemplo, cuando se verifican cuatro soluciones con el siguiente número de archivos en cada una: 50, 3, 4 y 5. Una tarea que verifica una solución de tres archivos probablemente funcionará pronto. Y en lugar de ello, sería óptimo agregar una solución en la que haya ocho o más archivos (para utilizar un máximo de ocho secuencias disponibles para esta solución). Luego, en total, usaremos ya 25 hilos (8 + 8 + 4 + 5). No esta mal. Sin embargo, siete hilos todavía se dejaron sin usar. Y aquí surge la idea de otro refinamiento, relacionado con la eliminación de la restricción en cuatro hilos para verificar soluciones. De hecho, en el ejemplo anterior, puede agregar no una, sino varias soluciones, utilizando la mayor cantidad posible de los 32 hilos. Imaginemos que tenemos dos soluciones más, tres y cuatro archivos cada una. Agregar estas tareas cerrará por completo la "brecha" en los hilos no utilizados, y habrá 32 (8 + 8 + 4 + 5 + 3 + 4 ).

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

Era necesario refinar la clase de tarea: herencia de System.Threading.Tasks.Task y agregar el campo "peso". Para establecer el peso de la solución, se utiliza un algoritmo simple: si el número de archivos en la solución es menor que ocho, entonces el peso se establece igual a este valor (por ejemplo, 5), si el número de archivos es mayor o igual a ocho, entonces el peso se elige igual a ocho.

También era necesario refinar el programador: enseñarle a elegir soluciones con el peso adecuado para lograr un valor máximo de 32 hilos. También era necesario permitir la asignación de más de cuatro hilos para la verificación simultánea de soluciones.

Finalmente, se tomó un paso preliminar para analizar todas las soluciones de agrupación (evaluación utilizando la API de MSBuild) para calcular y establecer los pesos de la solución (obtener el número de archivos con código fuente).

Resultado


Creo que, después de una presentación tan larga, ya adivinaste que el resultado fue cero.

Cuadro 15

Es bueno que las mejoras fueran simples y rápidas.

Bueno, ahora, de hecho, comienza la parte del artículo sobre "nos creó muchos problemas", y eso es todo.

Efectos secundarios


Entonces, un resultado negativo también es un resultado. Resultó que la cantidad de soluciones grandes en el grupo excede significativamente la cantidad de soluciones pequeñas (menos de ocho archivos). En estas condiciones, las mejoras realizadas no tienen un efecto notable, ya que son prácticamente invisibles: su verificación lleva un tiempo microscópico en comparación con los grandes proyectos.

Sin embargo, se decidió dejar la revisión como "sin interferir" 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" de SelfTester. Bueno, eso pasa. Para evitar que se pierda este error, se lanzó un incidente interno (ticket) con el nombre "Excepción al trabajar con SelfTester". El error ocurrió durante la evaluación del proyecto. Es cierto que tal abundancia de ventanas atestiguó el problema también en el controlador de errores. Pero esto fue eliminado rápidamente, y durante la semana siguiente no se rompió nada. De repente, otro usuario se quejó de SelfTester. Y nuevamente al error de la evaluación del proyecto:

Cuadro 8

Esta vez, la pila contenía más información útil: un error en el formato xml. Probablemente, mientras procesaba el archivo de proyecto Proto_IRC.vcxproj (su representación xml), algo le sucedió al archivo en sí, por lo que XmlTextReader no pudo procesarlo.

La presencia de 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, se hizo un análisis del último lugar de la caída. Lamentablemente, no se pudo identificar nada sospechoso. Por si acaso, pidieron a los desarrolladores (usuarios de SelfTester) que estuvieran alertas y reportaran posibles errores.

Un punto importante: el código en el que ocurrió el error se reutilizó en SelfTester. Inicialmente, se utiliza para evaluar proyectos en el analizador ( PVS-Studio_Cmd.exe ). Es por eso que la atención al problema ha crecido. Sin embargo, no se produjeron caídas similares en el analizador.

Mientras tanto, un boleto sobre problemas con SelfTester se reponía con nuevos errores:

Cuadro 9

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

  1. Evaluación de proyectos durante el cálculo preliminar de los pesos de decisión: un nuevo paso que inicialmente despertó sospechas;
  2. Actualización de proyectos a las versiones necesarias de Visual Studio: realizadas inmediatamente antes de la verificación (los proyectos no se superponen) y no deberían afectar el trabajo;
  3. Evaluación de proyectos durante la verificación: mecanismo de seguridad de subprocesos depurado, que se reutilizó de PVS-Studio_Cmd.exe ;
  4. Recuperar archivos de proyecto (reemplazando archivos .vcxproj modificados con los archivos de referencia originales) al salir de SelfTester, ya que los archivos de proyecto se pueden actualizar a las versiones necesarias de Visual Studio en el proceso: el paso final, que tampoco afecta a otros mecanismos.

La sospecha recayó en el nuevo código agregado para la optimización (cálculo de pesos). Pero el estudio de este código mostró que si el usuario iniciaba el análisis inmediatamente después del inicio de SelfTester, el probador siempre esperaba correctamente el final de la evaluación preliminar. Este lugar parecía seguro.

Nuevamente, no pudimos identificar la fuente del problema.

Dolor


Durante el mes siguiente, SelfTester continuó cayendo de vez en cuando. El ticket se rellenó con datos, pero no estaba claro qué hacer con estos datos. La mayoría de los bloqueos fueron todos con la misma XmlException . Ocasionalmente había algo más, pero en el mismo código reutilizado de PVS-Studio_Cmd.exe .

Imagen 1

Por tradición, no se imponen requisitos tan altos a las herramientas internas, por lo tanto, el trabajo con errores de SelfTester se realizó de forma residual. De vez en cuando, diferentes personas se conectaban (durante todo el tiempo del incidente, seis personas trabajaron en el problema, incluidos dos pasantes en prácticas). Sin embargo, la tarea tuvo que ser distraída.

Nuestro primer error De hecho, aquí ya era posible resolver el 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 todo, todo funcionó bien, y el código reutilizado obviamente no podría ser tan malo. Además, esta optimización no trajo ningún beneficio. Entonces, ¿qué había que hacer? Eliminar esta optimización . Como saben, esto no se hizo. Continuamos trabajando en un problema que nosotros mismos habíamos creado. La búsqueda continuó por la respuesta a la pregunta: "¿CÓMO?" ¿Cómo se cae? Sin embargo, parece estar escrito correctamente.

Nuestro segundo error. Otras personas estaban conectadas a la solución del problema. Un muy, muy gran error. Desafortunadamente, esto no solo no resolvió el problema, sino que se gastaron recursos adicionales. Sí, nuevas personas aportaron nuevas ideas, pero para su implementación tomó (absolutamente desperdiciado) mucho tiempo de trabajo. En cierta etapa, los programas de prueba fueron escritos (por los mismos aprendices) que emulan la evaluación del mismo proyecto en diferentes hilos con modificación paralela del proyecto en otro hilo. No sirvió de nada. Además de lo que ya sabíamos antes, la API de MSBuild es segura para subprocesos en su interior, no han descubierto nada nuevo. Y en SelfTester, se agregó un mini-dump cuando se lanzó una XmlException . Entonces todo esto alguien lo debatió, horror. Se sostuvieron discusiones, se hicieron muchas otras cosas innecesarias.

Finalmente, nuestro tercer error . ¿Sabes cuánto tiempo ha pasado desde que surgió el problema con SelfTester y hasta que se resolvió? Aunque no, cuenta tú mismo. El incidente se creó el 17/09/2018 y se cerró el 20/02/2019, y hay más de 40 (¡cuarenta!) Mensajes allí. Chicos, esto es mucho tiempo! Nos permitimos hacer TI durante cinco meses. Al mismo tiempo (en paralelo), participamos en el soporte para Visual Studio 2019, agregamos el lenguaje Java, comenzamos a implementar el estándar MISRA C / C ++, mejoramos el analizador C #, participamos activamente en conferencias, escribimos un montón de artículos, etc. Y todos estos trabajos no recibieron el tiempo de los desarrolladores debido al estúpido error SelfTester.

Ciudadanos, aprendan de nuestros errores y nunca hagan eso. Y no lo haremos.

Lo tengo todo

Cuadro 17

Por supuesto, esto es una broma, y ​​te diré cuál fue el problema con SelfTester :)

Bingo!


Afortunadamente, entre nosotros había una persona con la conciencia menos nublada (mi colega Sergey Vasiliev), que solo miró el problema desde un ángulo completamente diferente (y también tuvo un poco de suerte). ¿Qué pasa si dentro de SelfTester está realmente bien y los proyectos rompen algo desde afuera? Paralelamente a SelfTester, por lo general, nada comenzó, en algunos casos controlamos estrictamente el tiempo de ejecución. En este caso, este "algo" solo podría ser SelfTester en sí mismo, pero otra instancia más.

Al salir de SelfTester, la secuencia que restaura los archivos de proyecto de los estándares continúa funcionando durante algún tiempo. En este punto, puede reiniciar el probador. La protección contra la ejecución de varias instancias de SelfTester al mismo tiempo se agregó más tarde y ahora se ve así:

Cuadro 16

Pero entonces ella se fue.

Increíblemente, durante casi medio año de tormento nadie le prestó atención. Restaurar proyectos a partir de estándares es un procedimiento de fondo lo suficientemente rápido, pero, desafortunadamente, no lo suficientemente rápido como para no interferir con el reinicio de SelfTester. ¿Y qué pasa en el inicio? Así es, calculando los pesos de decisión. Un proceso sobrescribe los archivos .vcxproj , mientras que otro intenta leerlos. Hola, XmlException .

Sergey descubrió todo esto cuando agregó al probador la capacidad de cambiar al modo de trabajar con otro conjunto de registros estándar. La necesidad de esto surgió después de agregar el conjunto de reglas MISRA al analizador. Puede cambiar directamente en la interfaz, mientras el usuario ve la ventana:

Cuadro 14

Después de lo cual SelfTester se reinicia . Bueno, antes, aparentemente, los usuarios de alguna manera emularon el problema ellos mismos, comenzando el probador nuevamente.

Debriefing y conclusiones


Por supuesto, eliminamos, o mejor dicho, deshabilitamos la optimización creada anteriormente. Además, fue mucho más fácil que hacer algún tipo de sincronización entre el resto del probador. Y todo comenzó a funcionar bien, como antes. Y como medida adicional, se agregó la protección descrita anteriormente contra el lanzamiento simultáneo del probador.

Ya escribí anteriormente sobre nuestros principales errores durante la búsqueda del problema, por lo que la autoflagelación es suficiente. También somos personas y, por lo tanto, estamos equivocados. Es importante aprender de sus errores y sacar conclusiones. Las conclusiones aquí son bastante simples:

  • Es necesario rastrear y evaluar el crecimiento de la complejidad de la tarea;
  • Parar a tiempo;
  • Trate de ver el problema de manera más amplia, ya que con el tiempo la vista está "borrosa" y el ángulo de visión se estrecha;
  • No tengas miedo de eliminar el código viejo o innecesario.

Ahora seguro, eso es todo. Gracias por leer A todos los códigos sin esperanza!



Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Sergey Khrenov. Lo mejor es enemigo de lo bueno .

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


All Articles