
Cet article raconte comment nous avons décidé d'améliorer notre outil SelfTester interne que nous appliquons pour tester la qualité de l'analyseur PVS-Studio. L'amélioration était simple et semblait utile, mais nous a causé des ennuis. Plus tard, il s'est avéré que nous ferions mieux d'abandonner l'idée.
Selftester
Nous développons et promouvons l'analyseur de code statique PVS-Studio pour C, C ++, C # et Java. Pour tester la qualité de notre analyseur, nous utilisons des outils internes, génériquement appelés SelfTester. Nous avons créé une version SelfTester distincte pour chaque langue prise en charge. Cela est dû aux spécificités des tests, et c'est juste plus pratique. Ainsi, nous avons actuellement trois outils internes SelfTester dans notre entreprise pour C \ C ++, C # et Java, respectivement. De plus, je vais vous parler de la version Windows de SelfTester pour les projets Visual Studio C \ C ++, en l'appelant simplement SelfTester. Ce testeur a été le premier d'une gamme d'outils internes similaires, c'est le plus avancé et le plus complexe de tous.
Comment fonctionne SelfTester? L'idée est simple: prendre un pool de projets de test (nous utilisons de vrais projets open source) et les analyser à l'aide de PVS-Studio. Par conséquent, un journal d'analyseur est généré pour chaque projet. Ce journal est comparé au journal de
référence du même projet. Lors de la comparaison des journaux, SelfTester crée un
résumé des journaux comparant d'une manière conviviale pour les développeurs.
Après avoir étudié le résumé, un développeur conclut sur les changements dans le comportement de l'analyseur en fonction du nombre et du type d'avertissements, de la vitesse de travail, des erreurs internes de l'analyseur, etc. Toutes ces informations sont très importantes: elles vous permettent de savoir comment l'analyseur gère son travail.
Sur la base du résumé de la comparaison des journaux, un développeur introduit des modifications dans le cœur de l'analyseur (par exemple, lors de la création d'une nouvelle règle de diagnostic) et contrôle immédiatement le résultat de ses modifications. Si un développeur n'a plus de problème avec la comparaison d'un journal normal, il crée une
référence de journal d'avertissements en cours pour un projet. Sinon, le travail continue.
Ainsi, la tâche de SelfTester est de travailler avec un pool de projets de test (en passant, il y en a plus de 120 pour C / C ++). Les projets du pool sont sélectionnés sous forme de solutions Visual Studio. Cela est fait afin de vérifier en outre le travail de l'analyseur sur différentes versions de Visual Studio, qui prennent en charge l'analyseur (à ce stade de Visual Studio 2010 à Visual Studio 2019).
Remarque: plus loin, je séparerai les concepts
solution et
projet , en considérant un projet comme faisant partie d'une solution.
L'interface de SelfTester se présente comme suit:
Sur la gauche, il y a une liste de solutions, sur la droite - les résultats d'une vérification pour chaque version de Visual Studio.
Les étiquettes grises "Non pris en charge" indiquent qu'une solution ne prend pas en charge une version choisie de Visual Studio ou qu'elle n'a pas été convertie pour cette version. Certaines solutions ont une configuration dans un pool, qui indique une version spécifique de Visual Studio pour une vérification. Si aucune version n'est spécifiée, une solution sera mise à jour pour toutes les versions ultérieures de Visual Studio. Un exemple d'une telle solution se trouve sur la capture d'écran - "smart_ptr_check.sln" (une vérification est effectuée pour toutes les versions de Visual Studio).
Une étiquette verte "OK" indique qu'une vérification régulière n'a pas détecté de différences avec le journal de référence. Une étiquette rouge "Diff" indique les différences. Ces étiquettes doivent faire l'objet d'une attention particulière. Après avoir cliqué deux fois sur l'étiquette requise, la solution choisie sera ouverte dans une version Visual Studio associée. Une fenêtre avec un journal des avertissements y sera également ouverte. Les boutons de contrôle en bas vous permettent de relancer l'analyse de la solution sélectionnée ou de toutes les solutions, de faire référence au journal choisi (ou en une seule fois), etc.
Les résultats de SelfTester sont toujours dupliqués dans le rapport html (rapport diffs)
En plus de l'interface graphique, SelfTester dispose également de modes automatisés pour les exécutions de construction de nuit. Cependant, le modèle d'utilisation habituel répété par le développeur s'exécute par un développeur pendant la journée de travail. Par conséquent, l'une des caractéristiques les plus importantes de SelfTester est la vitesse de travail.
Pourquoi la vitesse est importante:
- Les performances de chaque étape sont assez cruciales en termes de tests de nuit. De toute évidence, plus les tests réussissent rapidement, mieux c'est. À l'heure actuelle, le temps de performance moyen de SelfTester dépasse 2 heures;
- Lors de l'exécution de SelfTester pendant la journée, un développeur doit attendre moins le résultat, ce qui augmente la productivité de sa main-d'œuvre.
C'est l'accélération des performances qui est devenue la raison des améliorations cette fois.
Multi-threading dans SelfTester
SelfTester a été initialement créé comme une application multithread avec la possibilité de tester simultanément plusieurs solutions. La seule limitation était que vous ne pouviez pas vérifier simultanément la même solution pour différentes versions de Visual Studio, car de nombreuses solutions doivent être mises à jour vers certaines versions de Visual Studio avant de tester. Au cours de celui-ci, des modifications sont introduites directement dans les fichiers des projets
.vcxproj , ce qui entraîne des erreurs lors de l'exécution parallèle.
Afin de rendre le travail plus efficace, SelfTester utilise un planificateur de tâches intelligent pour définir une valeur strictement limitée de threads parallèles et la maintenir.
Le planificateur est utilisé sur deux niveaux. Le premier est le niveau des
solutions , il est utilisé pour commencer à tester la solution
.sln à l'aide de l'utilitaire
PVS-Studio_Cmd.exe . Le même ordonnanceur, mais avec un autre paramètre de
degré de
parallélisme , est utilisé dans
PVS-Studio_Cmd.exe (au niveau du test des
fichiers source).
Le degré de parallélisme est un paramètre qui indique combien de threads parallèles doivent être exécutés simultanément.
Quatre et
huit valeurs par défaut ont été choisies pour le degré de parallélisme des solutions et du niveau des fichiers, respectivement. Ainsi, le nombre de threads parallèles dans cette implémentation doit être de 32 (4 solutions testées simultanément et 8 fichiers). Ce paramètre nous semble optimal pour le travail de l'analyseur sur un processeur à huit cœurs.
Un développeur peut définir lui-même d'autres valeurs du degré de parallélisme en fonction des performances de son ordinateur ou des tâches en cours. Si un développeur ne spécifie pas ce paramètre, le nombre de processeurs de système logique sera choisi par défaut.
Remarque: supposons en outre que nous traitons du degré de parallélisme par défaut.
Le planificateur
LimitedConcurrencyLevelTaskScheduler est hérité de
System.Threading.Tasks.TaskScheduler et affiné pour fournir le niveau de parallélisme maximal lors de l'utilisation de
ThreadPool . Hiérarchie d'héritage:
LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... }
PausableTaskScheduler vous permet de suspendre les performances des tâches et, en plus de cela,
LimitedConcurrencyLevelTaskScheduler fournit un contrôle intellectuel de la file d'attente des tâches et la planification de leurs performances, en tenant compte du degré de parallélisme, de l'étendue des tâches planifiées et d'autres facteurs. Un planificateur est utilisé lors de l'exécution des tâches
LimitedConcurrencyLevelTaskScheduler .
Raisons des améliorations
Le procédé décrit ci-dessus présente un inconvénient: il n'est pas optimal lorsqu'il s'agit de solutions de tailles différentes. Et la taille des solutions dans le pool de tests est
très variée: de 8 Ko à 4 Go - la taille d'un dossier avec une solution et de 1 à plusieurs milliers de fichiers de code source dans chacun.
Le planificateur place les solutions dans la file d'attente les unes après les autres, sans aucun composant intelligent. Permettez-moi de vous rappeler que par défaut, pas plus de quatre solutions peuvent être testées simultanément. Si quatre grandes solutions sont actuellement testées (le nombre de fichiers dans chacune est supérieur à huit), il est supposé que nous travaillons efficacement car nous utilisons autant de threads que possible (32).
Mais imaginons une situation assez fréquente, lorsque plusieurs petites solutions sont testées. Par exemple, une solution est volumineuse et contient 50 fichiers (le nombre maximum de threads sera utilisé), tandis que les trois autres solutions contiennent trois, quatre, cinq fichiers chacune. Dans ce cas, nous n'utiliserons que 20 threads (8 + 3 + 4 + 5). Nous obtenons une sous-utilisation du temps du processeur et des performances globales réduites.
Remarque : en fait, le goulot d'étranglement est généralement le sous-système de disque, pas le processeur.
Améliorations
L'amélioration qui va de soi dans ce cas est le classement de la liste des solutions testées. Nous devons obtenir une utilisation optimale du nombre défini de threads exécutés simultanément (32), en passant à tester des projets avec le nombre correct de fichiers.
Reprenons notre exemple de test de quatre solutions avec le nombre de fichiers suivant dans chacune: 50, 3, 4 et 5. La tâche qui vérifie une solution de
trois fichiers est susceptible de fonctionner le plus rapidement. Il serait préférable d'ajouter une solution avec huit fichiers ou plus au lieu de celle-ci (afin d'utiliser au maximum les threads disponibles pour cette solution). De cette façon, nous utiliserons 25 threads à la fois (8 +
8 + 4 + 5). Pas mal. Cependant, sept threads ne sont toujours pas impliqués. Et voici l'idée d'un autre raffinement, qui consiste à supprimer la limite de quatre threads sur les solutions de test. Parce que nous pouvons maintenant ajouter non pas une, mais plusieurs solutions, en utilisant 32 threads. Imaginons que nous ayons deux autres solutions de trois et quatre fichiers chacune. L'ajout de ces tâches comblera complètement le "vide" des threads inutilisés et il y en aura 32 (8 + 8 + 4 + 5 +
3 +
4 ).
J'espère que l'idée est claire. En fait, la mise en œuvre de ces améliorations n'a pas non plus exigé beaucoup d'efforts. Tout a été fait en une journée.
Nous avions besoin de retravailler la classe de tâches: héritant de
System.Threading.Tasks.Task et affectation du champ "weight". Nous utilisons un algorithme simple pour définir le poids d'une solution: si le nombre de fichiers est inférieur à huit, le poids est égal à ce nombre (par exemple, 5). Si le nombre est supérieur ou égal à huit, le poids sera égal à huit.
Nous avons également dû élaborer l'ordonnanceur: lui apprendre à choisir les solutions avec le poids nécessaire afin d'atteindre la valeur maximale de 32 threads. Nous avons également dû autoriser plus de quatre threads pour les tests de solutions simultanés.
Enfin, nous avions besoin d'une étape préliminaire pour analyser toutes les solutions du pool (évaluation à l'aide de l'API MSBuild) pour évaluer et définir le poids des solutions (obtenir le nombre de fichiers avec le code source).
Résultat
Je pense qu'après une si longue introduction, vous avez déjà deviné que rien n'en est sorti.
C'est bien que les améliorations aient été simples et rapides.
Voici la partie de l'article où je vais vous parler de ce qui nous a «causé de nombreux ennuis» et de tout ce qui s'y rapporte.
Effets secondaires
Ainsi, un résultat négatif est également un résultat. Il s'est avéré que le nombre de grandes solutions dans le pool
dépasse de loin le nombre de petites (moins de huit fichiers). Dans ce cas, ces améliorations n'ont pas d'effet très notable, car elles sont presque invisibles: tester de petits projets prend très peu de temps par rapport au temps, nécessaire pour les grands projets.
Cependant, nous avons décidé de laisser le nouveau raffinement comme "non dérangeant" et potentiellement utile. En outre, le pool de solutions de test est constamment renouvelé, donc à l'avenir, la situation changera peut-être.
Et puis ...
L'un des développeurs s'est plaint du crash du SelfTester. Eh bien, la vie arrive. Pour éviter que cette erreur ne soit perdue, nous avons créé un incident interne (ticket) avec le nom "Exception lors de l'utilisation de SelfTester". L'erreur s'est produite lors de l'évaluation du projet. Bien qu'un grand nombre de fenêtres avec des erreurs aient indiqué le problème dans le gestionnaire d'erreurs. Mais cela a été rapidement éliminé et au cours de la semaine suivante, rien ne s'est écrasé. Soudain, un autre utilisateur s'est plaint de SelfTester. Encore une fois, l'erreur d'une évaluation de projet:
Cette fois, la pile contenait beaucoup d'informations utiles - l'erreur était au format xml. Il est probable que lors de la manipulation du fichier du projet
Proto_IRC.vcxproj (sa représentation xml), quelque chose soit arrivé au fichier lui-même, c'est pourquoi
XmlTextReader n'a pas pu le gérer.
Le fait d'avoir deux erreurs en un laps de temps assez court nous a fait examiner de plus près le problème. De plus, comme je l'ai dit plus haut, SelfTester est très activement utilisé par les développeurs.
Pour commencer, nous avons analysé le dernier crash. Malheureusement, nous n'avons rien trouvé de suspect. Juste au cas où nous aurions demandé aux développeurs (utilisateurs SelfTester) de garder un œil et de signaler les erreurs possibles.
Point important: le code erroné a été réutilisé dans SelfTester. Il était à l'origine utilisé pour évaluer des projets dans l'analyseur lui
- même (
PVS-Studio_Cmd.exe ). C'est pourquoi l'attention portée au problème a augmenté. Cependant, il n'y a pas eu de tels plantages dans l'analyseur.
Pendant ce temps, le ticket sur les problèmes avec SelfTester a été complété par de nouvelles erreurs:
XmlException à nouveau. De toute évidence, il existe des threads concurrents quelque part qui fonctionnent avec la lecture et l'écriture des fichiers de projet. SelfTester fonctionne avec des projets dans les cas suivants:
- Évaluation des projets au cours du calcul préliminaire des poids des solutions: une nouvelle étape qui a d'abord suscité la suspicion;
- La mise à jour des projets vers les versions de Visual Studio nécessaires: est effectuée juste avant le test (les projets n'interfèrent pas) et ne doit pas affecter le processus de travail.
- Évaluation des projets pendant les tests: un mécanisme thread-safe bien établi, réutilisé à partir de PVS-Studio_Cmd.exe ;
- Restauration des fichiers de projet (remplacement des fichiers .vcxproj modifiés par des fichiers de référence initiaux) lors de la fermeture de SelfTester, car les fichiers de projet peuvent se mettre à jour vers les versions de Visual Studio nécessaires pendant le travail. C'est une dernière étape, qui n'a aucun impact sur les autres mécanismes.
Les soupçons sont tombés sur le nouveau code ajouté pour l'optimisation (calcul du poids). Mais son enquête sur le code a montré que si un utilisateur exécute l'analyse juste après le démarrage de SelfTester, le testeur attend toujours correctement jusqu'à la fin de la pré-évaluation. Cet endroit avait l'air sûr.
Encore une fois, nous n'avons pas pu identifier la source du problème.
La douleur
Tout le mois suivant, SelfTester a continué de planter. Le ticket continuait de se remplir de données, mais il n'était pas clair que faire de ces données. La plupart des plantages étaient avec la même
exception XmlException. Parfois, il y avait autre chose, mais sur le même code réutilisé de
PVS-Studio_Cmd.exe .
Traditionnellement, les outils internes ne sont pas soumis à des exigences très élevées, nous avons donc continué à dérouter les erreurs de SelfTester sur un principe résiduel. De temps en temps, différentes personnes se sont impliquées (pendant tout l'incident, six personnes ont travaillé sur le problème, dont deux internes). Cependant, nous avons dû nous laisser distraire par cette tâche.
Notre première erreur. En fait, à ce stade, nous aurions pu résoudre ce problème une fois pour toutes. Comment? Il était clair que l'erreur était due à une nouvelle optimisation. Après tout, avant que tout fonctionne bien, et le code réutilisé ne peut clairement pas être si mauvais. De plus, cette optimisation n'avait apporté aucun avantage. Alors, que fallait-il faire?
Supprimez cette optimisation. Comme vous le comprenez probablement, cela n'a pas été fait. Nous avons continué à travailler sur le problème, que nous avons créé nous-mêmes. Nous avons continué à chercher la réponse: "COMMENT ???" Comment ça plante? Il semblait être écrit correctement.
Notre deuxième erreur. D'autres personnes se sont impliquées dans la résolution du problème
. C'est une très, très grosse erreur. Non seulement le problème n'a pas été résolu, mais il a également nécessité des ressources supplémentaires inutiles. Oui, de nouvelles personnes ont apporté de nouvelles idées, mais il a fallu beaucoup de temps pour mettre en œuvre (pour rien) ces idées. À un moment donné, nos stagiaires ont écrit des programmes de test émulant l'évaluation d'un seul et même projet dans différents threads avec modification parallèle d'un projet dans un autre projet. Ça n'a pas aidé. Nous avons seulement découvert que l'API MSBuild était thread-safe à l'intérieur, ce que nous connaissions déjà. Nous avons également ajouté l'enregistrement automatique de mini vidage lorsque l'exception
XmlException se produit. Nous avions quelqu'un qui déboguait tout cela. Pauvre gars! Il y a eu des discussions, nous avons fait d'autres choses inutiles.
Enfin, troisième erreur. Savez-vous combien de temps s'est écoulé entre le moment où le problème SelfTester s'est produit et celui où il a été résolu? Eh bien, vous pouvez vous compter. Le billet a été créé le 17/09/2018 et fermé le 20/02/2019. Il y a eu plus de 40 commentaires! Les gars, c'est beaucoup de temps! Nous
nous sommes
permis d'être occupés pendant cinq mois avec CECI. Dans le même temps, nous étions occupés à prendre en charge Visual Studio 2019, à ajouter le support du langage Java, à introduire la norme MISRA C / C ++, à améliorer l'analyseur C #, à participer activement aux conférences, à rédiger un tas d'articles, etc. Toutes ces activités ont reçu moins de temps de la part des développeurs en raison d'une erreur stupide dans SelfTester.
Mes amis, apprenez de nos erreurs et ne faites jamais ça. Nous non plus.
Ça y est, j'ai fini.
D'accord, c'était une blague, je vais vous dire quel était le problème avec SelfTester :)
Bingo!
Heureusement, il y avait une personne parmi nous qui avait les yeux clairs (mon collègue Sergey Vasiliev), qui vient de regarder le problème sous un angle très différent (et aussi - il a eu un peu de chance). Et si tout va bien à l'intérieur du SelfTester, mais que quelque chose de l'extérieur bloque les projets? Habituellement, nous n'avions rien lancé avec SelfTester, dans certains cas, nous contrôlions strictement l'environnement d'exécution. Dans ce cas, ce "quelque chose" pourrait être SelfTester lui-même, mais une instance différente.
Lorsque vous quittez SelfTester, le thread qui restaure les fichiers de projet à partir des références continue de fonctionner pendant un certain temps. À ce stade, le testeur peut être relancé. La protection contre les exécutions simultanées de plusieurs instances SelfTester a été ajoutée
ultérieurement et se présente désormais comme suit:
Mais à ce moment-là, nous ne l'avions pas.
Des noix, mais c'est vrai - pendant près de six mois de tourments, personne n'y a prêté attention. La restauration de projets à partir de références est une procédure d'arrière-plan assez rapide, mais malheureusement pas assez rapide pour ne pas interférer avec la relance de SelfTester. Et que se passe-t-il lorsque nous le lançons? C'est vrai, calculer le poids des solutions. Un processus réécrit les fichiers
.vcxproj tandis qu'un autre essaie de les lire. Dites bonjour à
XmlException .
Sergey a découvert tout cela en ajoutant au testeur la possibilité de passer à un autre ensemble de journaux de référence. Cela est devenu nécessaire après l'ajout d'un ensemble de règles MISRA dans l'analyseur. Vous pouvez basculer directement dans l'interface, pendant que l'utilisateur voit cette fenêtre:
Après cela,
SelfTester redémarre. Et plus tôt, apparemment, les utilisateurs ont eux-mêmes émulé le problème, exécutant à nouveau le testeur.
Blamestorming et conclusions
Bien sûr, nous avons supprimé (c'est-à-dire désactivé) l'optimisation créée précédemment. De plus, c'était beaucoup plus facile que d'effectuer une sorte de synchronisation entre les redémarrages du testeur par lui-même. Et tout a commencé à fonctionner parfaitement, comme avant. Et comme mesure supplémentaire, nous avons ajouté la protection ci-dessus contre le lancement simultané du testeur.
J'ai déjà écrit ci-dessus sur nos principales erreurs lors de la recherche du problème, donc assez d'auto-flagellation. Nous sommes des êtres humains, donc nous pourrions nous tromper. Il est important d'apprendre de ses propres erreurs et de tirer des conclusions. Les conclusions de ce cas sont assez simples:
- Nous devons surveiller et estimer la complexité des tâches;
- Parfois, nous devons nous arrêter à un moment donné;
- Essayez de regarder le problème plus largement. Au fil du temps, on peut obtenir une vision tunnel de l'affaire alors qu'elle nécessite une nouvelle perspective.
- N'ayez pas peur de supprimer le code ancien ou inutile.
Voilà, cette fois, j'ai définitivement terminé. Merci d'avoir lu jusqu'à la fin. Je vous souhaite du code sans code!